Skip to content

Commit 9682337

Browse files
User Management (#153)
Co-authored-by: Joel Butcher <[email protected]>
1 parent 24ca011 commit 9682337

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1626
-41
lines changed

composer.json

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"illuminate/events": "^11.23.0",
3030
"illuminate/queue": "^11.23.0",
3131
"illuminate/support": "^11.23.0",
32+
"laravel/sanctum": "^4.0",
3233
"nesbot/carbon": "^2.70",
3334
"spatie/laravel-data": "^4.11",
3435
"spatie/laravel-query-builder": "^5.5",

config/cachet.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
| This is the model that will be used to authenticate users. This model
3434
| must be an instance of Illuminate\Foundation\Auth\User.
3535
*/
36-
'user_model' => \App\Models\User::class,
36+
'user_model' => env('CACHET_USER_MODEL', \App\Models\User::class),
37+
38+
'user_migrations' => env('CACHET_USER_MIGRATIONS', true),
3739

3840
/*
3941
|--------------------------------------------------------------------------

database/factories/UserFactory.php

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace Cachet\Database\Factories;
4+
5+
use Illuminate\Database\Eloquent\Factories\Factory;
6+
use Illuminate\Support\Facades\Hash;
7+
use Illuminate\Support\Str;
8+
9+
/**
10+
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\Cachet\Models\User>
11+
*/
12+
class UserFactory extends Factory
13+
{
14+
/**
15+
* The current password being used by the factory.
16+
*/
17+
protected static ?string $password;
18+
19+
/**
20+
* Define the model's default state.
21+
*
22+
* @return array<string, mixed>
23+
*/
24+
public function definition(): array
25+
{
26+
return [
27+
'name' => fake()->name(),
28+
'email' => fake()->unique()->safeEmail(),
29+
'email_verified_at' => now(),
30+
'password' => static::$password ??= Hash::make('password'),
31+
'remember_token' => Str::random(10),
32+
];
33+
}
34+
35+
/**
36+
* Indicate that the model's email address should be unverified.
37+
*/
38+
public function unverified(): static
39+
{
40+
return $this->state(fn (array $attributes) => [
41+
'email_verified_at' => null,
42+
]);
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*/
12+
public function up(): void
13+
{
14+
Schema::create('api_keys', function (Blueprint $table) {
15+
$table->id();
16+
$table->string('name')->unique();
17+
$table->string('token_', 64)->unique();
18+
$table->foreignId('user_id')->constrained()->onDelete('cascade');
19+
$table->timestamp('last_used_at')->nullable();
20+
$table->timestamp('expires_at')->nullable();
21+
$table->boolean('revoked_at')->nullable();
22+
$table->timestamps();
23+
});
24+
}
25+
26+
/**
27+
* Reverse the migrations.
28+
*/
29+
public function down(): void
30+
{
31+
Schema::dropIfExists('api_keys');
32+
}
33+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*/
12+
public function up(): void
13+
{
14+
Schema::table('users', function (Blueprint $table) {
15+
$table->boolean('is_admin')->default(false);
16+
});
17+
}
18+
19+
/**
20+
* Reverse the migrations.
21+
*/
22+
public function down(): void
23+
{
24+
Schema::table('users', function (Blueprint $table) {
25+
$table->dropColumn('is_admin');
26+
});
27+
}
28+
};

database/seeders/DatabaseSeeder.php

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public function run(): void
5050
'email' => '[email protected]',
5151
'password' => bcrypt('test123'),
5252
'email_verified_at' => now(),
53+
'is_admin' => true,
5354
]);
5455

5556
Schedule::create([

phpstan-baseline.neon

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
parameters:
2+
ignoreErrors:
3+
-
4+
message: '#^Access to an undefined property Laravel\\Sanctum\\PersonalAccessToken\:\:\$expires_at\.$#'
5+
identifier: property.notFound
6+
count: 3
7+
path: src/Filament/Resources/ApiKeyResource.php

phpstan.neon.dist

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
includes:
22
- vendor/larastan/larastan/extension.neon
3+
- phpstan-baseline.neon
34

45
parameters:
56
level: 5

resources/lang/en/api_key.php

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
return [
4+
'resource_label' => 'API Key|API Keys',
5+
'show_token' => [
6+
'heading' => 'Your API Token has been generated',
7+
'description' => 'Please copy your new API token. For your security, it won\'t be shown again.',
8+
'copy_tooltip' => 'Token copied!',
9+
],
10+
'abilities_label' => ':ability :resource',
11+
'form' => [
12+
'name_label' => 'Token Name',
13+
'expires_at_label' => 'Expires At',
14+
'expires_at_helper' => 'Expires at midnight. Leave empty for no expiry',
15+
'expires_at_validation' => 'The expiry date must be in the future',
16+
'abilities_label' => 'Permissions',
17+
'abilities_hint' => 'Leaving this empty will give the token full permissions',
18+
],
19+
'list' => [
20+
'actions' => [
21+
'revoke' => 'Revoke',
22+
],
23+
'headers' => [
24+
'name' => 'Token Name',
25+
'abilities' => 'Permissions',
26+
'created_at' => 'Created At',
27+
'expires_at' => 'Expires At',
28+
'updated_at' => 'Updated At',
29+
],
30+
],
31+
];

resources/lang/en/navigation.php

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
'manage_cachet' => 'Manage Cachet',
88
'manage_customization' => 'Manage Customization',
99
'manage_theme' => 'Manage Theme',
10+
'manage_api_keys' => 'Manage API Keys',
1011
'manage_webhooks' => 'Manage Webhooks',
1112
],
1213
],

resources/lang/en/user.php

+19
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,23 @@
55
'admin' => 'Admin',
66
'user' => 'User',
77
],
8+
'resource_label' => 'User|Users',
9+
'list' => [
10+
'headers' => [
11+
'name' => 'Name',
12+
'email' => 'Email Address',
13+
'email_verified_at' => 'Email Verified At',
14+
'is_admin' => 'Is Admin?',
15+
],
16+
'actions' => [
17+
'verify_email' => 'Verify Email',
18+
],
19+
],
20+
'form' => [
21+
'name_label' => 'Name',
22+
'email_label' => 'Email Address',
23+
'password_label' => 'Password',
24+
'password_confirmation_label' => 'Confirm Password',
25+
'is_admin_label' => 'Admin',
26+
],
827
];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<x-filament::page>
2+
@if($token = session('api-token'))
3+
<x-filament::section :heading="__('cachet::api_key.show_token.heading')">
4+
<p class="text-sm leading-6 text-gray-500 dark:text-gray-400">
5+
{{ __('cachet::api_key.show_token.description') }}
6+
</p>
7+
8+
<div class="flex items-center gap-3 mt-3">
9+
<div
10+
style="--c-50:var(--success-50);--c-400:var(--success-400);--c-600:var(--success-600);"
11+
class="fi-badge cursor-pointer inline-block rounded-md px-3 py-2 text-sm ring-1 ring-inset min-w-[theme(spacing.6)] fi-color-custom bg-custom-50 text-custom-600 ring-custom-600/10 dark:bg-custom-400/10 dark:text-custom-400 dark:ring-custom-400/30 fi-color-success"
12+
x-on:click="
13+
window.navigator.clipboard.writeText(@js($token))
14+
$tooltip(@js(__('cachet::api_key.show_token.copy_tooltip')), {
15+
theme: $store.theme,
16+
timeout: 2000,
17+
})
18+
"
19+
>
20+
{{ $token }}
21+
</div>
22+
</div>
23+
</x-filament::section>
24+
@endsession
25+
26+
{{ $this->table }}
27+
</x-filament::page>

routes/api.php

+38-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php
22

3+
use Cachet\Enums\ApiAbility;
34
use Cachet\Http\Controllers\Api\ComponentController;
45
use Cachet\Http\Controllers\Api\ComponentGroupController;
56
use Cachet\Http\Controllers\Api\GeneralController;
@@ -20,18 +21,51 @@
2021
'incident-templates' => IncidentTemplateController::class,
2122
'metrics' => MetricController::class,
2223
'schedules' => ScheduleController::class,
23-
]);
24+
], ['except' => ['store', 'update', 'destroy']]);
2425

25-
Route::apiResource('incidents.updates', IncidentUpdateController::class)
26+
Route::apiResource('incidents.updates', IncidentUpdateController::class, [
27+
'except' => ['store', 'update', 'destroy'],
28+
])
2629
->scoped(['updateable_id']);
2730

28-
Route::apiResource('schedules.updates', ScheduleUpdateController::class)
31+
Route::apiResource('schedules.updates', ScheduleUpdateController::class, [
32+
'except' => ['store', 'update', 'destroy'],
33+
])
2934
->scoped(['updateable_id']);
3035

31-
Route::apiResource('metrics.points', MetricPointController::class)
36+
Route::apiResource('metrics.points', MetricPointController::class, [
37+
'except' => ['store', 'update', 'destroy'],
38+
])
3239
->parameter('points', 'metricPoint')
3340
->scoped();
3441

42+
Route::middleware(['auth:sanctum'])->group(function () {
43+
Route::apiResources([
44+
'components' => ComponentController::class,
45+
'component-groups' => ComponentGroupController::class,
46+
'incidents' => IncidentController::class,
47+
'incident-templates' => IncidentTemplateController::class,
48+
'metrics' => MetricController::class,
49+
'schedules' => ScheduleController::class,
50+
], ['except' => ['index', 'show']]);
51+
52+
Route::apiResource('incidents.updates', IncidentUpdateController::class, [
53+
'except' => ['index', 'show'],
54+
])
55+
->scoped(['updateable_id']);
56+
57+
Route::apiResource('schedules.updates', ScheduleUpdateController::class, [
58+
'except' => ['index', 'show'],
59+
])
60+
->scoped(['updateable_id']);
61+
62+
Route::apiResource('metrics.points', MetricPointController::class, [
63+
'except' => ['index', 'show'],
64+
])
65+
->parameter('points', 'metricPoint')
66+
->scoped();
67+
});
68+
3569
Route::get('/ping', [GeneralController::class, 'ping'])->name('ping');
3670
Route::get('/version', [GeneralController::class, 'version'])->name('version');
3771
Route::get('/status', StatusController::class)->name('status');

src/Cachet.php

+16
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,20 @@ public static function version(): string
6767
{
6868
return trim(file_get_contents(__DIR__.'/../VERSION'));
6969
}
70+
71+
/** @return array<string, list<string>> */
72+
public static function getResourceApiAbilities(): array
73+
{
74+
return [
75+
'components' => ['manage', 'delete'],
76+
'component-groups' => ['manage', 'delete'],
77+
'incidents' => ['manage', 'delete'],
78+
'incident-updates' => ['manage', 'delete'],
79+
'incident-templates' => ['manage', 'delete'],
80+
'metrics' => ['manage', 'delete'],
81+
'metric-points' => ['manage', 'delete'],
82+
'schedules' => ['manage', 'delete'],
83+
'schedule-updates' => ['manage', 'delete'],
84+
];
85+
}
7086
}

0 commit comments

Comments
 (0)