From 96823378ec1a24e3e5b0392d1a65e2d508ee1fd5 Mon Sep 17 00:00:00 2001 From: James Brooks Date: Wed, 15 Jan 2025 21:40:46 +0000 Subject: [PATCH] User Management (#153) Co-authored-by: Joel Butcher --- composer.json | 1 + config/cachet.php | 4 +- database/factories/UserFactory.php | 44 +++++ ...025_01_11_090556_create_api_keys_table.php | 33 ++++ ...5_121008_add_admin_flag_to_users_table.php | 28 ++++ database/seeders/DatabaseSeeder.php | 1 + phpstan-baseline.neon | 7 + phpstan.neon.dist | 1 + resources/lang/en/api_key.php | 31 ++++ resources/lang/en/navigation.php | 1 + resources/lang/en/user.php | 19 +++ .../filament/pages/api-key/index.blade.php | 27 ++++ routes/api.php | 42 ++++- src/Cachet.php | 16 ++ src/Commands/MakeUserCommand.php | 47 ++++-- src/Concerns/CachetUser.php | 11 ++ src/Concerns/GuardsApiAbilities.php | 21 +++ src/Concerns/User.php | 8 - src/Filament/Resources/ApiKeyResource.php | 153 ++++++++++++++++++ .../ApiKeyResource/Pages/CreateApiKey.php | 35 ++++ .../ApiKeyResource/Pages/ListApiKeys.php | 23 +++ src/Filament/Resources/UserResource.php | 135 ++++++++++++++++ .../UserResource/Pages/CreateUser.php | 12 ++ .../Resources/UserResource/Pages/EditUser.php | 19 +++ .../UserResource/Pages/ListUsers.php | 19 +++ .../Pages/CreateWebhookSubscription.php | 1 + .../Controllers/Api/ComponentController.php | 9 ++ .../Api/ComponentGroupController.php | 8 + .../Controllers/Api/IncidentController.php | 9 ++ .../Api/IncidentTemplateController.php | 9 ++ .../Api/IncidentUpdateController.php | 9 ++ src/Http/Controllers/Api/MetricController.php | 9 ++ .../Controllers/Api/MetricPointController.php | 7 + .../Controllers/Api/ScheduleController.php | 9 ++ .../Api/ScheduleUpdateController.php | 9 ++ src/Models/User.php | 77 +++++++++ testbench.yaml | 12 ++ tests/Feature/Api/ComponentGroupTest.php | 74 +++++++++ tests/Feature/Api/ComponentTest.php | 85 ++++++++++ tests/Feature/Api/IncidentTemplateTest.php | 74 +++++++++ tests/Feature/Api/IncidentTest.php | 83 ++++++++++ tests/Feature/Api/IncidentUpdateTest.php | 92 +++++++++++ tests/Feature/Api/MetricPointTest.php | 47 ++++++ tests/Feature/Api/MetricTest.php | 74 +++++++++ tests/Feature/Api/ScheduleTest.php | 81 ++++++++++ tests/Feature/Api/ScheduleUpdateTest.php | 91 +++++++++++ workbench/app/User.php | 27 ++-- ...01_create_personal_access_tokens_table.php | 33 ++++ 48 files changed, 1626 insertions(+), 41 deletions(-) create mode 100644 database/factories/UserFactory.php create mode 100644 database/migrations/2025_01_11_090556_create_api_keys_table.php create mode 100644 database/migrations/2025_01_15_121008_add_admin_flag_to_users_table.php create mode 100644 phpstan-baseline.neon create mode 100644 resources/lang/en/api_key.php create mode 100644 resources/views/filament/pages/api-key/index.blade.php create mode 100644 src/Concerns/CachetUser.php create mode 100644 src/Concerns/GuardsApiAbilities.php delete mode 100644 src/Concerns/User.php create mode 100644 src/Filament/Resources/ApiKeyResource.php create mode 100644 src/Filament/Resources/ApiKeyResource/Pages/CreateApiKey.php create mode 100644 src/Filament/Resources/ApiKeyResource/Pages/ListApiKeys.php create mode 100644 src/Filament/Resources/UserResource.php create mode 100644 src/Filament/Resources/UserResource/Pages/CreateUser.php create mode 100644 src/Filament/Resources/UserResource/Pages/EditUser.php create mode 100644 src/Filament/Resources/UserResource/Pages/ListUsers.php create mode 100644 src/Models/User.php create mode 100644 workbench/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php diff --git a/composer.json b/composer.json index 11482eb8..8eb099fd 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ "illuminate/events": "^11.23.0", "illuminate/queue": "^11.23.0", "illuminate/support": "^11.23.0", + "laravel/sanctum": "^4.0", "nesbot/carbon": "^2.70", "spatie/laravel-data": "^4.11", "spatie/laravel-query-builder": "^5.5", diff --git a/config/cachet.php b/config/cachet.php index 760f3584..8db6dc36 100644 --- a/config/cachet.php +++ b/config/cachet.php @@ -33,7 +33,9 @@ | This is the model that will be used to authenticate users. This model | must be an instance of Illuminate\Foundation\Auth\User. */ - 'user_model' => \App\Models\User::class, + 'user_model' => env('CACHET_USER_MODEL', \App\Models\User::class), + + 'user_migrations' => env('CACHET_USER_MIGRATIONS', true), /* |-------------------------------------------------------------------------- diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php new file mode 100644 index 00000000..5787735b --- /dev/null +++ b/database/factories/UserFactory.php @@ -0,0 +1,44 @@ + + */ +class UserFactory extends Factory +{ + /** + * The current password being used by the factory. + */ + protected static ?string $password; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->name(), + 'email' => fake()->unique()->safeEmail(), + 'email_verified_at' => now(), + 'password' => static::$password ??= Hash::make('password'), + 'remember_token' => Str::random(10), + ]; + } + + /** + * Indicate that the model's email address should be unverified. + */ + public function unverified(): static + { + return $this->state(fn (array $attributes) => [ + 'email_verified_at' => null, + ]); + } +} diff --git a/database/migrations/2025_01_11_090556_create_api_keys_table.php b/database/migrations/2025_01_11_090556_create_api_keys_table.php new file mode 100644 index 00000000..ed7e7747 --- /dev/null +++ b/database/migrations/2025_01_11_090556_create_api_keys_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('name')->unique(); + $table->string('token_', 64)->unique(); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable(); + $table->boolean('revoked_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('api_keys'); + } +}; diff --git a/database/migrations/2025_01_15_121008_add_admin_flag_to_users_table.php b/database/migrations/2025_01_15_121008_add_admin_flag_to_users_table.php new file mode 100644 index 00000000..0b3c749b --- /dev/null +++ b/database/migrations/2025_01_15_121008_add_admin_flag_to_users_table.php @@ -0,0 +1,28 @@ +boolean('is_admin')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('is_admin'); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 288fec8b..00e6cc2d 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -50,6 +50,7 @@ public function run(): void 'email' => 'test@test.com', 'password' => bcrypt('test123'), 'email_verified_at' => now(), + 'is_admin' => true, ]); Schedule::create([ diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000..3597b3b2 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,7 @@ +parameters: + ignoreErrors: + - + message: '#^Access to an undefined property Laravel\\Sanctum\\PersonalAccessToken\:\:\$expires_at\.$#' + identifier: property.notFound + count: 3 + path: src/Filament/Resources/ApiKeyResource.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index b2045ecb..16353a52 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,6 @@ includes: - vendor/larastan/larastan/extension.neon + - phpstan-baseline.neon parameters: level: 5 diff --git a/resources/lang/en/api_key.php b/resources/lang/en/api_key.php new file mode 100644 index 00000000..f51b28bc --- /dev/null +++ b/resources/lang/en/api_key.php @@ -0,0 +1,31 @@ + 'API Key|API Keys', + 'show_token' => [ + 'heading' => 'Your API Token has been generated', + 'description' => 'Please copy your new API token. For your security, it won\'t be shown again.', + 'copy_tooltip' => 'Token copied!', + ], + 'abilities_label' => ':ability :resource', + 'form' => [ + 'name_label' => 'Token Name', + 'expires_at_label' => 'Expires At', + 'expires_at_helper' => 'Expires at midnight. Leave empty for no expiry', + 'expires_at_validation' => 'The expiry date must be in the future', + 'abilities_label' => 'Permissions', + 'abilities_hint' => 'Leaving this empty will give the token full permissions', + ], + 'list' => [ + 'actions' => [ + 'revoke' => 'Revoke', + ], + 'headers' => [ + 'name' => 'Token Name', + 'abilities' => 'Permissions', + 'created_at' => 'Created At', + 'expires_at' => 'Expires At', + 'updated_at' => 'Updated At', + ], + ], +]; diff --git a/resources/lang/en/navigation.php b/resources/lang/en/navigation.php index 6ae17832..c2bb3cf9 100644 --- a/resources/lang/en/navigation.php +++ b/resources/lang/en/navigation.php @@ -7,6 +7,7 @@ 'manage_cachet' => 'Manage Cachet', 'manage_customization' => 'Manage Customization', 'manage_theme' => 'Manage Theme', + 'manage_api_keys' => 'Manage API Keys', 'manage_webhooks' => 'Manage Webhooks', ], ], diff --git a/resources/lang/en/user.php b/resources/lang/en/user.php index 81b48181..9e0a4ff9 100644 --- a/resources/lang/en/user.php +++ b/resources/lang/en/user.php @@ -5,4 +5,23 @@ 'admin' => 'Admin', 'user' => 'User', ], + 'resource_label' => 'User|Users', + 'list' => [ + 'headers' => [ + 'name' => 'Name', + 'email' => 'Email Address', + 'email_verified_at' => 'Email Verified At', + 'is_admin' => 'Is Admin?', + ], + 'actions' => [ + 'verify_email' => 'Verify Email', + ], + ], + 'form' => [ + 'name_label' => 'Name', + 'email_label' => 'Email Address', + 'password_label' => 'Password', + 'password_confirmation_label' => 'Confirm Password', + 'is_admin_label' => 'Admin', + ], ]; diff --git a/resources/views/filament/pages/api-key/index.blade.php b/resources/views/filament/pages/api-key/index.blade.php new file mode 100644 index 00000000..17ac8af5 --- /dev/null +++ b/resources/views/filament/pages/api-key/index.blade.php @@ -0,0 +1,27 @@ + + @if($token = session('api-token')) + +

+ {{ __('cachet::api_key.show_token.description') }} +

+ +
+
+ {{ $token }} +
+
+
+ @endsession + + {{ $this->table }} +
diff --git a/routes/api.php b/routes/api.php index 223a4914..7871188e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,5 +1,6 @@ IncidentTemplateController::class, 'metrics' => MetricController::class, 'schedules' => ScheduleController::class, -]); +], ['except' => ['store', 'update', 'destroy']]); -Route::apiResource('incidents.updates', IncidentUpdateController::class) +Route::apiResource('incidents.updates', IncidentUpdateController::class, [ + 'except' => ['store', 'update', 'destroy'], +]) ->scoped(['updateable_id']); -Route::apiResource('schedules.updates', ScheduleUpdateController::class) +Route::apiResource('schedules.updates', ScheduleUpdateController::class, [ + 'except' => ['store', 'update', 'destroy'], +]) ->scoped(['updateable_id']); -Route::apiResource('metrics.points', MetricPointController::class) +Route::apiResource('metrics.points', MetricPointController::class, [ + 'except' => ['store', 'update', 'destroy'], +]) ->parameter('points', 'metricPoint') ->scoped(); +Route::middleware(['auth:sanctum'])->group(function () { + Route::apiResources([ + 'components' => ComponentController::class, + 'component-groups' => ComponentGroupController::class, + 'incidents' => IncidentController::class, + 'incident-templates' => IncidentTemplateController::class, + 'metrics' => MetricController::class, + 'schedules' => ScheduleController::class, + ], ['except' => ['index', 'show']]); + + Route::apiResource('incidents.updates', IncidentUpdateController::class, [ + 'except' => ['index', 'show'], + ]) + ->scoped(['updateable_id']); + + Route::apiResource('schedules.updates', ScheduleUpdateController::class, [ + 'except' => ['index', 'show'], + ]) + ->scoped(['updateable_id']); + + Route::apiResource('metrics.points', MetricPointController::class, [ + 'except' => ['index', 'show'], + ]) + ->parameter('points', 'metricPoint') + ->scoped(); +}); + Route::get('/ping', [GeneralController::class, 'ping'])->name('ping'); Route::get('/version', [GeneralController::class, 'version'])->name('version'); Route::get('/status', StatusController::class)->name('status'); diff --git a/src/Cachet.php b/src/Cachet.php index 324a2018..12812de7 100644 --- a/src/Cachet.php +++ b/src/Cachet.php @@ -67,4 +67,20 @@ public static function version(): string { return trim(file_get_contents(__DIR__.'/../VERSION')); } + + /** @return array> */ + public static function getResourceApiAbilities(): array + { + return [ + 'components' => ['manage', 'delete'], + 'component-groups' => ['manage', 'delete'], + 'incidents' => ['manage', 'delete'], + 'incident-updates' => ['manage', 'delete'], + 'incident-templates' => ['manage', 'delete'], + 'metrics' => ['manage', 'delete'], + 'metric-points' => ['manage', 'delete'], + 'schedules' => ['manage', 'delete'], + 'schedule-updates' => ['manage', 'delete'], + ]; + } } diff --git a/src/Commands/MakeUserCommand.php b/src/Commands/MakeUserCommand.php index 5064d891..6a8c394e 100644 --- a/src/Commands/MakeUserCommand.php +++ b/src/Commands/MakeUserCommand.php @@ -4,6 +4,7 @@ use Illuminate\Console\Command; +use function Laravel\Prompts\confirm; use function Laravel\Prompts\password; use function Laravel\Prompts\text; @@ -14,7 +15,7 @@ class MakeUserCommand extends Command * * @var string */ - protected $signature = 'cachet:make:user {email?} {--password=}'; + protected $signature = 'cachet:make:user {email?} {--password= : The user\'s password} {--admin : Whether the user is an admin}'; /** * The console command description. @@ -38,26 +39,37 @@ class MakeUserCommand extends Command */ protected ?string $password = null; + /** + * Whether the user is an admin. + */ + protected ?bool $isAdmin = null; + /** * Execute the console command. */ - public function handle() + public function handle(): int { - if ($password = $this->option('password')) { - $this->password = $password; + $this->email = $this->argument('email'); + $this->isAdmin = $this->option('admin'); + $this->password = $this->option('password'); + + $this->promptName(); + + if (! $this->email) { + $this->promptEmail(); } - if ($this->email = $this->argument('email')) { - $this->createUser(); + if (! $this->isAdmin) { + $this->promptIsAdmin(); + } - return; + if (! $this->password) { + $this->promptPassword(); } - $this - ->promptEmail() - ->promptName() - ->promptPassword() - ->createUser(); + $this->createUser(); + + return self::SUCCESS; } /** @@ -94,6 +106,16 @@ protected function promptPassword(): self return $this; } + /** + * Prompt the user for whether they are an admin. + */ + protected function promptIsAdmin(): self + { + $this->isAdmin = confirm('Is the user an admin?', default: false); + + return $this; + } + /** * Create the user. */ @@ -105,6 +127,7 @@ protected function createUser(): void 'name' => $this->data['name'], 'email' => $this->email, 'password' => bcrypt($this->password), + 'is_admin' => $this->isAdmin, ]); $this->components->info('User created successfully.'); diff --git a/src/Concerns/CachetUser.php b/src/Concerns/CachetUser.php new file mode 100644 index 00000000..008df2d9 --- /dev/null +++ b/src/Concerns/CachetUser.php @@ -0,0 +1,11 @@ +user(); + + if (! $user->tokenCan($ability)) { + throw new MissingAbilityException($ability); + } + } +} diff --git a/src/Concerns/User.php b/src/Concerns/User.php deleted file mode 100644 index da232a5c..00000000 --- a/src/Concerns/User.php +++ /dev/null @@ -1,8 +0,0 @@ -where('tokenable_type', auth()->user()::class) + ->where('tokenable_id', auth()->id()); + } + + public static function getNavigationGroup(): ?string + { + return __('cachet::navigation.settings.label'); + } + + public static function getNavigationLabel(): string + { + return __('cachet::navigation.settings.items.manage_api_keys'); + } + + public static function form(Form $form): Form + { + return $form + ->schema([ + Section::make()->schema([ + Forms\Components\TextInput::make('name') + ->label(__('cachet::api_key.form.name_label')) + ->required() + ->unique('api_keys', 'name') + ->maxLength(255) + ->columnSpanFull() + ->autofocus() + ->autocomplete(false), + Forms\Components\DatePicker::make('expires_at') + ->label(__('cachet::api_key.form.expires_at_label')) + ->helperText(__('cachet::api_key.form.expires_at_helper')) + ->nullable() + ->rules(['after:today']) + ->validationMessages(['after' => __('cachet::api_key.form.expires_at_validation')]) + ->placeholder(null), + Forms\Components\CheckboxList::make('abilities') + ->label(__('cachet::api_key.form.abilities_label')) + ->hint(__('cachet::api_key.form.abilities_hint')) + ->hintColor('warning') + ->options(self::getAbilities()) + ->columns(3) + ])->columnSpan(4), + ])->columns(4); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('name') + ->label(__('cachet::api_key.list.headers.name')) + ->searchable(), + Tables\Columns\TextColumn::make('abilities') + ->label(__('cachet::api_key.list.headers.abilities')) + ->color('gray') + ->badge() + ->limitList(3), + Tables\Columns\TextColumn::make('created_at') + ->label(__('cachet::api_key.list.headers.created_at')) + ->dateTime() + ->sortable(), + Tables\Columns\TextColumn::make('expires_at') + ->label(__('cachet::api_key.list.headers.expires_at')) + ->sortable() + ->color(fn (PersonalAccessToken $record) => $record->expires_at ? null : 'gray') + ->badge(fn (PersonalAccessToken $record) => !$record->expires_at) + ->getStateUsing(fn (PersonalAccessToken $record) => $record->expires_at?->format(Table::$defaultDateDisplayFormat) ?? 'N/A'), + Tables\Columns\TextColumn::make('updated_at') + ->label(__('cachet::api_key.list.headers.updated_at')) + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + Tables\Filters\Filter::make('expired') + ->toggle() + ->query(fn (Builder $query) => $query->where('expires_at', '<', now())), + ]) + ->actions([ + Tables\Actions\DeleteAction::make('revoke') + ->label(__('cachet::api_key.list.actions.revoke')), + ]) + ->bulkActions([ + Tables\Actions\BulkAction::make('revoke') + ->label(__('cachet::api_key.list.actions.revoke')) + ->action(fn (Collection $records) => $records->each->delete()) + ->deselectRecordsAfterCompletion() + ->requiresConfirmation() + ->color('danger') + ->icon('heroicon-o-trash'), + ]); + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListApiKeys::route('/'), + 'create' => Pages\CreateApiKey::route('/create'), + ]; + } + + public static function getModelLabel(): string + { + return trans_choice('cachet::api_key.resource_label', 1); + } + + public static function getPluralModelLabel(): string + { + return trans_choice('cachet::api_key.resource_label', 2); + } + + /** @return array */ + private static function getAbilities(): array + { + $abilities = []; + + foreach (Cachet::getResourceApiAbilities() as $resource => $apiAbilities) { + foreach ($apiAbilities as $ability) { + $key = "{$resource}.{$ability}"; + $abilities[$key] = Str::headline(__('cachet::api_key.abilities_label', [ + 'ability' => $ability, + 'resource' => Str::plural($resource), + ])); + } + } + + return $abilities; + } +} diff --git a/src/Filament/Resources/ApiKeyResource/Pages/CreateApiKey.php b/src/Filament/Resources/ApiKeyResource/Pages/CreateApiKey.php new file mode 100644 index 00000000..b2df36e0 --- /dev/null +++ b/src/Filament/Resources/ApiKeyResource/Pages/CreateApiKey.php @@ -0,0 +1,35 @@ +user(); + + $token = $user->createToken( + name: $data['name'], + abilities: empty($data['abilities']) ? ['*'] : $data['abilities'], + expiresAt: filled($data['expires_at']) ? Carbon::parse($data['expires_at']) : null, + ); + + session()->flash('api-token', $token->plainTextToken); + + return $token->accessToken; + } +} diff --git a/src/Filament/Resources/ApiKeyResource/Pages/ListApiKeys.php b/src/Filament/Resources/ApiKeyResource/Pages/ListApiKeys.php new file mode 100644 index 00000000..3df911cf --- /dev/null +++ b/src/Filament/Resources/ApiKeyResource/Pages/ListApiKeys.php @@ -0,0 +1,23 @@ +user()->isAdmin(); + } + + public static function form(Form $form): Form + { + return $form + ->schema([ + Forms\Components\Section::make()->columns()->schema([ + Forms\Components\TextInput::make('name') + ->label(__('cachet::user.form.name_label')) + ->required() + ->maxLength(255) + ->autocomplete(false), + + Forms\Components\TextInput::make('email') + ->label(__('cachet::user.form.email_label')) + ->email() + ->required() + ->maxLength(255) + ->autocomplete(false) + ->unique('users', 'email', ignoreRecord: true), + + Forms\Components\TextInput::make('password') + ->label(__('cachet::user.form.password_label')) + ->password() + ->required(fn (string $context): bool => $context === 'create') + ->maxLength(255) + ->autocomplete(false) + ->dehydrateStateUsing(fn ($state) => Hash::make($state)) + ->dehydrated(fn ($state) => filled($state)), + + Forms\Components\TextInput::make('password_confirmation') + ->password() + ->required(fn (string $context): bool => $context === 'create') + ->maxLength(255) + ->same('password') + ->label(__('cachet::user.form.password_confirmation_label')), + + Forms\Components\Toggle::make('is_admin') + ->label(__('cachet::user.form.is_admin_label')) + ->disabled(fn (?User $record) => $record?->is(auth()->user())), + ]) + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('name') + ->label(__('cachet::user.list.headers.name')) + ->searchable(), + + Tables\Columns\TextColumn::make('email') + ->label(__('cachet::user.list.headers.email')) + ->searchable(), + + Tables\Columns\TextColumn::make('email_verified_at') + ->label(__('cachet::user.list.headers.email_verified_at')) + ->dateTime(), + + Tables\Columns\ToggleColumn::make('is_admin') + ->disabled(fn (User $record) => auth()->user()->is($record)) + ->label(__('cachet::user.list.headers.is_admin')), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + Tables\Actions\Action::make('verify-email') + ->label(__('cachet::user.list.actions.verify_email')) + ->icon('heroicon-o-check-badge') + ->disabled(fn (User $record): bool => $record->hasVerifiedEmail()) + ->action(fn (Builder $query, User $record) => $record->sendEmailVerificationNotification()), + ]) + ->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'), + ]; + } + + public static function getLabel(): ?string + { + return trans_choice('cachet::user.resource_label', 1); + } + + public static function getPluralLabel(): ?string + { + return trans_choice('cachet::user.resource_label', 2); + } + + public static function getModel(): string + { + return config('cachet.user_model'); + } +} diff --git a/src/Filament/Resources/UserResource/Pages/CreateUser.php b/src/Filament/Resources/UserResource/Pages/CreateUser.php new file mode 100644 index 00000000..a17f2322 --- /dev/null +++ b/src/Filament/Resources/UserResource/Pages/CreateUser.php @@ -0,0 +1,12 @@ +guard('components.manage'); + $component = $createComponentAction->handle( $data, ); @@ -101,6 +106,8 @@ public function show(Component $component) */ public function update(UpdateComponentRequestData $data, Component $component, UpdateComponent $updateComponentAction) { + $this->guard('components.manage'); + $updateComponentAction->handle($component, $data); return ComponentResource::make($component->fresh()); @@ -115,6 +122,8 @@ public function update(UpdateComponentRequestData $data, Component $component, U */ public function destroy(Component $component, DeleteComponent $deleteComponentAction) { + $this->guard('components.delete'); + // @todo what happens to incidents linked to this component? // @todo re-calculate existing component orders? diff --git a/src/Http/Controllers/Api/ComponentGroupController.php b/src/Http/Controllers/Api/ComponentGroupController.php index 797d9f56..a3775561 100644 --- a/src/Http/Controllers/Api/ComponentGroupController.php +++ b/src/Http/Controllers/Api/ComponentGroupController.php @@ -5,6 +5,7 @@ use Cachet\Actions\ComponentGroup\CreateComponentGroup; use Cachet\Actions\ComponentGroup\DeleteComponentGroup; use Cachet\Actions\ComponentGroup\UpdateComponentGroup; +use Cachet\Concerns\GuardsApiAbilities; use Cachet\Data\Requests\ComponentGroup\CreateComponentGroupRequestData; use Cachet\Data\Requests\ComponentGroup\UpdateComponentGroupRequestData; use Cachet\Http\Resources\ComponentGroup as ComponentGroupResource; @@ -18,6 +19,8 @@ */ class ComponentGroupController extends Controller { + use GuardsApiAbilities; + /** * List Component Groups * @@ -51,6 +54,8 @@ public function index() */ public function store(CreateComponentGroupRequestData $data, CreateComponentGroup $createComponentGroupAction) { + $this->guard('component-groups.manage'); + $componentGroup = $createComponentGroupAction->handle($data); return ComponentGroupResource::make($componentGroup); @@ -87,6 +92,8 @@ public function show(ComponentGroup $componentGroup) */ public function update(UpdateComponentGroupRequestData $data, ComponentGroup $componentGroup, UpdateComponentGroup $updateComponentGroupAction) { + $this->guard('component-groups.manage'); + $updateComponentGroupAction->handle($componentGroup, $data); return ComponentGroupResource::make($componentGroup->fresh()); @@ -103,6 +110,7 @@ public function update(UpdateComponentGroupRequestData $data, ComponentGroup $co */ public function destroy(ComponentGroup $componentGroup, DeleteComponentGroup $deleteComponentGroupAction) { + $this->guard('component-groups.delete'); $deleteComponentGroupAction->handle($componentGroup); return response()->noContent(); diff --git a/src/Http/Controllers/Api/IncidentController.php b/src/Http/Controllers/Api/IncidentController.php index be034867..d363d3f2 100644 --- a/src/Http/Controllers/Api/IncidentController.php +++ b/src/Http/Controllers/Api/IncidentController.php @@ -5,6 +5,7 @@ use Cachet\Actions\Incident\CreateIncident; use Cachet\Actions\Incident\DeleteIncident; use Cachet\Actions\Incident\UpdateIncident; +use Cachet\Concerns\GuardsApiAbilities; use Cachet\Data\Requests\Incident\CreateIncidentRequestData; use Cachet\Data\Requests\Incident\UpdateIncidentRequestData; use Cachet\Http\Resources\Incident as IncidentResource; @@ -19,6 +20,8 @@ */ class IncidentController extends Controller { + use GuardsApiAbilities; + /** * The list of allowed includes. */ @@ -67,6 +70,8 @@ public function index() */ public function store(CreateIncidentRequestData $data, CreateIncident $createIncidentAction) { + $this->guard('incidents.manage'); + $incident = $createIncidentAction->handle($data); return IncidentResource::make($incident); @@ -103,6 +108,8 @@ public function show(Incident $incident) */ public function update(UpdateIncidentRequestData $data, Incident $incident, UpdateIncident $updateIncidentAction) { + $this->guard('incidents.manage'); + $updateIncidentAction->handle($incident, $data); return IncidentResource::make($incident->fresh()); @@ -117,6 +124,8 @@ public function update(UpdateIncidentRequestData $data, Incident $incident, Upda */ public function destroy(Incident $incident, DeleteIncident $deleteIncidentAction) { + $this->guard('incidents.delete'); + $deleteIncidentAction->handle($incident); return response()->noContent(); diff --git a/src/Http/Controllers/Api/IncidentTemplateController.php b/src/Http/Controllers/Api/IncidentTemplateController.php index 829864d1..9a4d591b 100644 --- a/src/Http/Controllers/Api/IncidentTemplateController.php +++ b/src/Http/Controllers/Api/IncidentTemplateController.php @@ -5,6 +5,7 @@ use Cachet\Actions\IncidentTemplate\CreateIncidentTemplate; use Cachet\Actions\IncidentTemplate\DeleteIncidentTemplate; use Cachet\Actions\IncidentTemplate\UpdateIncidentTemplate; +use Cachet\Concerns\GuardsApiAbilities; use Cachet\Data\Requests\IncidentTemplate\CreateIncidentTemplateRequestData; use Cachet\Data\Requests\IncidentTemplate\UpdateIncidentTemplateRequestData; use Cachet\Http\Resources\IncidentTemplate as IncidentTemplateResource; @@ -18,6 +19,8 @@ */ class IncidentTemplateController extends Controller { + use GuardsApiAbilities; + /** * List Incident Templates * @@ -53,6 +56,8 @@ public function index() */ public function store(CreateIncidentTemplateRequestData $data, CreateIncidentTemplate $createIncidentTemplateAction) { + $this->guard('incident-templates.manage'); + $template = $createIncidentTemplateAction->handle($data); return IncidentTemplateResource::make($template); @@ -83,6 +88,8 @@ public function show(IncidentTemplate $incidentTemplate) */ public function update(UpdateIncidentTemplateRequestData $data, IncidentTemplate $incidentTemplate, UpdateIncidentTemplate $updateIncidentTemplateAction) { + $this->guard('incident-templates.manage'); + $template = $updateIncidentTemplateAction->handle($incidentTemplate, $data); return IncidentTemplateResource::make($template); @@ -97,6 +104,8 @@ public function update(UpdateIncidentTemplateRequestData $data, IncidentTemplate */ public function destroy(IncidentTemplate $incidentTemplate) { + $this->guard('incident-templates.delete'); + app(DeleteIncidentTemplate::class)->handle($incidentTemplate); return response()->noContent(); diff --git a/src/Http/Controllers/Api/IncidentUpdateController.php b/src/Http/Controllers/Api/IncidentUpdateController.php index 4ae22c2a..9843aae3 100644 --- a/src/Http/Controllers/Api/IncidentUpdateController.php +++ b/src/Http/Controllers/Api/IncidentUpdateController.php @@ -5,6 +5,7 @@ use Cachet\Actions\Update\CreateUpdate; use Cachet\Actions\Update\DeleteUpdate; use Cachet\Actions\Update\EditUpdate; +use Cachet\Concerns\GuardsApiAbilities; use Cachet\Data\Requests\IncidentUpdate\CreateIncidentUpdateRequestData; use Cachet\Data\Requests\IncidentUpdate\EditIncidentUpdateRequestData; use Cachet\Http\Resources\Update as UpdateResource; @@ -20,6 +21,8 @@ */ class IncidentUpdateController extends Controller { + use GuardsApiAbilities; + /** * List Incident Updates * @@ -58,6 +61,8 @@ public function index(Incident $incident) */ public function store(CreateIncidentUpdateRequestData $data, Incident $incident, CreateUpdate $createUpdateAction) { + $this->guard('incident-updates.manage'); + $update = $createUpdateAction->handle($incident, $data); return UpdateResource::make($update); @@ -96,6 +101,8 @@ public function show(Incident $incident, Update $update) */ public function update(EditIncidentUpdateRequestData $data, Incident $incident, Update $update, EditUpdate $editUpdateAction) { + $this->guard('incident-updates.manage'); + $editUpdateAction->handle($update, $data); return UpdateResource::make($update->fresh()); @@ -110,6 +117,8 @@ public function update(EditIncidentUpdateRequestData $data, Incident $incident, */ public function destroy(Incident $incident, Update $update, DeleteUpdate $deleteUpdateAction) { + $this->guard('incident-updates.delete'); + $deleteUpdateAction->handle($update); return response()->noContent(); diff --git a/src/Http/Controllers/Api/MetricController.php b/src/Http/Controllers/Api/MetricController.php index 573e0b6c..8bacf905 100644 --- a/src/Http/Controllers/Api/MetricController.php +++ b/src/Http/Controllers/Api/MetricController.php @@ -5,6 +5,7 @@ use Cachet\Actions\Metric\CreateMetric; use Cachet\Actions\Metric\DeleteMetric; use Cachet\Actions\Metric\UpdateMetric; +use Cachet\Concerns\GuardsApiAbilities; use Cachet\Data\Requests\Metric\CreateMetricRequestData; use Cachet\Data\Requests\Metric\UpdateMetricRequestData; use Cachet\Http\Resources\Metric as MetricResource; @@ -19,6 +20,8 @@ */ class MetricController extends Controller { + use GuardsApiAbilities; + /** * List Metrics * @@ -60,6 +63,8 @@ public function index() */ public function store(CreateMetricRequestData $data, CreateMetric $createMetricAction) { + $this->guard('metrics.manage'); + $metric = $createMetricAction->handle($data); return MetricResource::make($metric); @@ -96,6 +101,8 @@ public function show(Metric $metric) */ public function update(UpdateMetricRequestData $data, Metric $metric, UpdateMetric $updateMetricAction) { + $this->guard('metrics.manage'); + $updateMetricAction->handle($metric, $data); return MetricResource::make($metric->fresh()); @@ -110,6 +117,8 @@ public function update(UpdateMetricRequestData $data, Metric $metric, UpdateMetr */ public function destroy(Metric $metric, DeleteMetric $deleteMetricAction) { + $this->guard('metrics.delete'); + $deleteMetricAction->handle($metric); return response()->noContent(); diff --git a/src/Http/Controllers/Api/MetricPointController.php b/src/Http/Controllers/Api/MetricPointController.php index a3817b6b..fb80d33f 100644 --- a/src/Http/Controllers/Api/MetricPointController.php +++ b/src/Http/Controllers/Api/MetricPointController.php @@ -4,6 +4,7 @@ use Cachet\Actions\Metric\CreateMetricPoint; use Cachet\Actions\Metric\DeleteMetricPoint; +use Cachet\Concerns\GuardsApiAbilities; use Cachet\Data\Requests\Metric\CreateMetricPointRequestData; use Cachet\Http\Resources\MetricPoint as MetricPointResource; use Cachet\Models\Metric; @@ -17,6 +18,8 @@ */ class MetricPointController extends Controller { + use GuardsApiAbilities; + /** * List Metric Points * @@ -53,6 +56,8 @@ public function index(Metric $metric) */ public function store(CreateMetricPointRequestData $data, Metric $metric, CreateMetricPoint $createMetricPointAction) { + $this->guard('metric-points.manage'); + $metricPoint = $createMetricPointAction->handle($metric, $data); return MetricPointResource::make($metricPoint) @@ -89,6 +94,8 @@ public function show(Metric $metric, MetricPoint $metricPoint) */ public function destroy(Metric $metric, MetricPoint $metricPoint, DeleteMetricPoint $deleteMetricPointAction) { + $this->guard('metric-points.delete'); + $deleteMetricPointAction->handle($metricPoint); return response()->noContent(); diff --git a/src/Http/Controllers/Api/ScheduleController.php b/src/Http/Controllers/Api/ScheduleController.php index 505545f6..19f14a69 100644 --- a/src/Http/Controllers/Api/ScheduleController.php +++ b/src/Http/Controllers/Api/ScheduleController.php @@ -5,6 +5,7 @@ use Cachet\Actions\Schedule\CreateSchedule; use Cachet\Actions\Schedule\DeleteSchedule; use Cachet\Actions\Schedule\UpdateSchedule; +use Cachet\Concerns\GuardsApiAbilities; use Cachet\Data\Requests\Schedule\CreateScheduleRequestData; use Cachet\Data\Requests\Schedule\UpdateScheduleRequestData; use Cachet\Http\Resources\Schedule as ScheduleResource; @@ -18,6 +19,8 @@ */ class ScheduleController extends Controller { + use GuardsApiAbilities; + /** * List Schedules * @@ -53,6 +56,8 @@ public function index() */ public function store(CreateScheduleRequestData $data, CreateSchedule $createScheduleAction) { + $this->guard('schedules.manage'); + $schedule = $createScheduleAction->handle($data); return ScheduleResource::make($schedule); @@ -89,6 +94,8 @@ public function show(Schedule $schedule) */ public function update(UpdateScheduleRequestData $data, Schedule $schedule, UpdateSchedule $updateScheduleAction) { + $this->guard('schedules.manage'); + $updateScheduleAction->handle($schedule, $data); return ScheduleResource::make($schedule->fresh()); @@ -103,6 +110,8 @@ public function update(UpdateScheduleRequestData $data, Schedule $schedule, Upda */ public function destroy(Schedule $schedule, DeleteSchedule $deleteScheduleAction) { + $this->guard('schedules.delete'); + $deleteScheduleAction->handle($schedule); return response()->noContent(); diff --git a/src/Http/Controllers/Api/ScheduleUpdateController.php b/src/Http/Controllers/Api/ScheduleUpdateController.php index 57721515..d479be06 100644 --- a/src/Http/Controllers/Api/ScheduleUpdateController.php +++ b/src/Http/Controllers/Api/ScheduleUpdateController.php @@ -5,6 +5,7 @@ use Cachet\Actions\Update\CreateUpdate; use Cachet\Actions\Update\DeleteUpdate; use Cachet\Actions\Update\EditUpdate; +use Cachet\Concerns\GuardsApiAbilities; use Cachet\Data\Requests\ScheduleUpdate\CreateScheduleUpdateRequestData; use Cachet\Data\Requests\ScheduleUpdate\EditScheduleUpdateRequestData; use Cachet\Http\Resources\Update as UpdateResource; @@ -21,6 +22,8 @@ */ class ScheduleUpdateController extends Controller { + use GuardsApiAbilities; + /** * List Schedule Updates * @@ -58,6 +61,8 @@ public function index(Schedule $schedule) */ public function store(CreateScheduleUpdateRequestData $data, Schedule $schedule, CreateUpdate $createUpdateAction) { + $this->guard('schedule-updates.manage'); + $update = $createUpdateAction->handle($schedule, $data); return UpdateResource::make($update); @@ -96,6 +101,8 @@ public function show(Schedule $schedule, Update $update) */ public function update(EditScheduleUpdateRequestData $data, Schedule $schedule, Update $update, EditUpdate $editUpdateAction) { + $this->guard('schedule-updates.manage'); + $editUpdateAction->handle($update, $data); return UpdateResource::make($update->fresh()); @@ -110,6 +117,8 @@ public function update(EditScheduleUpdateRequestData $data, Schedule $schedule, */ public function destroy(Schedule $schedule, Update $update, DeleteUpdate $deleteUpdateAction) { + $this->guard('schedule-updates.delete'); + $deleteUpdateAction->handle($update); return response()->noContent(); diff --git a/src/Models/User.php b/src/Models/User.php new file mode 100644 index 00000000..8292cb77 --- /dev/null +++ b/src/Models/User.php @@ -0,0 +1,77 @@ + */ + use HasApiTokens, HasFactory, Notifiable; + + /** + * The attributes that are mass assignable. + * + * @var list + */ + protected $fillable = [ + 'name', + 'email', + 'password', + 'is_admin', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var list + */ + protected $hidden = [ + 'password', + 'remember_token', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + 'is_admin' => 'bool', + ]; + } + + /** + * Determine if the user is an admin. + */ + public function isAdmin(): bool + { + return $this->is_admin; + } + + /** + * Create a new factory instance for the model. + */ + protected static function newFactory(): Factory + { + return UserFactory::new(); + } +} diff --git a/testbench.yaml b/testbench.yaml index e0ae06ef..588604c3 100644 --- a/testbench.yaml +++ b/testbench.yaml @@ -4,6 +4,18 @@ providers: - Cachet\CachetDashboardServiceProvider - Spatie\LaravelSettings\LaravelSettingsServiceProvider - Spatie\LaravelData\LaravelDataServiceProvider + - Laravel\Sanctum\SanctumServiceProvider + +env: + - AUTH_MODEL=\Workbench\App\User + - MAIL_MAILER=smtp + - MAIL_HOST=127.0.0.1 + - MAIL_PORT=2525 + - MAIL_USERNAME=cachet + - MAIL_PASSWORD=null + - MAIL_ENCRYPTION=null + - MAIL_FROM_ADDRESS="hello@cachethq.io" + - MAIL_FROM_NAME=Cachet migrations: - workbench/database/migrations diff --git a/tests/Feature/Api/ComponentGroupTest.php b/tests/Feature/Api/ComponentGroupTest.php index 4bcbae74..00812665 100644 --- a/tests/Feature/Api/ComponentGroupTest.php +++ b/tests/Feature/Api/ComponentGroupTest.php @@ -3,6 +3,10 @@ use Cachet\Models\Component; use Cachet\Models\ComponentGroup; +use Laravel\Sanctum\Sanctum; + +use Workbench\App\User; + use function Pest\Laravel\deleteJson; use function Pest\Laravel\getJson; use function Pest\Laravel\postJson; @@ -65,7 +69,27 @@ $response->assertJsonFragment(['id' => $componentGroup->id]); }); +it('cannot create a component group when not authenticated', function () { + $response = postJson('/status/api/component-groups', [ + 'name' => 'New Group', + ]); + + $response->assertUnauthorized(); +}); + +it('cannot create a component group without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $response = postJson('/status/api/component-groups', [ + 'name' => 'New Group', + ]); + + $response->assertForbidden(); +}); + it('can create a component group without components', function () { + Sanctum::actingAs(User::factory()->create(), ['component-groups.manage']); + $response = postJson('/status/api/component-groups', [ 'name' => 'New Group', ]); @@ -80,6 +104,8 @@ }); it('can create a component group and attach existing components', function () { + Sanctum::actingAs(User::factory()->create(), ['component-groups.manage']); + $components = Component::factory()->create(); $response = postJson('/status/api/component-groups', [ @@ -99,7 +125,31 @@ ]); }); +it('cannot update a component group when not authenticated', function () { + $componentGroup = ComponentGroup::factory()->create(); + + $response = putJson('/status/api/component-groups/'.$componentGroup->id, [ + 'name' => 'Updated Component Group Name', + ]); + + $response->assertUnauthorized(); +}); + +it('cannot update a component group without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $componentGroup = ComponentGroup::factory()->create(); + + $response = putJson('/status/api/component-groups/'.$componentGroup->id, [ + 'name' => 'Updated Component Group Name', + ]); + + $response->assertForbidden(); +}); + it('can update a component group', function () { + Sanctum::actingAs(User::factory()->create(), ['component-groups.manage']); + $componentGroup = ComponentGroup::factory()->create(); $response = putJson('/status/api/component-groups/'.$componentGroup->id, [ @@ -116,6 +166,8 @@ }); it('can update a component group with components', function () { + Sanctum::actingAs(User::factory()->create(), ['component-groups.manage']); + $components = Component::factory()->count(3)->create(); $componentGroup = ComponentGroup::factory()->create(); @@ -136,7 +188,27 @@ ]); }); +it('cannot delete a component group when not authenticated', function () { + $componentGroup = ComponentGroup::factory()->create(); + + $response = deleteJson('/status/api/component-groups/'.$componentGroup->id); + + $response->assertUnauthorized(); +}); + +it('cannot delete a component group without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $componentGroup = ComponentGroup::factory()->create(); + + $response = deleteJson('/status/api/component-groups/'.$componentGroup->id); + + $response->assertForbidden(); +}); + it('can delete a component group', function () { + Sanctum::actingAs(User::factory()->create(), ['component-groups.delete']); + $componentGroup = ComponentGroup::factory()->create(); $response = deleteJson('/status/api/component-groups/'.$componentGroup->id); @@ -148,6 +220,8 @@ }); it('updates components group id when a group is deleted', function () { + Sanctum::actingAs(User::factory()->create(), ['component-groups.delete']); + $componentGroup = ComponentGroup::factory()->hasComponents(2)->create(); $response = deleteJson('/status/api/component-groups/'.$componentGroup->id); diff --git a/tests/Feature/Api/ComponentTest.php b/tests/Feature/Api/ComponentTest.php index 4aaa596c..3fc84ab7 100644 --- a/tests/Feature/Api/ComponentTest.php +++ b/tests/Feature/Api/ComponentTest.php @@ -4,6 +4,9 @@ use Cachet\Models\Component; use Cachet\Models\ComponentGroup; +use Laravel\Sanctum\Sanctum; +use Workbench\App\User; + use function Pest\Laravel\deleteJson; use function Pest\Laravel\getJson; use function Pest\Laravel\postJson; @@ -170,7 +173,31 @@ $response->assertJsonFragment(['id' => $component->id]); }); +it('cannot create a component when not authenticated', function () { + $response = postJson('/status/api/components', [ + 'name' => 'Test', + 'description' => 'This is a new component, created by the API.', + 'order' => 2, + ]); + + $response->assertUnauthorized(); +}); + +it('cannot create a component without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $response = postJson('/status/api/components', [ + 'name' => 'Test', + 'description' => 'This is a new component, created by the API.', + 'order' => 2, + ]); + + $response->assertForbidden(); +}); + it('can create a component', function () { + Sanctum::actingAs(User::factory()->create(), ['components.manage']); + $response = postJson('/status/api/components', [ 'name' => 'Test', 'description' => 'This is a new component, created by the API.', @@ -190,6 +217,8 @@ }); it('can create a component and attach to a component group', function () { + Sanctum::actingAs(User::factory()->create(), ['components.manage']); + $componentGroup = ComponentGroup::factory()->create(); $response = postJson('/status/api/components', [ 'name' => 'Test', @@ -209,6 +238,8 @@ }); it('cannot attach a new component to a component group that does not exist', function () { + Sanctum::actingAs(User::factory()->create(), ['components.manage']); + $response = postJson('/status/api/components', [ 'name' => 'Test', 'description' => 'This is a new component, created by the API.', @@ -223,7 +254,37 @@ ]); }); +it('cannot update a component when not authenticated', function () { + $component = Component::factory()->create([ + 'order' => 10, + ]); + + $response = putJson('/status/api/components/'.$component->id, [ + 'name' => 'Updated Component Name', + 'description' => 'This is an updated component.', + ]); + + $response->assertUnauthorized(); +}); + +it('cannot update a component without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $component = Component::factory()->create([ + 'order' => 10, + ]); + + $response = putJson('/status/api/components/'.$component->id, [ + 'name' => 'Updated Component Name', + 'description' => 'This is an updated component.', + ]); + + $response->assertForbidden(); +}); + it('can update a component', function () { + Sanctum::actingAs(User::factory()->create(), ['components.manage']); + $component = Component::factory()->create([ 'order' => 10, ]); @@ -246,6 +307,8 @@ }); it('can update a component and attach it to a component group', function () { + Sanctum::actingAs(User::factory()->create(), ['components.manage']); + $componentGroup = ComponentGroup::factory()->create(); $component = Component::factory()->create([ 'order' => 10, @@ -271,6 +334,8 @@ }); it('cannot update a component and attach it to a component group that does not exist', function () { + Sanctum::actingAs(User::factory()->create(), ['components.manage']); + $component = Component::factory()->create(); $response = putJson('/status/api/components/'.$component->id, [ @@ -286,7 +351,27 @@ ]); }); +it('cannot delete a component when not authenticated', function () { + $component = Component::factory()->create(); + + $response = deleteJson('/status/api/components/'.$component->id); + + $response->assertUnauthorized(); +}); + +it('cannot delete a component without the token ability', function () { + Sanctum::actingAs(User::factory()->create(), ['components.manage']); + + $component = Component::factory()->create(); + + $response = deleteJson('/status/api/components/'.$component->id); + + $response->assertForbidden(); +}); + it('can delete a component', function () { + Sanctum::actingAs(User::factory()->create(), ['components.delete']); + $component = Component::factory()->create(); $response = deleteJson('/status/api/components/'.$component->id); diff --git a/tests/Feature/Api/IncidentTemplateTest.php b/tests/Feature/Api/IncidentTemplateTest.php index 91796f8d..2375b94a 100644 --- a/tests/Feature/Api/IncidentTemplateTest.php +++ b/tests/Feature/Api/IncidentTemplateTest.php @@ -2,6 +2,10 @@ use Cachet\Models\IncidentTemplate; +use Laravel\Sanctum\Sanctum; + +use Workbench\App\User; + use function Pest\Laravel\deleteJson; use function Pest\Laravel\getJson; use function Pest\Laravel\postJson; @@ -45,7 +49,33 @@ ]); }); +it('cannot create an incident template when not authenticated', function () { + $response = postJson('/status/api/incident-templates', [ + 'name' => 'New Template', + 'slug' => 'new-template', + 'template' => 'Hello {{ name }}', + 'engine' => 'twig', + ]); + + $response->assertUnauthorized(); +}); + +it('cannot create an incident template without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $response = postJson('/status/api/incident-templates', [ + 'name' => 'New Template', + 'slug' => 'new-template', + 'template' => 'Hello {{ name }}', + 'engine' => 'twig', + ]); + + $response->assertForbidden(); +}); + it('can create an incident template', function () { + Sanctum::actingAs(User::factory()->create(), ['incident-templates.manage']); + $response = postJson('/status/api/incident-templates', [ 'name' => 'New Template', 'slug' => 'new-template', @@ -62,7 +92,31 @@ ]); }); +it('cannot update an incident template when not authenticated', function () { + $incidentTemplate = IncidentTemplate::factory()->create(); + + $response = putJson('/status/api/incident-templates/'.$incidentTemplate->id, [ + 'name' => 'Updated Template', + ]); + + $response->assertUnauthorized(); +}); + +it('cannot update an incident template without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $incidentTemplate = IncidentTemplate::factory()->create(); + + $response = putJson('/status/api/incident-templates/'.$incidentTemplate->id, [ + 'name' => 'Updated Template', + ]); + + $response->assertForbidden(); +}); + it('can update an incident template', function () { + Sanctum::actingAs(User::factory()->create(), ['incident-templates.manage']); + $incidentTemplate = IncidentTemplate::factory()->create(); $response = putJson('/status/api/incident-templates/'.$incidentTemplate->id, [ @@ -75,7 +129,27 @@ ]); }); +it('cannot delete an incident template when not authenticated', function () { + $incidentTemplate = IncidentTemplate::factory()->create(); + + $response = deleteJson('/status/api/incident-templates/'.$incidentTemplate->id); + + $response->assertUnauthorized(); +}); + +it('cannot delete an incident template without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $incidentTemplate = IncidentTemplate::factory()->create(); + + $response = deleteJson('/status/api/incident-templates/'.$incidentTemplate->id); + + $response->assertForbidden(); +}); + it('can delete an incident template', function () { + Sanctum::actingAs(User::factory()->create(), ['incident-templates.delete']); + $incidentTemplate = IncidentTemplate::factory()->create(); $response = deleteJson('/status/api/incident-templates/'.$incidentTemplate->id); diff --git a/tests/Feature/Api/IncidentTest.php b/tests/Feature/Api/IncidentTest.php index 93fff734..f5722e35 100644 --- a/tests/Feature/Api/IncidentTest.php +++ b/tests/Feature/Api/IncidentTest.php @@ -4,6 +4,9 @@ use Cachet\Models\Incident; use Cachet\Models\IncidentTemplate; +use Laravel\Sanctum\Sanctum; +use Workbench\App\User; + use function Pest\Laravel\deleteJson; use function Pest\Laravel\getJson; use function Pest\Laravel\postJson; @@ -182,7 +185,31 @@ ]); }); +it('cannot create an incident if not authenticated', function () { + $response = postJson('/status/api/incidents', [ + 'name' => 'New Incident Occurred', + 'message' => 'Something went wrong.', + 'status' => 2, + ]); + + $response->assertUnauthorized(); +}); + +it('cannot create an incident without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $response = postJson('/status/api/incidents', [ + 'name' => 'New Incident Occurred', + 'message' => 'Something went wrong.', + 'status' => 2, + ]); + + $response->assertForbidden(); +}); + it('can create an incident', function () { + Sanctum::actingAs(User::factory()->create(), ['incidents.manage']); + $response = postJson('/status/api/incidents', $payload = [ 'name' => 'New Incident Occurred', 'message' => 'Something went wrong.', @@ -204,6 +231,8 @@ }); it('can create an incident with a template', function () { + Sanctum::actingAs(User::factory()->create(), ['incidents.manage']); + $incidentTemplate = IncidentTemplate::factory()->twig()->create(); $response = postJson('/status/api/incidents', [ @@ -227,6 +256,8 @@ }); it('cannot create an incident with bad data', function (array $payload) { + Sanctum::actingAs(User::factory()->create(), ['incidents.manage']); + $response = postJson('/status/api/incidents', $payload); $response->assertUnprocessable(); @@ -237,7 +268,35 @@ fn () => ['name' => 'New Incident', 'template' => 123, 'status' => 999], ]); +it('cannot update an incident if not authenticated', function () { + $incident = Incident::factory()->create(); + + $response = putJson('/status/api/incidents/'.$incident->id, [ + 'name' => 'New Incident Occurred', + 'message' => 'Something went wrong.', + 'status' => 2, + ]); + + $response->assertUnauthorized(); +}); + +it('cannot update an incident without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $incident = Incident::factory()->create(); + + $response = putJson('/status/api/incidents/'.$incident->id, [ + 'name' => 'New Incident Occurred', + 'message' => 'Something went wrong.', + 'status' => 2, + ]); + + $response->assertForbidden(); +}); + it('can update an incident', function () { + Sanctum::actingAs(User::factory()->create(), ['incidents.manage']); + $incident = Incident::factory()->create(); $response = putJson('/status/api/incidents/'.$incident->id, [ @@ -250,6 +309,8 @@ }); it('can update an incident while passing null data', function (array $payload) { + Sanctum::actingAs(User::factory()->create(), ['incidents.manage']); + $incident = Incident::factory()->create(); $response = putJson('/status/api/incidents/'.$incident->id, $payload); @@ -269,6 +330,8 @@ ]); it('cannot update an incident with bad data', function (array $payload) { + Sanctum::actingAs(User::factory()->create(), ['incidents.manage']); + $incident = Incident::factory()->create(); $response = putJson('/status/api/incidents/'.$incident->id, $payload); @@ -280,7 +343,27 @@ fn () => ['name' => 'New Incident', 'message' => null, 'status' => 999], ]); +it('cannot delete an incident if not authenticated', function () { + $incident = Incident::factory()->create(); + + $response = deleteJson('/status/api/incidents/'.$incident->id); + + $response->assertUnauthorized(); +}); + +it('cannot delete an incident without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $incident = Incident::factory()->create(); + + $response = deleteJson('/status/api/incidents/'.$incident->id); + + $response->assertForbidden(); +}); + it('can delete an incident', function () { + Sanctum::actingAs(User::factory()->create(), ['incidents.delete']); + $incident = Incident::factory()->create(); $response = deleteJson('/status/api/incidents/'.$incident->id); diff --git a/tests/Feature/Api/IncidentUpdateTest.php b/tests/Feature/Api/IncidentUpdateTest.php index 524059a1..287d6a01 100644 --- a/tests/Feature/Api/IncidentUpdateTest.php +++ b/tests/Feature/Api/IncidentUpdateTest.php @@ -5,8 +5,13 @@ use Cachet\Models\Update; use Illuminate\Database\Eloquent\Relations\Relation; +use Laravel\Sanctum\Sanctum; + +use Workbench\App\User; + use function Pest\Laravel\deleteJson; use function Pest\Laravel\getJson; +use function Pest\Laravel\postJson; use function Pest\Laravel\putJson; it('can list incident updates', function () { @@ -133,7 +138,72 @@ ]); }); +it('cannot create an incident update if not authenticated', function () { + $incident = Incident::factory()->create(); + + $response = postJson("/status/api/incidents/{$incident->id}/updates", [ + 'status' => IncidentStatusEnum::identified->value, + 'message' => 'This is a test message.', + ]); + + $response->assertUnauthorized(); +}); + +it('cannot create an incident update without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $incident = Incident::factory()->create(); + + $response = postJson("/status/api/incidents/{$incident->id}/updates", [ + 'status' => IncidentStatusEnum::identified->value, + 'message' => 'This is a test message.', + ]); + + $response->assertForbidden(); +}); + +it('can create an incident update', function () { + Sanctum::actingAs(User::factory()->create(), ['incident-updates.manage']); + + $incident = Incident::factory()->create(); + + $response = postJson("/status/api/incidents/{$incident->id}/updates", [ + 'status' => IncidentStatusEnum::identified->value, + 'message' => 'This is a test message.', + ]); + + $response->assertCreated(); + $this->assertDatabaseHas('updates', [ + 'status' => IncidentStatusEnum::identified->value, + 'message' => 'This is a test message.', + ]); +}); + +it('cannot update an incident update if not authenticated', function () { + $incidentUpdate = Update::factory()->forIncident()->create(); + + $response = putJson("/status/api/incidents/{$incidentUpdate->updateable_id}/updates/{$incidentUpdate->id}", [ + 'message' => 'This is an updated message.', + ]); + + $response->assertUnauthorized(); +}); + +it('cannot update an incident update without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $incidentUpdate = Update::factory()->forIncident()->create(); + + $response = putJson("/status/api/incidents/{$incidentUpdate->updateable_id}/updates/{$incidentUpdate->id}", [ + 'message' => 'This is an updated message.', + ]); + + $response->assertForbidden(); +}); + it('can update an incident update', function () { + Sanctum::actingAs(User::factory()->create(), ['incident-updates.manage']); + $incidentUpdate = Update::factory()->forIncident()->create(); $data = [ @@ -150,7 +220,27 @@ ]); }); +it('cannot delete an incident update if not authenticated', function () { + $incidentUpdate = Update::factory()->forIncident()->create(); + + $response = deleteJson("/status/api/incidents/{$incidentUpdate->updateable_id}/updates/{$incidentUpdate->id}"); + + $response->assertUnauthorized(); +}); + +it('cannot delete an incident update without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $incidentUpdate = Update::factory()->forIncident()->create(); + + $response = deleteJson("/status/api/incidents/{$incidentUpdate->updateable_id}/updates/{$incidentUpdate->id}"); + + $response->assertForbidden(); +}); + it('can delete an incident update', function () { + Sanctum::actingAs(User::factory()->create(), ['incident-updates.delete']); + $incidentUpdate = Update::factory()->forIncident()->create(); $response = deleteJson("/status/api/incidents/{$incidentUpdate->updateable_id}/updates/{$incidentUpdate->id}"); @@ -162,6 +252,8 @@ }); it('cannot delete an incident update from another incident', function () { + Sanctum::actingAs(User::factory()->create(), ['incidents.delete']); + $incidentUpdate = Update::factory()->forUpdateable()->create(); $response = deleteJson("/status/api/incidents/{$incidentUpdate->updateable_id}/updates/{$incidentUpdate->id}"); diff --git a/tests/Feature/Api/MetricPointTest.php b/tests/Feature/Api/MetricPointTest.php index 8755fc2d..4ffe3113 100644 --- a/tests/Feature/Api/MetricPointTest.php +++ b/tests/Feature/Api/MetricPointTest.php @@ -3,6 +3,9 @@ use Cachet\Models\Metric; use Cachet\Models\MetricPoint; +use Laravel\Sanctum\Sanctum; +use Workbench\App\User; + use function Pest\Laravel\deleteJson; use function Pest\Laravel\getJson; use function Pest\Laravel\postJson; @@ -57,7 +60,31 @@ ]); }); +it('cannot create a metric point if not authenticated', function () { + $metric = Metric::factory()->create(); + + $response = postJson('/status/api/metrics/'.$metric->id.'/points', [ + 'value' => 10, + ]); + + $response->assertUnauthorized(); +}); + +it('cannot create a metric point without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $metric = Metric::factory()->create(); + + $response = postJson('/status/api/metrics/'.$metric->id.'/points', [ + 'value' => 10, + ]); + + $response->assertForbidden(); +}); + it('can create a metric point', function () { + Sanctum::actingAs(User::factory()->create(), ['metric-points.manage']); + $metric = Metric::factory()->create(); $response = postJson('/status/api/metrics/'.$metric->id.'/points', [ @@ -72,7 +99,27 @@ ]); }); +it('cannot delete a metric point if not authenticated', function () { + $metricPoint = MetricPoint::factory()->forMetric()->create(); + + $response = deleteJson('/status/api/metrics/'.$metricPoint->metric_id.'/points/'.$metricPoint->id); + + $response->assertUnauthorized(); +}); + +it('cannot delete a metric point without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $metricPoint = MetricPoint::factory()->forMetric()->create(); + + $response = deleteJson('/status/api/metrics/'.$metricPoint->metric_id.'/points/'.$metricPoint->id); + + $response->assertForbidden(); +}); + it('can delete a metric point', function () { + Sanctum::actingAs(User::factory()->create(), ['metric-points.delete']); + $metricPoint = MetricPoint::factory()->forMetric()->create(); $response = deleteJson('/status/api/metrics/'.$metricPoint->metric_id.'/points/'.$metricPoint->id); diff --git a/tests/Feature/Api/MetricTest.php b/tests/Feature/Api/MetricTest.php index 14ed70e7..86290701 100644 --- a/tests/Feature/Api/MetricTest.php +++ b/tests/Feature/Api/MetricTest.php @@ -3,6 +3,10 @@ use Cachet\Enums\MetricTypeEnum; use Cachet\Models\Metric; +use Laravel\Sanctum\Sanctum; + +use Workbench\App\User; + use function Pest\Laravel\deleteJson; use function Pest\Laravel\getJson; use function Pest\Laravel\postJson; @@ -150,7 +154,29 @@ ]); }); +it('cannot create a metric if not authenticated', function () { + $response = postJson('/status/api/metrics', [ + 'name' => 'New Metric', + 'suffix' => 'cups of tea', + ]); + + $response->assertUnauthorized(); +}); + +it('cannot create a metric without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $response = postJson('/status/api/metrics', [ + 'name' => 'New Metric', + 'suffix' => 'cups of tea', + ]); + + $response->assertForbidden(); +}); + it('can create a metric', function () { + Sanctum::actingAs(User::factory()->create(), ['metrics.manage']); + $response = postJson('/status/api/metrics', [ 'name' => 'New Metric', 'suffix' => 'cups of tea', @@ -166,7 +192,33 @@ ]); }); +it('cannot update a metric if not authenticated', function () { + $metric = Metric::factory()->create(); + + $response = putJson('/status/api/metrics/'.$metric->id, [ + 'name' => 'Updated Metric', + 'suffix' => 'cups of tea', + ]); + + $response->assertUnauthorized(); +}); + +it('cannot update a metric without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $metric = Metric::factory()->create(); + + $response = putJson('/status/api/metrics/'.$metric->id, [ + 'name' => 'Updated Metric', + 'suffix' => 'cups of tea', + ]); + + $response->assertForbidden(); +}); + it('can update a metric', function () { + Sanctum::actingAs(User::factory()->create(), ['metrics.manage']); + $metric = Metric::factory()->create(); $response = putJson('/status/api/metrics/'.$metric->id, [ @@ -181,6 +233,8 @@ }); it('cannot update a metric with bad data', function (array $payload) { + Sanctum::actingAs(User::factory()->create(), ['metrics.manage']); + $metric = Metric::factory()->create(); $response = putJson('/status/api/metrics/'.$metric->id, $payload); @@ -192,7 +246,27 @@ fn () => ['name' => 123], ]); +it('cannot delete a metric if not authenticated', function () { + $metric = Metric::factory()->hasMetricPoints(1)->create(); + + $response = deleteJson('/status/api/metrics/'.$metric->id); + + $response->assertUnauthorized(); +}); + +it('cannot delete a metric without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $metric = Metric::factory()->hasMetricPoints(1)->create(); + + $response = deleteJson('/status/api/metrics/'.$metric->id); + + $response->assertForbidden(); +}); + it('can delete metric', function () { + Sanctum::actingAs(User::factory()->create(), ['metrics.delete']); + $metric = Metric::factory()->hasMetricPoints(1)->create(); $response = deleteJson('/status/api/metrics/'.$metric->id); diff --git a/tests/Feature/Api/ScheduleTest.php b/tests/Feature/Api/ScheduleTest.php index 73dcb466..1de3655a 100644 --- a/tests/Feature/Api/ScheduleTest.php +++ b/tests/Feature/Api/ScheduleTest.php @@ -3,6 +3,9 @@ use Cachet\Models\Component; use Cachet\Models\Schedule; +use Laravel\Sanctum\Sanctum; +use Workbench\App\User; + use function Pest\Laravel\deleteJson; use function Pest\Laravel\getJson; use function Pest\Laravel\postJson; @@ -126,7 +129,33 @@ ]); }); +it('cannot create a schedule if not authenticated', function () { + $response = postJson('/status/api/schedules', [ + 'name' => 'New Scheduled Maintenance', + 'message' => 'Something will go wrong.', + 'scheduled_at' => now()->addWeek()->toDateTimeString(), + 'completed_at' => now()->addWeek()->addDay()->toDateTimeString(), + ]); + + $response->assertUnauthorized(); +}); + +it('cannot create a schedule without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $response = postJson('/status/api/schedules', [ + 'name' => 'New Scheduled Maintenance', + 'message' => 'Something will go wrong.', + 'scheduled_at' => now()->addWeek()->toDateTimeString(), + 'completed_at' => now()->addWeek()->addDay()->toDateTimeString(), + ]); + + $response->assertForbidden(); +}); + it('can create a schedule', function () { + Sanctum::actingAs(User::factory()->create(), ['schedules.manage']); + $response = postJson('/status/api/schedules', [ 'name' => 'New Scheduled Maintenance', 'message' => 'Something will go wrong.', @@ -152,6 +181,8 @@ }); it('can create a schedule with components', function () { + Sanctum::actingAs(User::factory()->create(), ['schedules.manage']); + [$componentA, $componentB] = Component::factory(2)->create(); $response = postJson('/status/api/schedules', [ @@ -183,6 +214,8 @@ }); it('cannot create a schedule with bad data', function (array $payload) { + Sanctum::actingAs(User::factory()->create(), ['schedules.manage']); + $response = postJson('/status/api/schedules', $payload); $response->assertUnprocessable(); @@ -193,7 +226,33 @@ fn () => ['name' => 'Invalid Scheduled At', 'message' => 'Something', 'scheduled_at' => 'invalid'], ]); +it('cannot update a schedule if not authenticated', function () { + $schedule = Schedule::factory()->create(); + + $response = putJson('/status/api/schedules/'.$schedule->id, [ + 'name' => 'Updated Scheduled Maintenance', + 'message' => 'Something went wrong.', + ]); + + $response->assertUnauthorized(); +}); + +it('cannot update a schedule without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $schedule = Schedule::factory()->create(); + + $response = putJson('/status/api/schedules/'.$schedule->id, [ + 'name' => 'Updated Scheduled Maintenance', + 'message' => 'Something went wrong.', + ]); + + $response->assertForbidden(); +}); + it('can update a schedule', function () { + Sanctum::actingAs(User::factory()->create(), ['schedules.manage']); + $schedule = Schedule::factory()->create(); $response = putJson('/status/api/schedules/'.$schedule->id, [ @@ -212,6 +271,8 @@ }); it('can update a schedule with components', function () { + Sanctum::actingAs(User::factory()->create(), ['schedules.manage']); + [$componentA, $componentB] = Component::factory(2)->create(); $schedule = Schedule::factory()->create(); @@ -235,7 +296,27 @@ ]); }); +it('cannot delete a schedule if not authenticated', function () { + $schedule = Schedule::factory()->create(); + + $response = deleteJson('/status/api/schedules/'.$schedule->id); + + $response->assertUnauthorized(); +}); + +it('cannot delete a schedule without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $schedule = Schedule::factory()->create(); + + $response = deleteJson('/status/api/schedules/'.$schedule->id); + + $response->assertForbidden(); +}); + it('can delete a schedule', function () { + Sanctum::actingAs(User::factory()->create(), ['schedules.delete']); + $schedule = Schedule::factory()->create(); $response = deleteJson('/status/api/schedules/'.$schedule->id); diff --git a/tests/Feature/Api/ScheduleUpdateTest.php b/tests/Feature/Api/ScheduleUpdateTest.php index 22eb6ca2..e3f0ca55 100644 --- a/tests/Feature/Api/ScheduleUpdateTest.php +++ b/tests/Feature/Api/ScheduleUpdateTest.php @@ -4,8 +4,12 @@ use Cachet\Models\Update; use Illuminate\Database\Eloquent\Relations\Relation; +use Laravel\Sanctum\Sanctum; +use Workbench\App\User; + use function Pest\Laravel\deleteJson; use function Pest\Laravel\getJson; +use function Pest\Laravel\postJson; use function Pest\Laravel\putJson; it('can list schedule updates', function () { @@ -89,7 +93,72 @@ ]); }); +it('cannot create a schedule if not authenticated', function () { + $schedule = Schedule::factory()->create(); + + $response = postJson("/status/api/schedules/{$schedule->id}/updates", [ + 'message' => 'This is a message.', + ]); + + $response->assertUnauthorized(); +}); + +it('cannot create a schedule without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $schedule = Schedule::factory()->create(); + + $response = postJson("/status/api/schedules/{$schedule->id}/updates", [ + 'message' => 'This is a message.', + ]); + + $response->assertForbidden(); +}); + +it('can create a schedule update', function () { + Sanctum::actingAs(User::factory()->create(), ['schedule-updates.manage']); + + $schedule = Schedule::factory()->create(); + + $response = postJson("/status/api/schedules/{$schedule->id}/updates", [ + 'message' => 'This is a message.', + ]); + + $response->assertCreated(); + $this->assertDatabaseHas('updates', [ + 'message' => 'This is a message.', + ]); +}); + +it('cannot update a schedule if not authenticated', function () { + $scheduleUpdate = Update::factory()->forSchedule()->create(); + + $data = [ + 'message' => 'This is an updated message.', + ]; + + $response = putJson("/status/api/schedules/{$scheduleUpdate->updateable_id}/updates/{$scheduleUpdate->id}", $data); + + $response->assertUnauthorized(); +}); + +it('cannot update a schedule without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $scheduleUpdate = Update::factory()->forSchedule()->create(); + + $data = [ + 'message' => 'This is an updated message.', + ]; + + $response = putJson("/status/api/schedules/{$scheduleUpdate->updateable_id}/updates/{$scheduleUpdate->id}", $data); + + $response->assertForbidden(); +}); + it('can update an schedule update', function () { + Sanctum::actingAs(User::factory()->create(), ['schedule-updates.manage']); + $scheduleUpdate = Update::factory()->forSchedule()->create(); $data = [ @@ -106,7 +175,27 @@ ]); }); +it('cannot delete a schedule if not authenticated', function () { + $scheduleUpdate = Update::factory()->forSchedule()->create(); + + $response = deleteJson("/status/api/schedules/{$scheduleUpdate->updateable_id}/updates/{$scheduleUpdate->id}"); + + $response->assertUnauthorized(); +}); + +it('cannot delete a schedule without the token ability', function () { + Sanctum::actingAs(User::factory()->create()); + + $scheduleUpdate = Update::factory()->forSchedule()->create(); + + $response = deleteJson("/status/api/schedules/{$scheduleUpdate->updateable_id}/updates/{$scheduleUpdate->id}"); + + $response->assertForbidden(); +}); + it('can delete an schedule update', function () { + Sanctum::actingAs(User::factory()->create(), ['schedule-updates.delete']); + $scheduleUpdate = Update::factory()->forSchedule()->create(); $response = deleteJson("/status/api/schedules/{$scheduleUpdate->updateable_id}/updates/{$scheduleUpdate->id}"); @@ -118,6 +207,8 @@ }); it('cannot delete an schedule update from another schedule', function () { + Sanctum::actingAs(User::factory()->create(), ['schedule-updates.delete']); + $schedule = Schedule::factory()->create(); $scheduleUpdate = Update::factory()->forSchedule()->create(); diff --git a/workbench/app/User.php b/workbench/app/User.php index 1fec7511..ff4f4fc9 100644 --- a/workbench/app/User.php +++ b/workbench/app/User.php @@ -2,23 +2,18 @@ namespace Workbench\App; -use Illuminate\Foundation\Auth\User as Authenticatable; -use Illuminate\Notifications\Notifiable; +use Cachet\Models\User as CachetUser; +use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Database\Eloquent\Factories\HasFactory; +use Workbench\Database\Factories\UserFactory; -class User extends Authenticatable +class User extends CachetUser { - use Notifiable; + /** @use HasFactory */ + use HasFactory; - /** - * The attributes that should be hidden for arrays. - * - * @var array - */ - protected $hidden = [ - 'password', 'remember_token', - ]; - - protected $fillable = [ - 'name', 'email', 'password', - ]; + protected static function newFactory(): Factory + { + return UserFactory::new(); + } } diff --git a/workbench/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php b/workbench/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php new file mode 100644 index 00000000..e828ad81 --- /dev/null +++ b/workbench/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->morphs('tokenable'); + $table->string('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +};