From 6bb6ce4e28427c44f62328e23a31f3d0409d8948 Mon Sep 17 00:00:00 2001 From: korridor <26689068+korridor@users.noreply.github.com> Date: Sun, 28 Jan 2024 18:57:28 +0100 Subject: [PATCH] Added telescope, basic project endpoint, basic filament resources, GitHub actions --- .env.ci | 5 + .github/workflows/npm-build.yml | 4 +- .github/workflows/npm-lint.yml | 4 +- .github/workflows/npm-typecheck.yml | 4 +- .github/workflows/phpstan.yml | 24 ++ .github/workflows/phpunit.yml | 43 +++ .github/workflows/pint.yml | 6 +- .github/workflows/playwright.yml | 4 +- .gitignore | 1 + app/Filament/Resources/ClientResource.php | 77 ++++ .../ClientResource/Pages/CreateClient.php | 13 + .../ClientResource/Pages/EditClient.php | 21 ++ .../ClientResource/Pages/ListClients.php | 21 ++ .../Resources/OrganizationResource.php | 82 +++++ .../Pages/CreateOrganization.php | 13 + .../Pages/EditOrganization.php | 21 ++ .../Pages/ListOrganizations.php | 21 ++ app/Filament/Resources/ProjectResource.php | 88 +++++ .../ProjectResource/Pages/CreateProject.php | 13 + .../ProjectResource/Pages/EditProject.php | 21 ++ .../ProjectResource/Pages/ListProjects.php | 21 ++ app/Filament/Resources/TagResource.php | 85 +++++ .../Resources/TagResource/Pages/CreateTag.php | 13 + .../Resources/TagResource/Pages/EditTag.php | 21 ++ .../Resources/TagResource/Pages/ListTags.php | 21 ++ app/Filament/Resources/TaskResource.php | 73 ++++ .../TaskResource/Pages/CreateTask.php | 13 + .../Resources/TaskResource/Pages/EditTask.php | 21 ++ .../TaskResource/Pages/ListTasks.php | 21 ++ app/Filament/Resources/TimeEntryResource.php | 112 ++++++ .../Pages/CreateTimeEntry.php | 13 + .../TimeEntryResource/Pages/EditTimeEntry.php | 21 ++ .../Pages/ListTimeEntries.php | 21 ++ app/Filament/Resources/UserResource.php | 89 +++++ .../UserResource/Pages/CreateUser.php | 13 + .../Resources/UserResource/Pages/EditUser.php | 21 ++ .../UserResource/Pages/ListUsers.php | 21 ++ app/Http/Controllers/Api/V1/Controller.php | 22 ++ .../Controllers/Api/V1/ProjectController.php | 77 ++++ app/Http/Kernel.php | 4 +- app/Http/Middleware/ForceJsonResponse.php | 24 ++ .../V1/Project/ProjectStoreRequest.php | 29 ++ .../V1/Project/ProjectCollection.php | 17 + .../Resources/V1/Project/ProjectResource.php | 28 ++ app/Models/Project.php | 2 + app/Models/TimeEntry.php | 6 + app/Policies/OrganizationPolicy.php | 35 +- app/Providers/AppServiceProvider.php | 24 +- app/Providers/Filament/AdminPanelProvider.php | 18 + app/Providers/JetstreamServiceProvider.php | 26 +- app/Providers/RouteServiceProvider.php | 1 + app/Providers/TelescopeServiceProvider.php | 67 ++++ composer.json | 6 +- composer.lock | 328 ++++++++++++------ config/app.php | 1 + config/telescope.php | 191 ++++++++++ ...5_21_100000_create_organizations_table.php | 2 +- ..._200000_create_organization_user_table.php | 2 +- ..._create_organization_invitations_table.php | 2 +- database/seeders/DatabaseSeeder.php | 31 +- phpunit.xml | 2 +- routes/api.php | 15 +- tests/Feature/UpdateTeamMemberRoleTest.php | 6 +- tests/Unit/Database/MigrationTest.php | 27 ++ tests/Unit/Database/SeederTest.php | 27 ++ .../Endpoint/Api/V1/ProjectEndpointTest.php | 106 ++++++ 66 files changed, 2080 insertions(+), 132 deletions(-) create mode 100644 .github/workflows/phpstan.yml create mode 100644 .github/workflows/phpunit.yml create mode 100644 app/Filament/Resources/ClientResource.php create mode 100644 app/Filament/Resources/ClientResource/Pages/CreateClient.php create mode 100644 app/Filament/Resources/ClientResource/Pages/EditClient.php create mode 100644 app/Filament/Resources/ClientResource/Pages/ListClients.php create mode 100644 app/Filament/Resources/OrganizationResource.php create mode 100644 app/Filament/Resources/OrganizationResource/Pages/CreateOrganization.php create mode 100644 app/Filament/Resources/OrganizationResource/Pages/EditOrganization.php create mode 100644 app/Filament/Resources/OrganizationResource/Pages/ListOrganizations.php create mode 100644 app/Filament/Resources/ProjectResource.php create mode 100644 app/Filament/Resources/ProjectResource/Pages/CreateProject.php create mode 100644 app/Filament/Resources/ProjectResource/Pages/EditProject.php create mode 100644 app/Filament/Resources/ProjectResource/Pages/ListProjects.php create mode 100644 app/Filament/Resources/TagResource.php create mode 100644 app/Filament/Resources/TagResource/Pages/CreateTag.php create mode 100644 app/Filament/Resources/TagResource/Pages/EditTag.php create mode 100644 app/Filament/Resources/TagResource/Pages/ListTags.php create mode 100644 app/Filament/Resources/TaskResource.php create mode 100644 app/Filament/Resources/TaskResource/Pages/CreateTask.php create mode 100644 app/Filament/Resources/TaskResource/Pages/EditTask.php create mode 100644 app/Filament/Resources/TaskResource/Pages/ListTasks.php create mode 100644 app/Filament/Resources/TimeEntryResource.php create mode 100644 app/Filament/Resources/TimeEntryResource/Pages/CreateTimeEntry.php create mode 100644 app/Filament/Resources/TimeEntryResource/Pages/EditTimeEntry.php create mode 100644 app/Filament/Resources/TimeEntryResource/Pages/ListTimeEntries.php create mode 100644 app/Filament/Resources/UserResource.php create mode 100644 app/Filament/Resources/UserResource/Pages/CreateUser.php create mode 100644 app/Filament/Resources/UserResource/Pages/EditUser.php create mode 100644 app/Filament/Resources/UserResource/Pages/ListUsers.php create mode 100644 app/Http/Controllers/Api/V1/Controller.php create mode 100644 app/Http/Controllers/Api/V1/ProjectController.php create mode 100644 app/Http/Middleware/ForceJsonResponse.php create mode 100644 app/Http/Requests/V1/Project/ProjectStoreRequest.php create mode 100644 app/Http/Resources/V1/Project/ProjectCollection.php create mode 100644 app/Http/Resources/V1/Project/ProjectResource.php create mode 100644 app/Providers/TelescopeServiceProvider.php create mode 100644 config/telescope.php create mode 100644 tests/Unit/Database/MigrationTest.php create mode 100644 tests/Unit/Database/SeederTest.php create mode 100644 tests/Unit/Endpoint/Api/V1/ProjectEndpointTest.php diff --git a/.env.ci b/.env.ci index e7c9623b..986ec588 100644 --- a/.env.ci +++ b/.env.ci @@ -9,6 +9,11 @@ LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug DB_CONNECTION=sqlite +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_DATABASE=laravel +DB_USERNAME=root +DB_PASSWORD=root BROADCAST_DRIVER=log CACHE_DRIVER=file diff --git a/.github/workflows/npm-build.yml b/.github/workflows/npm-build.yml index 7c2d0e38..9e43e262 100644 --- a/.github/workflows/npm-build.yml +++ b/.github/workflows/npm-build.yml @@ -8,7 +8,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: "Checkout code" + uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 with: php-version: '8.3.1' diff --git a/.github/workflows/npm-lint.yml b/.github/workflows/npm-lint.yml index 141e3248..1c280e98 100644 --- a/.github/workflows/npm-lint.yml +++ b/.github/workflows/npm-lint.yml @@ -8,7 +8,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: "Checkout code" + uses: actions/checkout@v4 + - name: Use Node.js uses: actions/setup-node@v3 with: diff --git a/.github/workflows/npm-typecheck.yml b/.github/workflows/npm-typecheck.yml index 3eaa079e..795e0c44 100644 --- a/.github/workflows/npm-typecheck.yml +++ b/.github/workflows/npm-typecheck.yml @@ -8,7 +8,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: "Checkout code" + uses: actions/checkout@v4 + - name: Use Node.js uses: actions/setup-node@v3 with: diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 00000000..7510f0a5 --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,24 @@ +name: Static code analysis (PHPStan) +on: push +jobs: + phpstan: + runs-on: ubuntu-latest + + steps: + - name: "Checkout code" + uses: actions/checkout@v4 + + - name: "Setup PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv + coverage: none + + - name: "Run composer install" + run: composer install -n --prefer-dist + + - name: "Run PHPStan" + run: composer analyse + + diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 00000000..6c004531 --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,43 @@ +name: PHPUnit Tests +on: push +jobs: + phpunit: + runs-on: ubuntu-latest + + services: + pgsql: + image: postgres:15 + env: + PGPASSWORD: 'root' + POSTGRES_DB: 'testing' + POSTGRES_USER: 'root' + POSTGRES_PASSWORD: 'root' + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: "Checkout code" + uses: actions/checkout@v4 + + - name: "Setup PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv + coverage: none + + - name: "Run composer install" + run: composer install -n --prefer-dist + + - name: "Prepare Laravel Application" + run: | + cp .env.ci .env + php artisan key:generate + + - name: "Run PHPUnit" + run: composer test diff --git a/.github/workflows/pint.yml b/.github/workflows/pint.yml index 65ec88eb..b84db419 100644 --- a/.github/workflows/pint.yml +++ b/.github/workflows/pint.yml @@ -4,8 +4,10 @@ jobs: pint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - name: "laravel-pint" + - name: "Checkout code" + uses: actions/checkout@v4 + + - name: "Check code style" uses: aglipanci/laravel-pint-action@2.0.0 with: configPath: "pint.json" diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 48a004f5..b68a48d0 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -13,7 +13,9 @@ jobs: image: 'axllent/mailpit:latest' steps: - - uses: actions/checkout@v3 + - name: "Checkout code" + uses: actions/checkout@v4 + - uses: actions/setup-node@v3 with: node-version: 18 diff --git a/.gitignore b/.gitignore index 5b7bf48d..085d45fc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /public/storage /public/css /public/js +/public/vendor /lang/vendor /storage/*.key /vendor diff --git a/app/Filament/Resources/ClientResource.php b/app/Filament/Resources/ClientResource.php new file mode 100644 index 00000000..b708a6e8 --- /dev/null +++ b/app/Filament/Resources/ClientResource.php @@ -0,0 +1,77 @@ +schema([ + // + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('name') + ->label('Name') + ->searchable() + ->sortable(), + Tables\Columns\TextColumn::make('organization.name') + ->sortable() + ->label('Organization'), + Tables\Columns\TextColumn::make('created_at') + ->label('Created at') + ->sortable(), + Tables\Columns\TextColumn::make('updated_at') + ->label('Updated at') + ->sortable(), + ]) + ->defaultSort('created_at', 'desc') + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListClients::route('/'), + 'create' => Pages\CreateClient::route('/create'), + 'edit' => Pages\EditClient::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/ClientResource/Pages/CreateClient.php b/app/Filament/Resources/ClientResource/Pages/CreateClient.php new file mode 100644 index 00000000..be1c6d45 --- /dev/null +++ b/app/Filament/Resources/ClientResource/Pages/CreateClient.php @@ -0,0 +1,13 @@ +schema([ + Forms\Components\TextInput::make('name') + ->label('Name') + ->required() + ->maxLength(255), + Forms\Components\Toggle::make('Is personal?') + ->label('Is personal?') + ->required(), + Forms\Components\Select::make('owner_id') + ->relationship(name: 'owner', titleAttribute: 'email') + ->searchable(['name', 'email']) + ->required(), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('name') + ->searchable() + ->sortable(), + Tables\Columns\ToggleColumn::make('is_personal') + ->label('Is personal?') + ->sortable(), + Tables\Columns\TextColumn::make('owner.email') + ->sortable(), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListOrganizations::route('/'), + 'create' => Pages\CreateOrganization::route('/create'), + 'edit' => Pages\EditOrganization::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/OrganizationResource/Pages/CreateOrganization.php b/app/Filament/Resources/OrganizationResource/Pages/CreateOrganization.php new file mode 100644 index 00000000..5466056d --- /dev/null +++ b/app/Filament/Resources/OrganizationResource/Pages/CreateOrganization.php @@ -0,0 +1,13 @@ +schema([ + Forms\Components\TextInput::make('name') + ->label('Name') + ->required() + ->maxLength(255), + Forms\Components\TextInput::make('color') + ->label('Color') + ->required() + ->maxLength(255), + Forms\Components\Select::make('organization_id') + ->relationship(name: 'organization', titleAttribute: 'name') + ->searchable(['name']) + ->required(), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make('name') + ->searchable() + ->sortable(), + TextColumn::make('color') + ->sortable(), + TextColumn::make('organization.name') + ->sortable(), + TextColumn::make('created_at') + ->sortable(), + TextColumn::make('updated_at') + ->sortable(), + ]) + ->filters([ + // + ]) + ->defaultSort('created_at', 'desc') + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListProjects::route('/'), + 'create' => Pages\CreateProject::route('/create'), + 'edit' => Pages\EditProject::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/ProjectResource/Pages/CreateProject.php b/app/Filament/Resources/ProjectResource/Pages/CreateProject.php new file mode 100644 index 00000000..239e5f53 --- /dev/null +++ b/app/Filament/Resources/ProjectResource/Pages/CreateProject.php @@ -0,0 +1,13 @@ +schema([ + TextInput::make('name') + ->label('Name') + ->required(), + Forms\Components\Select::make('organization_id') + ->relationship(name: 'organization', titleAttribute: 'name') + ->label('Organization') + ->searchable(['name']) + ->required(), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('name') + ->label('Name') + ->searchable() + ->sortable(), + Tables\Columns\TextColumn::make('organization.name') + ->sortable() + ->label('Organization'), + Tables\Columns\TextColumn::make('created_at') + ->label('Created at') + ->sortable(), + Tables\Columns\TextColumn::make('updated_at') + ->label('Updated at') + ->sortable(), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListTags::route('/'), + 'create' => Pages\CreateTag::route('/create'), + 'edit' => Pages\EditTag::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/TagResource/Pages/CreateTag.php b/app/Filament/Resources/TagResource/Pages/CreateTag.php new file mode 100644 index 00000000..e157795f --- /dev/null +++ b/app/Filament/Resources/TagResource/Pages/CreateTag.php @@ -0,0 +1,13 @@ +schema([ + // + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('name') + ->searchable() + ->sortable(), + Tables\Columns\TextColumn::make('project.name') + ->sortable(), + Tables\Columns\TextColumn::make('created_at') + ->sortable(), + Tables\Columns\TextColumn::make('updated_at') + ->sortable(), + ]) + ->filters([ + // + ]) + ->defaultSort('created_at', 'desc') + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListTasks::route('/'), + 'create' => Pages\CreateTask::route('/create'), + 'edit' => Pages\EditTask::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/TaskResource/Pages/CreateTask.php b/app/Filament/Resources/TaskResource/Pages/CreateTask.php new file mode 100644 index 00000000..24131b5e --- /dev/null +++ b/app/Filament/Resources/TaskResource/Pages/CreateTask.php @@ -0,0 +1,13 @@ +schema([ + TextInput::make('id') + ->label('ID') + ->readOnly() + ->disabled(), + TextInput::make('description') + ->label('Description') + ->required() + ->maxLength(255), + Toggle::make('billable') + ->label('Is Billable?') + ->required(), + DateTimePicker::make('start') + ->label('Start') + ->required(), + DateTimePicker::make('end') + ->label('End') + ->nullable() + ->rules([ + 'after:start', + ]), + Select::make('user_id') + ->relationship(name: 'user', titleAttribute: 'email') + ->searchable(['name', 'email']) + ->required(), + Select::make('project_id') + ->relationship(name: 'project', titleAttribute: 'name') + ->searchable(['name']) + ->nullable(), + // TODO + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make('description') + ->label('Description'), + TextColumn::make('user.email') + ->label('User'), + TextColumn::make('project.name') + ->label('Project'), + TextColumn::make('task.name') + ->label('Task'), + TextColumn::make('time') + ->getStateUsing(function (TimeEntry $record): string { + return ($record->getDuration()?->cascade()?->forHumans() ?? '-').' '. + ' ('.$record->start->toDateTimeString('minute').' - '. + ($record->end?->toDateTimeString('minute') ?? '...').')'; + }) + ->label('Time'), + ]) + ->filters([ + // + ]) + ->defaultSort('created_at', 'desc') + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListTimeEntries::route('/'), + 'create' => Pages\CreateTimeEntry::route('/create'), + 'edit' => Pages\EditTimeEntry::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/TimeEntryResource/Pages/CreateTimeEntry.php b/app/Filament/Resources/TimeEntryResource/Pages/CreateTimeEntry.php new file mode 100644 index 00000000..35a85daf --- /dev/null +++ b/app/Filament/Resources/TimeEntryResource/Pages/CreateTimeEntry.php @@ -0,0 +1,13 @@ +schema([ + Forms\Components\TextInput::make('id') + ->label('ID') + ->disabled() + ->visibleOn(['update', 'show']) + ->readOnly() + ->maxLength(255), + Forms\Components\TextInput::make('name') + ->label('Name') + ->required() + ->maxLength(255), + Forms\Components\TextInput::make('email') + ->label('Email') + ->required() + ->maxLength(255), + Forms\Components\TextInput::make('password') + ->label('Password') + ->required() + ->password() + ->maxLength(255), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('name') + ->searchable() + ->sortable(), + Tables\Columns\TextColumn::make('email') + ->icon('heroicon-m-envelope') + ->searchable() + ->sortable(), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListUsers::route('/'), + 'create' => Pages\CreateUser::route('/create'), + 'edit' => Pages\EditUser::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/UserResource/Pages/CreateUser.php b/app/Filament/Resources/UserResource/Pages/CreateUser.php new file mode 100644 index 00000000..72b81bb0 --- /dev/null +++ b/app/Filament/Resources/UserResource/Pages/CreateUser.php @@ -0,0 +1,13 @@ +hasTeamPermission($organization, $permission)) { + throw new AuthorizationException(); + } + } +} diff --git a/app/Http/Controllers/Api/V1/ProjectController.php b/app/Http/Controllers/Api/V1/ProjectController.php new file mode 100644 index 00000000..91895dae --- /dev/null +++ b/app/Http/Controllers/Api/V1/ProjectController.php @@ -0,0 +1,77 @@ +checkPermission($organization, 'projects:view'); + $projects = Project::query() + ->whereBelongsTo($organization, 'organization') + ->get(); + + return new ProjectCollection($projects); + } + + /** + * @throws AuthorizationException + */ + public function show(Organization $organization, Project $project): JsonResource + { + $this->checkPermission($organization, 'projects:view'); + $project->load('organization'); + + return new ProjectResource($project); + } + + /** + * @throws AuthorizationException + */ + public function store(Organization $organization, ProjectStoreRequest $request): JsonResource + { + $this->checkPermission($organization, 'projects:create'); + $project = new Project(); + $project->name = $request->input('name'); + $project->color = $request->input('color'); + $project->organization()->associate($organization); + $project->save(); + + return new ProjectResource($project); + } + + /** + * @throws AuthorizationException + */ + public function update(Organization $organization, Project $project): JsonResource + { + $this->checkPermission($organization, 'projects:update'); + $project->update(request()->all()); + + return new ProjectResource($project); + } + + /** + * @throws AuthorizationException + */ + public function destroy(Organization $organization, Project $project): JsonResource + { + $this->checkPermission($organization, 'projects:delete'); + $project->delete(); + + return new ProjectResource($project); + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index fb6479d3..8526e5b4 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -4,6 +4,7 @@ namespace App\Http; +use App\Http\Middleware\ForceJsonResponse; use Illuminate\Foundation\Http\Kernel as HttpKernel; class Kernel extends HttpKernel @@ -40,12 +41,13 @@ class Kernel extends HttpKernel \Illuminate\Routing\Middleware\SubstituteBindings::class, \App\Http\Middleware\HandleInertiaRequests::class, \Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class, + \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class, ], 'api' => [ - // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, \Illuminate\Routing\Middleware\ThrottleRequests::class.':api', \Illuminate\Routing\Middleware\SubstituteBindings::class, + ForceJsonResponse::class, ], ]; diff --git a/app/Http/Middleware/ForceJsonResponse.php b/app/Http/Middleware/ForceJsonResponse.php new file mode 100644 index 00000000..bae7216c --- /dev/null +++ b/app/Http/Middleware/ForceJsonResponse.php @@ -0,0 +1,24 @@ +headers->set('Accept', 'application/json'); + + return $next($request); + } +} diff --git a/app/Http/Requests/V1/Project/ProjectStoreRequest.php b/app/Http/Requests/V1/Project/ProjectStoreRequest.php new file mode 100644 index 00000000..20011363 --- /dev/null +++ b/app/Http/Requests/V1/Project/ProjectStoreRequest.php @@ -0,0 +1,29 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => [ + 'required', + 'string', + 'max:255', + ], + 'color' => [ + 'required', + 'string', + 'max:255', + ], + ]; + } +} diff --git a/app/Http/Resources/V1/Project/ProjectCollection.php b/app/Http/Resources/V1/Project/ProjectCollection.php new file mode 100644 index 00000000..1d33b07c --- /dev/null +++ b/app/Http/Resources/V1/Project/ProjectCollection.php @@ -0,0 +1,17 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->resource->id, + 'name' => $this->resource->name, + ]; + } +} diff --git a/app/Models/Project.php b/app/Models/Project.php index 4b6cee3e..6e772c41 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -15,6 +15,7 @@ /** * @property string $id * @property string $name + * @property string $color * @property string $organization_id * @property string $client_id * @property-read Organization $organization @@ -35,6 +36,7 @@ class Project extends Model */ protected $casts = [ 'name' => 'string', + 'color' => 'string', ]; /** diff --git a/app/Models/TimeEntry.php b/app/Models/TimeEntry.php index ae9344b4..b47e6879 100644 --- a/app/Models/TimeEntry.php +++ b/app/Models/TimeEntry.php @@ -4,6 +4,7 @@ namespace App\Models; +use Carbon\CarbonInterval; use Database\Factories\TimeEntryFactory; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -43,6 +44,11 @@ class TimeEntry extends Model 'tags' => 'array', ]; + public function getDuration(): ?CarbonInterval + { + return $this->end === null ? null : $this->start->diffAsCarbonInterval($this->end); + } + /** * @return BelongsTo */ diff --git a/app/Policies/OrganizationPolicy.php b/app/Policies/OrganizationPolicy.php index 32fbb092..e6ece6b9 100644 --- a/app/Policies/OrganizationPolicy.php +++ b/app/Policies/OrganizationPolicy.php @@ -6,6 +6,7 @@ use App\Models\Organization; use App\Models\User; +use Filament\Facades\Filament; use Illuminate\Auth\Access\HandlesAuthorization; class OrganizationPolicy @@ -17,7 +18,11 @@ class OrganizationPolicy */ public function viewAny(User $user): bool { - return true; + if (Filament::isServing()) { + return true; + } + + return false; } /** @@ -25,6 +30,10 @@ public function viewAny(User $user): bool */ public function view(User $user, Organization $organization): bool { + if (Filament::isServing()) { + return true; + } + return $user->belongsToTeam($organization); } @@ -33,6 +42,10 @@ public function view(User $user, Organization $organization): bool */ public function create(User $user): bool { + if (Filament::isServing()) { + return true; + } + return true; } @@ -41,6 +54,10 @@ public function create(User $user): bool */ public function update(User $user, Organization $organization): bool { + if (Filament::isServing()) { + return true; + } + return $user->ownsTeam($organization); } @@ -49,6 +66,10 @@ public function update(User $user, Organization $organization): bool */ public function addTeamMember(User $user, Organization $organization): bool { + if (Filament::isServing()) { + return true; + } + return $user->ownsTeam($organization); } @@ -57,6 +78,10 @@ public function addTeamMember(User $user, Organization $organization): bool */ public function updateTeamMember(User $user, Organization $organization): bool { + if (Filament::isServing()) { + return true; + } + return $user->ownsTeam($organization); } @@ -65,6 +90,10 @@ public function updateTeamMember(User $user, Organization $organization): bool */ public function removeTeamMember(User $user, Organization $organization): bool { + if (Filament::isServing()) { + return true; + } + return $user->ownsTeam($organization); } @@ -73,6 +102,10 @@ public function removeTeamMember(User $user, Organization $organization): bool */ public function delete(User $user, Organization $organization): bool { + if (Filament::isServing()) { + return true; + } + return $user->ownsTeam($organization); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index f1cc0727..e9d101b1 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,10 +4,16 @@ namespace App\Providers; +use App\Models\Client; use App\Models\Membership; use App\Models\Organization; use App\Models\OrganizationInvitation; +use App\Models\Project; +use App\Models\Tag; +use App\Models\Task; +use App\Models\TimeEntry; use App\Models\User; +use Filament\Forms\Components\Section; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\ServiceProvider; @@ -27,13 +33,27 @@ public function register(): void */ public function boot(): void { + if ($this->app->environment('local')) { + $this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class); + $this->app->register(TelescopeServiceProvider::class); + } + Model::preventLazyLoading(! $this->app->isProduction()); Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction()); Relation::enforceMorphMap([ 'membership' => Membership::class, - 'team' => Organization::class, - 'team_invitation' => OrganizationInvitation::class, + 'organization' => Organization::class, + 'organization-invitation' => OrganizationInvitation::class, 'user' => User::class, + 'time-entry' => TimeEntry::class, + 'project' => Project::class, + 'task' => Task::class, + 'client' => Client::class, + 'tag' => Tag::class, ]); + Model::unguard(); + Section::configureUsing(function (Section $section): void { + $section->columns(1); + }, null, true); } } diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 493bcf36..2fd6d226 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -7,6 +7,7 @@ use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\DisableBladeIconComponents; use Filament\Http\Middleware\DispatchServingFilamentEvent; +use Filament\Navigation\NavigationGroup; use Filament\Pages; use Filament\Panel; use Filament\PanelProvider; @@ -18,7 +19,9 @@ use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Session\Middleware\AuthenticateSession; use Illuminate\Session\Middleware\StartSession; +use Illuminate\Support\Facades\App; use Illuminate\View\Middleware\ShareErrorsFromSession; +use pxlrbt\FilamentEnvironmentIndicator\EnvironmentIndicatorPlugin; class AdminPanelProvider extends PanelProvider { @@ -40,6 +43,21 @@ public function panel(Panel $panel): Panel ->widgets([ Widgets\AccountWidget::class, ]) + ->plugins([ + EnvironmentIndicatorPlugin::make() + ->color(fn () => match (App::environment()) { + 'production' => null, + 'staging' => Color::Orange, + default => Color::Blue, + }), + ]) + ->navigationGroups([ + NavigationGroup::make() + ->label('Timetracking'), + NavigationGroup::make() + ->label('Users') + ->collapsed(), + ]) ->middleware([ EncryptCookies::class, AddQueuedCookiesToResponse::class, diff --git a/app/Providers/JetstreamServiceProvider.php b/app/Providers/JetstreamServiceProvider.php index 0f4742dc..b306a01b 100644 --- a/app/Providers/JetstreamServiceProvider.php +++ b/app/Providers/JetstreamServiceProvider.php @@ -49,19 +49,27 @@ public function boot(): void */ protected function configurePermissions(): void { - Jetstream::defaultApiTokenPermissions(['read']); + Jetstream::defaultApiTokenPermissions([]); Jetstream::role('admin', 'Administrator', [ - 'create', - 'read', - 'update', - 'delete', + 'projects:view', + 'projects:create', + 'projects:update', + 'projects:delete', ])->description('Administrator users can perform any action.'); - Jetstream::role('editor', 'Editor', [ - 'read', - 'create', - 'update', + Jetstream::role('manager', 'Manager', [ + 'projects:view', + 'projects:create', + 'projects:update', + 'projects:delete', + ])->description('Editor users have the ability to read, create, and update.'); + + Jetstream::role('employee', 'Employee', [ + 'projects:view', + 'projects:create', + 'projects:update', + 'projects:delete', ])->description('Editor users have the ability to read, create, and update.'); } } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 009d1b41..d7e3c8fa 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -33,6 +33,7 @@ public function boot(): void $this->routes(function () { Route::middleware('api') ->prefix('api') + ->name('api.') ->group(base_path('routes/api.php')); Route::middleware('web') diff --git a/app/Providers/TelescopeServiceProvider.php b/app/Providers/TelescopeServiceProvider.php new file mode 100644 index 00000000..ded37e5c --- /dev/null +++ b/app/Providers/TelescopeServiceProvider.php @@ -0,0 +1,67 @@ +hideSensitiveRequestDetails(); + + Telescope::filter(function (IncomingEntry $entry) { + if ($this->app->environment('local')) { + return true; + } + + return $entry->isReportableException() || + $entry->isFailedRequest() || + $entry->isFailedJob() || + $entry->isScheduledTask() || + $entry->hasMonitoredTag(); + }); + } + + /** + * Prevent sensitive request details from being logged by Telescope. + */ + protected function hideSensitiveRequestDetails(): void + { + if ($this->app->environment('local')) { + return; + } + + Telescope::hideRequestParameters(['_token']); + + Telescope::hideRequestHeaders([ + 'cookie', + 'x-csrf-token', + 'x-xsrf-token', + ]); + } + + /** + * Register the Telescope gate. + * + * This gate determines who can access Telescope in non-local environments. + */ + protected function gate(): void + { + Gate::define('viewTelescope', function (User $user): bool { + // Note: Telescope is only available in local environments, so this should not be relevant. + return false; + }); + } +} diff --git a/composer.json b/composer.json index 7c5b2894..d051ee81 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "laravel/jetstream": "^4.2", "laravel/passport": "*", "laravel/tinker": "^2.8", + "pxlrbt/filament-environment-indicator": "^2.0", "tightenco/ziggy": "^1.0", "tpetry/laravel-postgresql-enhanced": "^0.33.0" }, @@ -24,6 +25,7 @@ "larastan/larastan": "^2.0", "laravel/pint": "^1.0", "laravel/sail": "^1.18", + "laravel/telescope": "^4.17", "mockery/mockery": "^1.4.4", "nunomaduro/collision": "^7.0", "phpunit/phpunit": "^10.1", @@ -77,7 +79,9 @@ }, "extra": { "laravel": { - "dont-discover": [] + "dont-discover": [ + "laravel/telescope" + ] } }, "config": { diff --git a/composer.lock b/composer.lock index 56969805..e4759af3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ad3e9deab517156569b424191cdb7871", + "content-hash": "53e9ad1ac7367efb5c1d7c971499447b", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -792,16 +792,16 @@ }, { "name": "doctrine/dbal", - "version": "3.7.3", + "version": "3.8.0", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "ce594cbc39a4866c544f1a970d285ff0548221ad" + "reference": "d244f2e6e6bf32bff5174e6729b57214923ecec9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/ce594cbc39a4866c544f1a970d285ff0548221ad", - "reference": "ce594cbc39a4866c544f1a970d285ff0548221ad", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/d244f2e6e6bf32bff5174e6729b57214923ecec9", + "reference": "d244f2e6e6bf32bff5174e6729b57214923ecec9", "shasum": "" }, "require": { @@ -885,7 +885,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.7.3" + "source": "https://github.com/doctrine/dbal/tree/3.8.0" }, "funding": [ { @@ -901,7 +901,7 @@ "type": "tidelift" } ], - "time": "2024-01-21T07:53:09+00:00" + "time": "2024-01-25T21:44:02+00:00" }, { "name": "doctrine/deprecations", @@ -1339,16 +1339,16 @@ }, { "name": "filament/actions", - "version": "v3.2.9", + "version": "v3.2.16", "source": { "type": "git", "url": "https://github.com/filamentphp/actions.git", - "reference": "465ef83a1c43b3b7fe122dde50eda2ee0f9138ea" + "reference": "2ad35bd1aad0c72f62e9c5f877989056a39cf012" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/actions/zipball/465ef83a1c43b3b7fe122dde50eda2ee0f9138ea", - "reference": "465ef83a1c43b3b7fe122dde50eda2ee0f9138ea", + "url": "https://api.github.com/repos/filamentphp/actions/zipball/2ad35bd1aad0c72f62e9c5f877989056a39cf012", + "reference": "2ad35bd1aad0c72f62e9c5f877989056a39cf012", "shasum": "" }, "require": { @@ -1388,20 +1388,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-01-21T14:44:52+00:00" + "time": "2024-01-27T23:30:19+00:00" }, { "name": "filament/filament", - "version": "v3.2.9", + "version": "v3.2.16", "source": { "type": "git", "url": "https://github.com/filamentphp/panels.git", - "reference": "a830f2d38073d3a4cdbe3798c957b69be50d39c3" + "reference": "135ee98a43455a8c436367d8c51660d9a8b75ae4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/panels/zipball/a830f2d38073d3a4cdbe3798c957b69be50d39c3", - "reference": "a830f2d38073d3a4cdbe3798c957b69be50d39c3", + "url": "https://api.github.com/repos/filamentphp/panels/zipball/135ee98a43455a8c436367d8c51660d9a8b75ae4", + "reference": "135ee98a43455a8c436367d8c51660d9a8b75ae4", "shasum": "" }, "require": { @@ -1453,20 +1453,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-01-21T14:44:57+00:00" + "time": "2024-01-27T23:30:21+00:00" }, { "name": "filament/forms", - "version": "v3.2.9", + "version": "v3.2.16", "source": { "type": "git", "url": "https://github.com/filamentphp/forms.git", - "reference": "fc37c620f66a2e13e160b516cc4d0e5ad8ae9425" + "reference": "693ac4f2413e132501576cc0ca8f8aad636c362e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/forms/zipball/fc37c620f66a2e13e160b516cc4d0e5ad8ae9425", - "reference": "fc37c620f66a2e13e160b516cc4d0e5ad8ae9425", + "url": "https://api.github.com/repos/filamentphp/forms/zipball/693ac4f2413e132501576cc0ca8f8aad636c362e", + "reference": "693ac4f2413e132501576cc0ca8f8aad636c362e", "shasum": "" }, "require": { @@ -1509,20 +1509,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-01-21T14:44:57+00:00" + "time": "2024-01-27T23:30:18+00:00" }, { "name": "filament/infolists", - "version": "v3.2.9", + "version": "v3.2.16", "source": { "type": "git", "url": "https://github.com/filamentphp/infolists.git", - "reference": "b071063c45f0cd314c863947d1d841da09d40750" + "reference": "4ab39e8985cad7f5907b0c162d38023eb9dd402a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/infolists/zipball/b071063c45f0cd314c863947d1d841da09d40750", - "reference": "b071063c45f0cd314c863947d1d841da09d40750", + "url": "https://api.github.com/repos/filamentphp/infolists/zipball/4ab39e8985cad7f5907b0c162d38023eb9dd402a", + "reference": "4ab39e8985cad7f5907b0c162d38023eb9dd402a", "shasum": "" }, "require": { @@ -1560,11 +1560,11 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-01-21T14:44:53+00:00" + "time": "2024-01-26T12:42:37+00:00" }, { "name": "filament/notifications", - "version": "v3.2.9", + "version": "v3.2.16", "source": { "type": "git", "url": "https://github.com/filamentphp/notifications.git", @@ -1616,16 +1616,16 @@ }, { "name": "filament/support", - "version": "v3.2.9", + "version": "v3.2.16", "source": { "type": "git", "url": "https://github.com/filamentphp/support.git", - "reference": "3b4d5d197c04e5a0b2a250d97c4761a07da9c85e" + "reference": "8df5c195047d2849c49c1d20880951f716f111e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/support/zipball/3b4d5d197c04e5a0b2a250d97c4761a07da9c85e", - "reference": "3b4d5d197c04e5a0b2a250d97c4761a07da9c85e", + "url": "https://api.github.com/repos/filamentphp/support/zipball/8df5c195047d2849c49c1d20880951f716f111e0", + "reference": "8df5c195047d2849c49c1d20880951f716f111e0", "shasum": "" }, "require": { @@ -1669,20 +1669,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-01-21T14:44:58+00:00" + "time": "2024-01-27T23:30:41+00:00" }, { "name": "filament/tables", - "version": "v3.2.9", + "version": "v3.2.16", "source": { "type": "git", "url": "https://github.com/filamentphp/tables.git", - "reference": "87c55f6a1107d6d70b61a83f2cd7495343fdb19c" + "reference": "0b63e4df21b3e6957471ab77ec745cda75e51e85" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/tables/zipball/87c55f6a1107d6d70b61a83f2cd7495343fdb19c", - "reference": "87c55f6a1107d6d70b61a83f2cd7495343fdb19c", + "url": "https://api.github.com/repos/filamentphp/tables/zipball/0b63e4df21b3e6957471ab77ec745cda75e51e85", + "reference": "0b63e4df21b3e6957471ab77ec745cda75e51e85", "shasum": "" }, "require": { @@ -1722,11 +1722,11 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2024-01-21T14:45:11+00:00" + "time": "2024-01-27T23:30:50+00:00" }, { "name": "filament/widgets", - "version": "v3.2.9", + "version": "v3.2.16", "source": { "type": "git", "url": "https://github.com/filamentphp/widgets.git", @@ -2639,16 +2639,16 @@ }, { "name": "laravel/framework", - "version": "v10.41.0", + "version": "v10.42.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "da31969bd35e6ee0bbcd9e876f88952dc754b012" + "reference": "fef1aff874a6749c44f8e142e5764eab8cb96890" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/da31969bd35e6ee0bbcd9e876f88952dc754b012", - "reference": "da31969bd35e6ee0bbcd9e876f88952dc754b012", + "url": "https://api.github.com/repos/laravel/framework/zipball/fef1aff874a6749c44f8e142e5764eab8cb96890", + "reference": "fef1aff874a6749c44f8e142e5764eab8cb96890", "shasum": "" }, "require": { @@ -2840,7 +2840,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-01-16T15:23:58+00:00" + "time": "2024-01-23T15:07:56+00:00" }, { "name": "laravel/jetstream", @@ -2913,16 +2913,16 @@ }, { "name": "laravel/passport", - "version": "v11.10.1", + "version": "v11.10.2", "source": { "type": "git", "url": "https://github.com/laravel/passport.git", - "reference": "e1a651481cabff0ba174aaefbdc04a59e6a096ec" + "reference": "27a4f34aaf8b360eb64f53eb9c555ee50d565560" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/passport/zipball/e1a651481cabff0ba174aaefbdc04a59e6a096ec", - "reference": "e1a651481cabff0ba174aaefbdc04a59e6a096ec", + "url": "https://api.github.com/repos/laravel/passport/zipball/27a4f34aaf8b360eb64f53eb9c555ee50d565560", + "reference": "27a4f34aaf8b360eb64f53eb9c555ee50d565560", "shasum": "" }, "require": { @@ -2987,7 +2987,7 @@ "issues": "https://github.com/laravel/passport/issues", "source": "https://github.com/laravel/passport" }, - "time": "2024-01-10T14:44:24+00:00" + "time": "2024-01-17T14:57:00+00:00" }, { "name": "laravel/prompts", @@ -3641,16 +3641,16 @@ }, { "name": "league/flysystem", - "version": "3.23.0", + "version": "3.23.1", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "d4ad81e2b67396e33dc9d7e54ec74ccf73151dcc" + "reference": "199e1aebbe3e62bd39f4d4fc8c61ce0b3786197e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/d4ad81e2b67396e33dc9d7e54ec74ccf73151dcc", - "reference": "d4ad81e2b67396e33dc9d7e54ec74ccf73151dcc", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/199e1aebbe3e62bd39f4d4fc8c61ce0b3786197e", + "reference": "199e1aebbe3e62bd39f4d4fc8c61ce0b3786197e", "shasum": "" }, "require": { @@ -3715,7 +3715,7 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.23.0" + "source": "https://github.com/thephpleague/flysystem/tree/3.23.1" }, "funding": [ { @@ -3727,20 +3727,20 @@ "type": "github" } ], - "time": "2023-12-04T10:16:17+00:00" + "time": "2024-01-26T18:42:03+00:00" }, { "name": "league/flysystem-local", - "version": "3.23.0", + "version": "3.23.1", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "5cf046ba5f059460e86a997c504dd781a39a109b" + "reference": "b884d2bf9b53bb4804a56d2df4902bb51e253f00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/5cf046ba5f059460e86a997c504dd781a39a109b", - "reference": "5cf046ba5f059460e86a997c504dd781a39a109b", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/b884d2bf9b53bb4804a56d2df4902bb51e253f00", + "reference": "b884d2bf9b53bb4804a56d2df4902bb51e253f00", "shasum": "" }, "require": { @@ -3775,7 +3775,7 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem-local/issues", - "source": "https://github.com/thephpleague/flysystem-local/tree/3.23.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.23.1" }, "funding": [ { @@ -3787,7 +3787,7 @@ "type": "github" } ], - "time": "2023-12-04T10:14:46+00:00" + "time": "2024-01-26T18:25:23+00:00" }, { "name": "league/mime-type-detection", @@ -4109,20 +4109,21 @@ }, { "name": "livewire/livewire", - "version": "v3.3.5", + "version": "v3.4.2", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "1ef880fbcdc7b6e5e405cc9135a62cd5fdbcd06a" + "reference": "ab0baed58b774dde8e0ddbab1bbfd5b3d6334a82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/1ef880fbcdc7b6e5e405cc9135a62cd5fdbcd06a", - "reference": "1ef880fbcdc7b6e5e405cc9135a62cd5fdbcd06a", + "url": "https://api.github.com/repos/livewire/livewire/zipball/ab0baed58b774dde8e0ddbab1bbfd5b3d6334a82", + "reference": "ab0baed58b774dde8e0ddbab1bbfd5b3d6334a82", "shasum": "" }, "require": { "illuminate/database": "^10.0|^11.0", + "illuminate/routing": "^10.0|^11.0", "illuminate/support": "^10.0|^11.0", "illuminate/validation": "^10.0|^11.0", "league/mime-type-detection": "^1.9", @@ -4134,8 +4135,8 @@ "laravel/framework": "^10.0|^11.0", "laravel/prompts": "^0.1.6", "mockery/mockery": "^1.3.1", - "orchestra/testbench": "^8.0|^9.0", - "orchestra/testbench-dusk": "^8.0|^9.0", + "orchestra/testbench": "8.20.0|^9.0", + "orchestra/testbench-dusk": "8.20.0|^9.0", "phpunit/phpunit": "^10.4", "psy/psysh": "^0.11.22|^0.12" }, @@ -4171,7 +4172,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.3.5" + "source": "https://github.com/livewire/livewire/tree/v3.4.2" }, "funding": [ { @@ -4179,7 +4180,7 @@ "type": "github" } ], - "time": "2024-01-02T14:29:17+00:00" + "time": "2024-01-26T14:25:51+00:00" }, { "name": "masterminds/html5", @@ -4415,16 +4416,16 @@ }, { "name": "nesbot/carbon", - "version": "2.72.1", + "version": "2.72.2", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "2b3b3db0a2d0556a177392ff1a3bf5608fa09f78" + "reference": "3e7edc41b58d65509baeb0d4a14c8fa41d627130" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/2b3b3db0a2d0556a177392ff1a3bf5608fa09f78", - "reference": "2b3b3db0a2d0556a177392ff1a3bf5608fa09f78", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/3e7edc41b58d65509baeb0d4a14c8fa41d627130", + "reference": "3e7edc41b58d65509baeb0d4a14c8fa41d627130", "shasum": "" }, "require": { @@ -4518,7 +4519,7 @@ "type": "tidelift" } ], - "time": "2023-12-08T23:47:49+00:00" + "time": "2024-01-19T00:21:53+00:00" }, { "name": "nette/schema", @@ -5877,6 +5878,68 @@ }, "time": "2023-12-20T15:28:09+00:00" }, + { + "name": "pxlrbt/filament-environment-indicator", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/pxlrbt/filament-environment-indicator.git", + "reference": "8942ad37142298a6eaf7fed747dd9c90402b0ba5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pxlrbt/filament-environment-indicator/zipball/8942ad37142298a6eaf7fed747dd9c90402b0ba5", + "reference": "8942ad37142298a6eaf7fed747dd9c90402b0ba5", + "shasum": "" + }, + "require": { + "filament/filament": "^3.0-stable", + "php": "^8.0" + }, + "require-dev": { + "laravel/pint": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "\\pxlrbt\\FilamentEnvironmentIndicator\\FilamentEnvironmentIndicatorServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "pxlrbt\\FilamentEnvironmentIndicator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dennis Koch", + "email": "info@pixelarbeit.de" + } + ], + "description": "Indicator for the current environment inside Filament", + "keywords": [ + "environment indicator", + "filament", + "laravel-filament" + ], + "support": { + "issues": "https://github.com/pxlrbt/filament-environment-indicator/issues", + "source": "https://github.com/pxlrbt/filament-environment-indicator/tree/v2.0.1" + }, + "funding": [ + { + "url": "https://github.com/pxlrbt", + "type": "github" + } + ], + "time": "2023-09-22T04:12:47+00:00" + }, { "name": "ralouphie/getallheaders", "version": "3.0.3", @@ -9739,16 +9802,16 @@ }, { "name": "laravel/pint", - "version": "v1.13.9", + "version": "v1.13.10", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "e3e269cc5d874c8efd2dc7962b1c7ff2585fe525" + "reference": "e2b5060885694ca30ac008c05dc9d47f10ed1abf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/e3e269cc5d874c8efd2dc7962b1c7ff2585fe525", - "reference": "e3e269cc5d874c8efd2dc7962b1c7ff2585fe525", + "url": "https://api.github.com/repos/laravel/pint/zipball/e2b5060885694ca30ac008c05dc9d47f10ed1abf", + "reference": "e2b5060885694ca30ac008c05dc9d47f10ed1abf", "shasum": "" }, "require": { @@ -9759,8 +9822,8 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.47.0", - "illuminate/view": "^10.40.0", + "friendsofphp/php-cs-fixer": "^3.47.1", + "illuminate/view": "^10.41.0", "larastan/larastan": "^2.8.1", "laravel-zero/framework": "^10.3.0", "mockery/mockery": "^1.6.7", @@ -9801,20 +9864,20 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-01-16T17:39:29+00:00" + "time": "2024-01-22T09:04:15+00:00" }, { "name": "laravel/sail", - "version": "v1.27.1", + "version": "v1.27.2", "source": { "type": "git", "url": "https://github.com/laravel/sail.git", - "reference": "9dc648978e4276f2bfd37a076a52e3bd9394777f" + "reference": "2276a8d9d6cfdcaad98bf67a34331d100149d5b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sail/zipball/9dc648978e4276f2bfd37a076a52e3bd9394777f", - "reference": "9dc648978e4276f2bfd37a076a52e3bd9394777f", + "url": "https://api.github.com/repos/laravel/sail/zipball/2276a8d9d6cfdcaad98bf67a34331d100149d5b6", + "reference": "2276a8d9d6cfdcaad98bf67a34331d100149d5b6", "shasum": "" }, "require": { @@ -9866,7 +9929,78 @@ "issues": "https://github.com/laravel/sail/issues", "source": "https://github.com/laravel/sail" }, - "time": "2024-01-13T18:46:48+00:00" + "time": "2024-01-21T17:13:42+00:00" + }, + { + "name": "laravel/telescope", + "version": "v4.17.4", + "source": { + "type": "git", + "url": "https://github.com/laravel/telescope.git", + "reference": "3cbe70e900a9d070491149f2615d5a4a5b51d4c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/telescope/zipball/3cbe70e900a9d070491149f2615d5a4a5b51d4c6", + "reference": "3cbe70e900a9d070491149f2615d5a4a5b51d4c6", + "shasum": "" + }, + "require": { + "ext-json": "*", + "laravel/framework": "^8.37|^9.0|^10.0", + "php": "^8.0", + "symfony/var-dumper": "^5.0|^6.0" + }, + "require-dev": { + "ext-gd": "*", + "guzzlehttp/guzzle": "^6.0|^7.0", + "laravel/octane": "^1.4", + "orchestra/testbench": "^6.0|^7.0|^8.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Telescope\\TelescopeServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Telescope\\": "src/", + "Laravel\\Telescope\\Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Mohamed Said", + "email": "mohamed@laravel.com" + } + ], + "description": "An elegant debug assistant for the Laravel framework.", + "keywords": [ + "debugging", + "laravel", + "monitoring" + ], + "support": { + "issues": "https://github.com/laravel/telescope/issues", + "source": "https://github.com/laravel/telescope/tree/v4.17.4" + }, + "time": "2024-01-22T16:15:52+00:00" }, { "name": "mockery/mockery", @@ -10307,16 +10441,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.56", + "version": "1.10.57", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "27816a01aea996191ee14d010f325434c0ee76fa" + "reference": "1627b1d03446904aaa77593f370c5201d2ecc34e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/27816a01aea996191ee14d010f325434c0ee76fa", - "reference": "27816a01aea996191ee14d010f325434c0ee76fa", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1627b1d03446904aaa77593f370c5201d2ecc34e", + "reference": "1627b1d03446904aaa77593f370c5201d2ecc34e", "shasum": "" }, "require": { @@ -10365,7 +10499,7 @@ "type": "tidelift" } ], - "time": "2024-01-15T10:43:00+00:00" + "time": "2024-01-24T11:51:34+00:00" }, { "name": "phpunit/php-code-coverage", @@ -10690,16 +10824,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.8", + "version": "10.5.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "08f4fa74d5fbfff1ef22abffee47aaedcaea227e" + "reference": "0bd663704f0165c9e76fe4f06ffa6a1ca727fdbe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/08f4fa74d5fbfff1ef22abffee47aaedcaea227e", - "reference": "08f4fa74d5fbfff1ef22abffee47aaedcaea227e", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0bd663704f0165c9e76fe4f06ffa6a1ca727fdbe", + "reference": "0bd663704f0165c9e76fe4f06ffa6a1ca727fdbe", "shasum": "" }, "require": { @@ -10771,7 +10905,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.8" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.9" }, "funding": [ { @@ -10787,7 +10921,7 @@ "type": "tidelift" } ], - "time": "2024-01-19T07:07:27+00:00" + "time": "2024-01-22T14:35:40+00:00" }, { "name": "sebastian/cli-parser", diff --git a/config/app.php b/config/app.php index 99b4fcde..3e4cd58d 100644 --- a/config/app.php +++ b/config/app.php @@ -171,6 +171,7 @@ App\Providers\EventServiceProvider::class, App\Providers\Filament\AdminPanelProvider::class, App\Providers\RouteServiceProvider::class, + App\Providers\TelescopeServiceProvider::class, App\Providers\FortifyServiceProvider::class, App\Providers\JetstreamServiceProvider::class, ])->toArray(), diff --git a/config/telescope.php b/config/telescope.php new file mode 100644 index 00000000..312fdf26 --- /dev/null +++ b/config/telescope.php @@ -0,0 +1,191 @@ + env('TELESCOPE_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | Telescope Path + |-------------------------------------------------------------------------- + | + | This is the URI path where Telescope will be accessible from. Feel free + | to change this path to anything you like. Note that the URI will not + | affect the paths of its internal API that aren't exposed to users. + | + */ + + 'path' => env('TELESCOPE_PATH', 'telescope'), + + /* + |-------------------------------------------------------------------------- + | Telescope Storage Driver + |-------------------------------------------------------------------------- + | + | This configuration options determines the storage driver that will + | be used to store Telescope's data. In addition, you may set any + | custom options as needed by the particular driver you choose. + | + */ + + 'driver' => env('TELESCOPE_DRIVER', 'database'), + + 'storage' => [ + 'database' => [ + 'connection' => env('DB_CONNECTION', 'mysql'), + 'chunk' => 1000, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Telescope Master Switch + |-------------------------------------------------------------------------- + | + | This option may be used to disable all Telescope watchers regardless + | of their individual configuration, which simply provides a single + | and convenient way to enable or disable Telescope data storage. + | + */ + + 'enabled' => env('TELESCOPE_ENABLED', true), + + /* + |-------------------------------------------------------------------------- + | Telescope Route Middleware + |-------------------------------------------------------------------------- + | + | These middleware will be assigned to every Telescope route, giving you + | the chance to add your own middleware to this list or change any of + | the existing middleware. Or, you can simply stick with this list. + | + */ + + 'middleware' => [ + 'web', + Authorize::class, + ], + + /* + |-------------------------------------------------------------------------- + | Allowed / Ignored Paths & Commands + |-------------------------------------------------------------------------- + | + | The following array lists the URI paths and Artisan commands that will + | not be watched by Telescope. In addition to this list, some Laravel + | commands, like migrations and queue commands, are always ignored. + | + */ + + 'only_paths' => [ + // 'api/*' + ], + + 'ignore_paths' => [ + 'livewire*', + 'nova-api*', + 'pulse*', + ], + + 'ignore_commands' => [ + // + ], + + /* + |-------------------------------------------------------------------------- + | Telescope Watchers + |-------------------------------------------------------------------------- + | + | The following array lists the "watchers" that will be registered with + | Telescope. The watchers gather the application's profile data when + | a request or task is executed. Feel free to customize this list. + | + */ + + 'watchers' => [ + Watchers\BatchWatcher::class => env('TELESCOPE_BATCH_WATCHER', true), + + Watchers\CacheWatcher::class => [ + 'enabled' => env('TELESCOPE_CACHE_WATCHER', true), + 'hidden' => [], + ], + + Watchers\ClientRequestWatcher::class => env('TELESCOPE_CLIENT_REQUEST_WATCHER', true), + + Watchers\CommandWatcher::class => [ + 'enabled' => env('TELESCOPE_COMMAND_WATCHER', true), + 'ignore' => [], + ], + + Watchers\DumpWatcher::class => [ + 'enabled' => env('TELESCOPE_DUMP_WATCHER', true), + 'always' => env('TELESCOPE_DUMP_WATCHER_ALWAYS', false), + ], + + Watchers\EventWatcher::class => [ + 'enabled' => env('TELESCOPE_EVENT_WATCHER', true), + 'ignore' => [], + ], + + Watchers\ExceptionWatcher::class => env('TELESCOPE_EXCEPTION_WATCHER', true), + + Watchers\GateWatcher::class => [ + 'enabled' => env('TELESCOPE_GATE_WATCHER', true), + 'ignore_abilities' => [], + 'ignore_packages' => true, + 'ignore_paths' => [], + ], + + Watchers\JobWatcher::class => env('TELESCOPE_JOB_WATCHER', true), + + Watchers\LogWatcher::class => [ + 'enabled' => env('TELESCOPE_LOG_WATCHER', true), + 'level' => 'error', + ], + + Watchers\MailWatcher::class => env('TELESCOPE_MAIL_WATCHER', true), + + Watchers\ModelWatcher::class => [ + 'enabled' => env('TELESCOPE_MODEL_WATCHER', true), + 'events' => ['eloquent.*'], + 'hydrations' => true, + ], + + Watchers\NotificationWatcher::class => env('TELESCOPE_NOTIFICATION_WATCHER', true), + + Watchers\QueryWatcher::class => [ + 'enabled' => env('TELESCOPE_QUERY_WATCHER', true), + 'ignore_packages' => true, + 'ignore_paths' => [], + 'slow' => 100, + ], + + Watchers\RedisWatcher::class => env('TELESCOPE_REDIS_WATCHER', true), + + Watchers\RequestWatcher::class => [ + 'enabled' => env('TELESCOPE_REQUEST_WATCHER', true), + 'size_limit' => env('TELESCOPE_RESPONSE_SIZE_LIMIT', 64), + 'ignore_http_methods' => [], + 'ignore_status_codes' => [], + ], + + Watchers\ScheduleWatcher::class => env('TELESCOPE_SCHEDULE_WATCHER', true), + Watchers\ViewWatcher::class => env('TELESCOPE_VIEW_WATCHER', true), + ], +]; diff --git a/database/migrations/2020_05_21_100000_create_organizations_table.php b/database/migrations/2020_05_21_100000_create_organizations_table.php index f2a41ebb..7fbcb9c4 100644 --- a/database/migrations/2020_05_21_100000_create_organizations_table.php +++ b/database/migrations/2020_05_21_100000_create_organizations_table.php @@ -27,6 +27,6 @@ public function up(): void */ public function down(): void { - Schema::dropIfExists('teams'); + Schema::dropIfExists('organizations'); } }; diff --git a/database/migrations/2020_05_21_200000_create_organization_user_table.php b/database/migrations/2020_05_21_200000_create_organization_user_table.php index 1097ffaa..71e50b2d 100644 --- a/database/migrations/2020_05_21_200000_create_organization_user_table.php +++ b/database/migrations/2020_05_21_200000_create_organization_user_table.php @@ -29,6 +29,6 @@ public function up(): void */ public function down(): void { - Schema::dropIfExists('team_user'); + Schema::dropIfExists('organization_user'); } }; diff --git a/database/migrations/2020_05_21_300000_create_organization_invitations_table.php b/database/migrations/2020_05_21_300000_create_organization_invitations_table.php index 5c8c49c3..1538296e 100644 --- a/database/migrations/2020_05_21_300000_create_organization_invitations_table.php +++ b/database/migrations/2020_05_21_300000_create_organization_invitations_table.php @@ -31,6 +31,6 @@ public function up(): void */ public function down(): void { - Schema::dropIfExists('team_invitations'); + Schema::dropIfExists('organization_invitations'); } }; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index eabb5750..be79a095 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -22,23 +22,32 @@ class DatabaseSeeder extends Seeder public function run(): void { $this->deleteAll(); - $organization = Organization::factory()->create([ + $organization1 = Organization::factory()->create([ 'name' => 'ACME Corp', ]); $user1 = User::factory()->withPersonalOrganization()->create([ 'name' => 'Test User', 'email' => 'test@example.com', ]); + $employee1 = User::factory()->withPersonalOrganization()->create([ + 'name' => 'Test User', + 'email' => 'employee@example.com', + ]); $userAcmeAdmin = User::factory()->create([ 'name' => 'ACME Admin', 'email' => 'admin@acme.test', ]); - $user1->organizations()->attach($organization, [ - 'role' => 'editor', + $user1->organizations()->attach($organization1, [ + 'role' => 'manager', ]); - $userAcmeAdmin->organizations()->attach($organization, [ + $userAcmeAdmin->organizations()->attach($organization1, [ 'role' => 'admin', ]); + $timeEntriesEmployees = TimeEntry::factory() + ->count(10) + ->forUser($employee1) + ->forOrganization($organization1) + ->create(); $client = Client::factory()->create([ 'name' => 'Big Company', ]); @@ -50,6 +59,20 @@ public function run(): void $internalProject = Project::factory()->create([ 'name' => 'Internal Project', ]); + + $organization2 = Organization::factory()->create([ + 'name' => 'Rival Corp', + ]); + $user1 = User::factory()->withPersonalOrganization()->create([ + 'name' => 'Other User', + 'email' => 'test@rival-company.test', + ]); + $user1->organizations()->attach($organization2, [ + 'role' => 'admin', + ]); + $otherCompanyProject = Project::factory()->forClient($client)->create([ + 'name' => 'Scale Company', + ]); } private function deleteAll(): void diff --git a/phpunit.xml b/phpunit.xml index 7aedc2e9..82235621 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -21,7 +21,7 @@ - + diff --git a/routes/api.php b/routes/api.php index 86db842f..5aa3c0c7 100644 --- a/routes/api.php +++ b/routes/api.php @@ -2,7 +2,7 @@ declare(strict_types=1); -use Illuminate\Http\Request; +use App\Http\Controllers\Api\V1\ProjectController; use Illuminate\Support\Facades\Route; /* @@ -16,6 +16,13 @@ | */ -//Route::middleware('auth:api')->get('/user', function (Request $request) { -// return $request->user(); -//}); +Route::middleware('auth:api')->prefix('v1')->name('v1.')->group(static function () { + Route::name('projects.')->group(static function () { + Route::get('/organization/{organization}/projects', [ProjectController::class, 'index'])->name('index'); + Route::get('/organization/{organization}/projects/{project}', [ProjectController::class, 'show'])->name('show'); + Route::post('/organization/{organization}/projects', [ProjectController::class, 'store'])->name('store'); + Route::put('/organization/{organization}/projects/{project}', [ProjectController::class, 'update'])->name('update'); + Route::delete('/organization/{organization}/projects/{project}', [ProjectController::class, 'destroy'])->name('destroy'); + }); + +}); diff --git a/tests/Feature/UpdateTeamMemberRoleTest.php b/tests/Feature/UpdateTeamMemberRoleTest.php index e12b748a..7eebecc9 100644 --- a/tests/Feature/UpdateTeamMemberRoleTest.php +++ b/tests/Feature/UpdateTeamMemberRoleTest.php @@ -21,11 +21,11 @@ public function test_team_member_roles_can_be_updated(): void ); $response = $this->put('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id, [ - 'role' => 'editor', + 'role' => 'employee', ]); $this->assertTrue($otherUser->fresh()->hasTeamRole( - $user->currentTeam->fresh(), 'editor' + $user->currentTeam->fresh(), 'employee' )); } @@ -40,7 +40,7 @@ public function test_only_team_owner_can_update_team_member_roles(): void $this->actingAs($otherUser); $response = $this->put('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id, [ - 'role' => 'editor', + 'role' => 'employee', ]); $this->assertTrue($otherUser->fresh()->hasTeamRole( diff --git a/tests/Unit/Database/MigrationTest.php b/tests/Unit/Database/MigrationTest.php new file mode 100644 index 00000000..40e7349f --- /dev/null +++ b/tests/Unit/Database/MigrationTest.php @@ -0,0 +1,27 @@ +assertTrue(true); + } + + public function testFreshMigrationWithSeederAndRollbackRunsSuccessfully(): void + { + Artisan::call('migrate:fresh --seed'); + Artisan::call('migrate:rollback'); + $this->assertTrue(true); + } +} diff --git a/tests/Unit/Database/SeederTest.php b/tests/Unit/Database/SeederTest.php new file mode 100644 index 00000000..7bec6d6a --- /dev/null +++ b/tests/Unit/Database/SeederTest.php @@ -0,0 +1,27 @@ +assertTrue(true); + } + + public function test_fresh_migration_with_seeder_and_rollback_runs_successfully(): void + { + Artisan::call('migrate:fresh --seed'); + Artisan::call('migrate:rollback'); + $this->assertTrue(true); + } +} diff --git a/tests/Unit/Endpoint/Api/V1/ProjectEndpointTest.php b/tests/Unit/Endpoint/Api/V1/ProjectEndpointTest.php new file mode 100644 index 00000000..cbdc9b8d --- /dev/null +++ b/tests/Unit/Endpoint/Api/V1/ProjectEndpointTest.php @@ -0,0 +1,106 @@ + $permissions + * @return object{user: User, organization: Organization} + */ + private function createUserWithPermission(array $permissions): object + { + Jetstream::role('custom-test', 'Custom Test', $permissions)->description('Role custom for testing'); + $organization = Organization::factory()->create(); + $user = User::factory()->create(); + $organization->users()->attach($user, [ + 'role' => 'custom-test', + ]); + + return (object) [ + 'user' => $user, + 'organization' => $organization, + ]; + } + + public function test_index_endpoint_fails_if_user_has_no_permission_to_view_projects(): void + { + // Arrange + $data = $this->createUserWithPermission([ + ]); + $projects = Project::factory()->forOrganization($data->organization)->createMany(4); + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.projects.index', [$data->organization->getKey()])); + + // Assert + $response->assertStatus(403); + } + + public function test_index_endpoint_returns_list_of_all_projects_of_organization(): void + { + // Arrange + $data = $this->createUserWithPermission([ + 'projects:view', + ]); + $projects = Project::factory()->forOrganization($data->organization)->createMany(4); + Passport::actingAs($data->user); + + // Act + $response = $this->getJson(route('api.v1.projects.index', [$data->organization->getKey()])); + + // Assert + $response->assertStatus(200); + $response->assertJsonCount(4, 'data'); + } + + public function test_store_endpoint_fails_if_user_has_no_permission_to_create_projects(): void + { + // Arrange + $data = $this->createUserWithPermission([ + ]); + $project = Project::factory()->forOrganization($data->organization)->make(); + Passport::actingAs($data->user); + + // Act + $response = $this->postJson(route('api.v1.projects.store', [$data->organization->getKey()]), $project->toArray()); + + // Assert + $response->assertStatus(403); + } + + public function test_store_endpoint_creates_new_project(): void + { + // Arrange + $data = $this->createUserWithPermission([ + 'projects:create', + ]); + $project = Project::factory()->forOrganization($data->organization)->make(); + Passport::actingAs($data->user); + + // Act + $response = $this->postJson(route('api.v1.projects.store', [$data->organization->getKey()]), [ + 'name' => $project->name, + 'color' => $project->color, + 'organization_id' => $project->organization_id, + ]); + + // Assert + $response->assertStatus(201); + $this->assertDatabaseHas(Project::class, [ + 'name' => $project->name, + 'color' => $project->color, + 'organization_id' => $project->organization_id, + ]); + } +}