diff --git a/.env.ci b/.env.ci index 5b3d9af2..2ce692c1 100644 --- a/.env.ci +++ b/.env.ci @@ -3,6 +3,7 @@ APP_ENV=local APP_KEY= APP_DEBUG=true APP_URL=http://localhost +SESSION_SECURE_COOKIE=false LOG_CHANNEL=stack LOG_DEPRECATIONS_CHANNEL=null diff --git a/.env.example b/.env.example index ed50bd57..8d1acdf3 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,7 @@ APP_KEY=base64:UNQNf1SXeASNkWux01Rj8EnHYx8FO0kAxWNDwktclkk= APP_DEBUG=true APP_URL=https://solidtime.test APP_FORCE_HTTPS=true +SESSION_SECURE_COOKIE=true SUPER_ADMINS=admin@example.com diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index a5d7dd47..740d2d53 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -5,7 +5,7 @@ jobs: runs-on: ubuntu-latest services: - pgsql: + pgsql_test: image: postgres:15 env: PGPASSWORD: 'root' diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 141b73e2..5db1823b 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -11,7 +11,7 @@ jobs: services: mailpit: image: 'axllent/mailpit:latest' - pgsql: + pgsql_test: image: postgres:15 env: PGPASSWORD: 'root' diff --git a/app/Http/Requests/V1/TimeEntry/TimeEntryIndexRequest.php b/app/Http/Requests/V1/TimeEntry/TimeEntryIndexRequest.php index 92d9a591..de310a2d 100644 --- a/app/Http/Requests/V1/TimeEntry/TimeEntryIndexRequest.php +++ b/app/Http/Requests/V1/TimeEntry/TimeEntryIndexRequest.php @@ -48,7 +48,8 @@ public function rules(): array ], // Filter only time entries that are active (have no end date, are still running) 'active' => [ - 'boolean', + 'string', + 'in:true,false', ], // Limit the number of returned time entries 'limit' => [ diff --git a/app/Service/Import/ImportDatabaseHelper.php b/app/Service/Import/ImportDatabaseHelper.php index 12c785e6..3c02285a 100644 --- a/app/Service/Import/ImportDatabaseHelper.php +++ b/app/Service/Import/ImportDatabaseHelper.php @@ -43,11 +43,15 @@ class ImportDatabaseHelper private int $createdCount; + /** + * @var array> + */ private array $validate; /** * @param class-string $model * @param array $identifiers + * @param array> $validate */ public function __construct(string $model, array $identifiers, bool $attachToExisting = false, ?Closure $queryModifier = null, ?Closure $afterCreate = null, array $validate = []) { diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index b59a486d..2e057b52 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -8,6 +8,7 @@ use App\Models\Client; use App\Models\Organization; use App\Models\Project; +use App\Models\Tag; use App\Models\Task; use App\Models\TimeEntry; use App\Models\User; @@ -109,6 +110,7 @@ private function deleteAll(): void { DB::table((new TimeEntry())->getTable())->delete(); DB::table((new Task())->getTable())->delete(); + DB::table((new Tag())->getTable())->delete(); DB::table((new Project())->getTable())->delete(); DB::table((new Client())->getTable())->delete(); DB::table((new User())->getTable())->delete(); diff --git a/openapi.json.client.ts b/openapi.json.client.ts index 5366233e..d9208c6f 100644 --- a/openapi.json.client.ts +++ b/openapi.json.client.ts @@ -10,6 +10,9 @@ const ClientResource = z }) .passthrough(); const ClientCollection = z.array(ClientResource); +const v1_import_import_Body = z + .object({ type: z.string(), data: z.string() }) + .passthrough(); const OrganizationResource = z .object({ id: z.string(), name: z.string(), is_personal: z.string() }) .passthrough(); @@ -72,10 +75,21 @@ const updateTimeEntry_Body = z tags: z.union([z.array(z.string()), z.null()]).optional(), }) .passthrough(); +const UserResource = z + .object({ + id: z.string(), + name: z.string(), + email: z.string(), + role: z.string(), + is_placeholder: z.boolean(), + }) + .passthrough(); +const UserCollection = z.array(UserResource); export const schemas = { ClientResource, ClientCollection, + v1_import_import_Body, OrganizationResource, ProjectResource, ProjectCollection, @@ -87,6 +101,8 @@ export const schemas = { TimeEntryCollection, createTimeEntry_Body, updateTimeEntry_Body, + UserResource, + UserCollection, }; const endpoints = makeApi([ @@ -306,6 +322,76 @@ const endpoints = makeApi([ }, ], }, + { + method: 'post', + path: '/v1/organizations/:organization/import', + alias: 'v1.import.import', + requestFormat: 'json', + parameters: [ + { + name: 'body', + type: 'Body', + schema: v1_import_import_Body, + }, + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z + .object({ + report: z + .object({ + clients: z + .object({ created: z.number().int() }) + .passthrough(), + projects: z + .object({ created: z.number().int() }) + .passthrough(), + tasks: z + .object({ created: z.number().int() }) + .passthrough(), + 'time-entries': z + .object({ created: z.number().int() }) + .passthrough(), + tags: z + .object({ created: z.number().int() }) + .passthrough(), + users: z + .object({ created: z.number().int() }) + .passthrough(), + }) + .passthrough(), + }) + .passthrough(), + errors: [ + { + status: 400, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 422, + description: `Validation error`, + schema: z + .object({ + message: z.string(), + errors: z.record(z.array(z.string())), + }) + .passthrough(), + }, + ], + }, { method: 'get', path: '/v1/organizations/:organization/projects', @@ -664,7 +750,7 @@ const endpoints = makeApi([ { name: 'active', type: 'Query', - schema: z.string().optional(), + schema: z.enum(['true', 'false']).optional(), }, { name: 'limit', @@ -824,6 +910,78 @@ const endpoints = makeApi([ }, ], }, + { + method: 'get', + path: '/v1/organizations/:organization/users', + alias: 'v1.users.index', + requestFormat: 'json', + parameters: [ + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z.object({ data: UserCollection }).passthrough(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 422, + description: `Validation error`, + schema: z + .object({ + message: z.string(), + errors: z.record(z.array(z.string())), + }) + .passthrough(), + }, + ], + }, + { + method: 'post', + path: '/v1/organizations/:organization/users/:user/invite-placeholder', + alias: 'v1.users.invite-placeholder', + requestFormat: 'json', + parameters: [ + { + name: 'body', + type: 'Body', + schema: z.object({}).partial().passthrough(), + }, + { + name: 'organization', + type: 'Path', + schema: z.string().uuid(), + }, + { + name: 'user', + type: 'Path', + schema: z.string().uuid(), + }, + ], + response: z.string(), + errors: [ + { + status: 403, + description: `Authorization error`, + schema: z.object({ message: z.string() }).passthrough(), + }, + { + status: 404, + description: `Not found`, + schema: z.object({ message: z.string() }).passthrough(), + }, + ], + }, ]); export const api = new Zodios('http://solidtime.test/api', endpoints); diff --git a/phpunit.xml b/phpunit.xml index f5ab132a..82235621 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -22,7 +22,6 @@ - diff --git a/playwright.config.ts b/playwright.config.ts index 5a1b230e..1c552630 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -20,7 +20,7 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', + reporter: process.env.CI ? 'list' : 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */