Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User Management #153

Merged
merged 12 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion config/cachet.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),

/*
|--------------------------------------------------------------------------
Expand Down
44 changes: 44 additions & 0 deletions database/factories/UserFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace Cachet\Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\Cachet\Models\User>
*/
class UserFactory extends Factory
{
/**
* The current password being used by the factory.
*/
protected static ?string $password;

/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
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,
]);
}
}
33 changes: 33 additions & 0 deletions database/migrations/2025_01_11_090556_create_api_keys_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('api_keys', function (Blueprint $table) {
$table->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');
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_admin')->default(false);
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_admin');
});
}
};
1 change: 1 addition & 0 deletions database/seeders/DatabaseSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public function run(): void
'email' => '[email protected]',
'password' => bcrypt('test123'),
'email_verified_at' => now(),
'is_admin' => true,
]);

Schedule::create([
Expand Down
7 changes: 7 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
includes:
- vendor/larastan/larastan/extension.neon
- phpstan-baseline.neon

parameters:
level: 5
Expand Down
31 changes: 31 additions & 0 deletions resources/lang/en/api_key.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

return [
'resource_label' => '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',
],
],
];
1 change: 1 addition & 0 deletions resources/lang/en/navigation.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],
],
Expand Down
19 changes: 19 additions & 0 deletions resources/lang/en/user.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],
];
27 changes: 27 additions & 0 deletions resources/views/filament/pages/api-key/index.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<x-filament::page>
@if($token = session('api-token'))
<x-filament::section :heading="__('cachet::api_key.show_token.heading')">
<p class="text-sm leading-6 text-gray-500 dark:text-gray-400">
{{ __('cachet::api_key.show_token.description') }}
</p>

<div class="flex items-center gap-3 mt-3">
<div
style="--c-50:var(--success-50);--c-400:var(--success-400);--c-600:var(--success-600);"
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"
x-on:click="
window.navigator.clipboard.writeText(@js($token))
$tooltip(@js(__('cachet::api_key.show_token.copy_tooltip')), {
theme: $store.theme,
timeout: 2000,
})
"
>
{{ $token }}
</div>
</div>
</x-filament::section>
@endsession

{{ $this->table }}
</x-filament::page>
42 changes: 38 additions & 4 deletions routes/api.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

use Cachet\Enums\ApiAbility;
use Cachet\Http\Controllers\Api\ComponentController;
use Cachet\Http\Controllers\Api\ComponentGroupController;
use Cachet\Http\Controllers\Api\GeneralController;
Expand All @@ -20,18 +21,51 @@
'incident-templates' => 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');
16 changes: 16 additions & 0 deletions src/Cachet.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,20 @@ public static function version(): string
{
return trim(file_get_contents(__DIR__.'/../VERSION'));
}

/** @return array<string, list<string>> */
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'],
];
}
}
Loading
Loading