diff --git a/.junie/skills/fluxui-development/SKILL.md b/.junie/skills/fluxui-development/SKILL.md old mode 100755 new mode 100644 diff --git a/.junie/skills/fortify-development/SKILL.md b/.junie/skills/fortify-development/SKILL.md old mode 100755 new mode 100644 diff --git a/.junie/skills/infer-conventions/SKILL.md b/.junie/skills/infer-conventions/SKILL.md old mode 100755 new mode 100644 diff --git a/.junie/skills/infer-conventions/references/checklist.md b/.junie/skills/infer-conventions/references/checklist.md old mode 100755 new mode 100644 diff --git a/.junie/skills/laravel-best-practices/SKILL.md b/.junie/skills/laravel-best-practices/SKILL.md old mode 100755 new mode 100644 diff --git a/.junie/skills/laravel-best-practices/rules/advanced-queries.md b/.junie/skills/laravel-best-practices/rules/advanced-queries.md old mode 100755 new mode 100644 diff --git a/.junie/skills/laravel-best-practices/rules/architecture.md b/.junie/skills/laravel-best-practices/rules/architecture.md old mode 100755 new mode 100644 diff --git a/.junie/skills/laravel-best-practices/rules/blade-views.md b/.junie/skills/laravel-best-practices/rules/blade-views.md old mode 100755 new mode 100644 diff --git a/.junie/skills/laravel-best-practices/rules/caching.md b/.junie/skills/laravel-best-practices/rules/caching.md old mode 100755 new mode 100644 diff --git a/.junie/skills/laravel-best-practices/rules/collections.md b/.junie/skills/laravel-best-practices/rules/collections.md old mode 100755 new mode 100644 diff --git a/.junie/skills/laravel-best-practices/rules/config.md b/.junie/skills/laravel-best-practices/rules/config.md old mode 100755 new mode 100644 diff --git a/.junie/skills/laravel-best-practices/rules/db-performance.md b/.junie/skills/laravel-best-practices/rules/db-performance.md old mode 100755 new mode 100644 diff --git a/.junie/skills/laravel-best-practices/rules/eloquent.md b/.junie/skills/laravel-best-practices/rules/eloquent.md old mode 100755 new mode 100644 diff --git a/.junie/skills/laravel-best-practices/rules/error-handling.md b/.junie/skills/laravel-best-practices/rules/error-handling.md old mode 100755 new mode 100644 diff --git a/.junie/skills/laravel-best-practices/rules/events-notifications.md b/.junie/skills/laravel-best-practices/rules/events-notifications.md old mode 100755 new mode 100644 diff --git a/.junie/skills/laravel-best-practices/rules/http-client.md b/.junie/skills/laravel-best-practices/rules/http-client.md old mode 100755 new mode 100644 diff --git a/.junie/skills/laravel-best-practices/rules/mail.md b/.junie/skills/laravel-best-practices/rules/mail.md old mode 100755 new mode 100644 diff --git a/.junie/skills/laravel-best-practices/rules/migrations.md b/.junie/skills/laravel-best-practices/rules/migrations.md old mode 100755 new mode 100644 diff --git a/.junie/skills/laravel-best-practices/rules/queue-jobs.md b/.junie/skills/laravel-best-practices/rules/queue-jobs.md old mode 100755 new mode 100644 diff --git a/.junie/skills/laravel-best-practices/rules/routing.md b/.junie/skills/laravel-best-practices/rules/routing.md old mode 100755 new mode 100644 diff --git a/.junie/skills/laravel-best-practices/rules/scheduling.md b/.junie/skills/laravel-best-practices/rules/scheduling.md old mode 100755 new mode 100644 diff --git a/.junie/skills/laravel-best-practices/rules/security.md b/.junie/skills/laravel-best-practices/rules/security.md old mode 100755 new mode 100644 diff --git a/.junie/skills/laravel-best-practices/rules/style.md b/.junie/skills/laravel-best-practices/rules/style.md old mode 100755 new mode 100644 diff --git a/.junie/skills/laravel-best-practices/rules/testing.md b/.junie/skills/laravel-best-practices/rules/testing.md old mode 100755 new mode 100644 diff --git a/.junie/skills/laravel-best-practices/rules/validation.md b/.junie/skills/laravel-best-practices/rules/validation.md old mode 100755 new mode 100644 diff --git a/.junie/skills/livewire-development/SKILL.md b/.junie/skills/livewire-development/SKILL.md old mode 100755 new mode 100644 diff --git a/.junie/skills/livewire-development/reference/javascript-hooks.md b/.junie/skills/livewire-development/reference/javascript-hooks.md old mode 100755 new mode 100644 diff --git a/.junie/skills/pest-testing/SKILL.md b/.junie/skills/pest-testing/SKILL.md old mode 100755 new mode 100644 diff --git a/.junie/skills/tailwindcss-development/SKILL.md b/.junie/skills/tailwindcss-development/SKILL.md old mode 100755 new mode 100644 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2a4f7e4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,168 @@ + +=== foundation rules === + +# Laravel Boost Guidelines + +The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications. + +## Foundational Context + +This application is a Laravel application running on PHP 8.3. You are an expert with the Laravel ecosystem. Always use the APIs that match the installed major version of each package — do not assume a version. + +Before relying on a package's API, confirm its installed version: +- PHP packages: run `composer show --direct` to list direct dependencies with versions, or `composer show ` for a single package. +- JS packages: check `package.json` for the installed versions. + +## Skills Activation + +This project has domain-specific skills available in `**/skills/**`. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck. + +## Conventions + +- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming. +- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. +- Check for existing components to reuse before writing a new one. + +## Verification Scripts + +- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important. + +## Application Structure & Architecture + +- Stick to existing directory structure; don't create new base folders without approval. +- Do not change the application's dependencies without approval. + +## Frontend Bundling + +- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. + +## Documentation Files + +- You must only create documentation files if explicitly requested by the user. + +## Replies + +- Be concise in your explanations - focus on what's important rather than explaining obvious details. + +=== boost rules === + +# Laravel Boost + +## Tools + +- Laravel Boost is an MCP server with tools designed specifically for this application. Prefer Boost tools over manual alternatives like shell commands or file reads. +- Use `database-query` to run read-only queries against the database instead of writing raw SQL in tinker. +- Use `database-schema` to inspect table structure before writing migrations or models. +- Use `get-absolute-url` to resolve the correct scheme, domain, and port for project URLs. Always use this before sharing a URL with the user. +- Use `browser-logs` to read browser logs, errors, and exceptions. Only recent logs are useful, ignore old entries. + +## Searching Documentation (IMPORTANT) + +- Always use `search-docs` before making code changes. Do not skip this step. It returns version-specific docs based on installed packages automatically. +- Pass a `packages` array to scope results when you know which packages are relevant. +- Use multiple broad, topic-based queries: `['rate limiting', 'routing rate limiting', 'routing']`. Expect the most relevant results first. +- Do not add package names to queries because package info is already shared. Use `test resource table`, not `filament 4 test resource table`. + +### Search Syntax + +1. Use words for auto-stemmed AND logic: `rate limit` matches both "rate" AND "limit". +2. Use `"quoted phrases"` for exact position matching: `"infinite scroll"` requires adjacent words in order. +3. Combine words and phrases for mixed queries: `middleware "rate limit"`. +4. Use multiple queries for OR logic: `queries=["authentication", "middleware"]`. + +## Project Rules + +- This project contains committed, area-grouped rules in `.ai/rules` when that directory exists (settled decisions, non-obvious traps, standing constraints). Framework and package guidelines that only apply to specific paths (testing, frontend, components) also live there, under `.ai/rules/boost` — this is not just recorded decisions, it is load-bearing guidance you have not seen inline. Before you enter plan mode or create/edit any file, you MUST first: open @.ai/rules/index.md (it maps file globs to rule files), read every rule file whose globs cover the path(s) in scope, and run `grep -rin 'keyword' .ai/rules` to catch what a path match alone misses. Do not write code until you have read and are following every matching rule. If `.ai/rules` does not exist, continue without it. +- Record durable rules with `record-rule` so the next agent or teammate inherits them instead of working them out again. Pass a `glob` (e.g. `app/Http/Controllers/**`), a short `title`, and a few-line `note`. Always use `record-rule`, never your native memory or notes tool — native memory is personal and session-scoped; only `.ai/rules` is shared with the team and persists in the repo. + +## Artisan + +- Run Artisan commands directly via the command line (e.g., `php artisan route:list`). Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters. +- Inspect routes with `php artisan route:list`. Filter with: `--method=GET`, `--name=users`, `--path=api`, `--except-vendor`, `--only-vendor`. +- Read configuration values using dot notation: `php artisan config:show app.name`, `php artisan config:show database.default`. Or read config files directly from the `config/` directory. + +## Tinker + +- Execute PHP in app context for debugging and testing code. Do not create models without user approval, prefer tests with factories instead. Prefer existing Artisan commands over custom tinker code. +- Always use single quotes to prevent shell expansion: `php artisan tinker --execute 'Your::code();'` + - Double quotes for PHP strings inside: `php artisan tinker --execute 'User::where("active", true)->count();'` + +=== php rules === + +# PHP + +- Always use curly braces for control structures, even for single-line bodies. +- Use PHP 8 constructor property promotion: `public function __construct(public GitHub $github) { }`. Do not leave empty zero-parameter `__construct()` methods unless the constructor is private. +- Use explicit return type declarations and type hints for all method parameters: `function isAccessible(User $user, ?string $path = null): bool` +- Use TitleCase for Enum keys: `FavoritePerson`, `BestLake`, `Monthly`. +- Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic. +- Use array shape type definitions in PHPDoc blocks. + +=== deployments rules === + +# Deployment + +- Laravel can be deployed using [Laravel Cloud](https://cloud.laravel.com/), which is the fastest way to deploy and scale production Laravel applications. + +=== tests rules === + +# Test Enforcement + +- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. +- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter. + +=== laravel/core rules === + +# Do Things the Laravel Way + +- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`. +- If you're creating a generic PHP class, use `php artisan make:class`. +- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. + +### Model Creation + +- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options. + +## APIs & Eloquent Resources + +- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. + +## URL Generation + +- When generating links to other pages, prefer named routes and the `route()` function. + +## Testing + +- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. +- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. +- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. + +## Vite Error + +- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. + +=== livewire/core rules === + +# Livewire + +- Livewire allow to build dynamic, reactive interfaces in PHP without writing JavaScript. +- You can use Alpine.js for client-side interactions instead of JavaScript frameworks. +- Keep state server-side so the UI reflects it. Validate and authorize in actions as you would in HTTP requests. + +=== pint/core rules === + +# Laravel Pint Code Formatter + +- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style. +- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues. + +=== pest/core rules === + +## Pest + +- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`. +- The `{name}` argument should not include the test suite directory. Use `php artisan make:test --pest SomeFeatureTest` instead of `php artisan make:test --pest Feature/SomeFeatureTest`. +- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`. +- Do NOT delete tests without approval. + + diff --git a/Modules/Helpdesk/Http/Controllers/NativeAuthController.php b/Modules/Helpdesk/Http/Controllers/NativeAuthController.php new file mode 100644 index 0000000..d6e5e04 --- /dev/null +++ b/Modules/Helpdesk/Http/Controllers/NativeAuthController.php @@ -0,0 +1,91 @@ +validate([ + 'email' => ['required', 'email'], + 'password' => ['required', 'string'], + 'device_name' => ['sometimes', 'string', 'max:100'], + ]); + + $user = User::query() + ->where('email', $data['email']) + ->first(); + + if (! $user || ! Hash::check($data['password'], $user->password)) { + throw ValidationException::withMessages([ + 'email' => ['The provided credentials are incorrect.'], + ]); + } + + // Native two-factor and passkey challenges are not implemented yet. + // Never issue a token to an account requiring these challenges. + if ( + ! empty($user->two_factor_secret) + || ! empty($user->two_factor_confirmed_at) + ) { + return response()->json([ + 'message' => 'This account requires additional authentication.', + 'code' => 'additional_authentication_required', + ], 403); + } + + $token = $user->createToken( + $data['device_name'] ?? 'HelpdeskMac', + ['helpdesk:native'], + now()->addDays(30) + ); + + return response()->json([ + 'token_type' => 'Bearer', + 'access_token' => $token->plainTextToken, + 'expires_at' => $token->accessToken->expires_at?->toIso8601String(), + 'user' => $this->userData($user), + ]); + } + + public function user(Request $request): JsonResponse + { + return response()->json([ + 'user' => $this->userData($request->user()), + ]); + } + + public function logout(Request $request): JsonResponse + { + $token = $request->user()->currentAccessToken(); + + if ($token instanceof PersonalAccessToken) { + $token->delete(); + } + + return response()->json([ + 'message' => 'Logged out successfully.', + ]); + } + + private function userData(User $user): array + { + return [ + 'id' => $user->getKey(), + 'name' => $user->name, + 'email' => $user->email, + 'helpdesk_agent' => $user->isHelpdeskAgent(), + 'helpdesk_admin' => $user->isHelpdeskAdmin(), + ]; + } +} diff --git a/Modules/Helpdesk/Http/Controllers/NativeAuthorizationController.php b/Modules/Helpdesk/Http/Controllers/NativeAuthorizationController.php new file mode 100644 index 0000000..759fa2d --- /dev/null +++ b/Modules/Helpdesk/Http/Controllers/NativeAuthorizationController.php @@ -0,0 +1,33 @@ +validate([ + 'client_id' => ['required', 'string'], + 'redirect_uri' => ['required', 'string'], + 'response_type' => ['required', 'in:code'], + 'state' => ['required', 'string', 'min:32'], + 'code_challenge' => ['required', 'string', 'size:43'], + 'code_challenge_method' => ['required', 'in:S256'], + ]); + + // Authorization-code issuance is not implemented yet. + // Never redirect a code or token to an unregistered URI. + abort(501, 'Native authorization is not yet implemented.'); + } + + public function approve(Request $request): void + { + abort(501, 'Native authorization approval is not yet implemented.'); + } +} diff --git a/Modules/Helpdesk/Http/Controllers/NativeTicketController.php b/Modules/Helpdesk/Http/Controllers/NativeTicketController.php index abd949e..2ccc8c2 100644 --- a/Modules/Helpdesk/Http/Controllers/NativeTicketController.php +++ b/Modules/Helpdesk/Http/Controllers/NativeTicketController.php @@ -7,44 +7,144 @@ namespace Modules\Helpdesk\Http\Controllers; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Routing\Controller; -use Illuminate\Support\Facades\DB; +use Illuminate\Validation\Rule; use Modules\Helpdesk\Models\Ticket; +use Modules\Helpdesk\Services\TicketService; final class NativeTicketController extends Controller { + public function __construct( + private readonly TicketService $tickets, + ) {} + public function index(Request $request): JsonResponse { $user = $request->user(); - $query = Ticket::query()->orderByDesc('id'); - if (!$user->can('helpdesk.admin')) { - $query->where(function ($q) use ($user) { $q->where('user_id', $user->getKey())->orWhere('agent_id', $user->getKey()); }); + + $query = Ticket::query() + ->visibleTo($user) + ->orderByDesc('id'); + + if ($request->filled('search')) { + $search = addcslashes( + (string) $request->query('search'), + '%_\\' + ); + + $query->where( + 'subject', + 'like', + '%'.$search.'%' + ); } - if ($search = $request->query('search')) { - $query->where('subject', 'like', '%'.addcslashes((string) $search, '%_\\').'%'); - } - return response()->json($query->paginate(30)); + + return response()->json( + $query->paginate(30) + ); } public function show(Request $request, Ticket $ticket): JsonResponse { - $this->authorizeTicket($request, $ticket); - return response()->json($ticket->load(['comments', 'status', 'priority', 'agent'])); + $user = $request->user(); + + $ticket = Ticket::query() + ->visibleTo($user) + ->whereKey($ticket->getKey()) + ->firstOrFail(); + + return response()->json( + $ticket->load([ + 'comments', + 'status', + 'priority', + 'category', + 'project', + 'team', + 'agent', + 'attachments', + 'tags', + ]) + ); } public function store(Request $request): JsonResponse { - $data = $request->validate(['subject' => ['required','string','max:255'], 'body' => ['required','string']]); - $ticket = DB::transaction(function () use ($request, $data) { - return Ticket::create($data + [ - 'reference' => 'MAC-'.strtoupper(bin2hex(random_bytes(6))), - 'user_id' => $request->user()->getKey(), - ]); - }); - return response()->json($ticket, 201); - } + $data = $request->validate([ + 'subject' => [ + 'required', + 'string', + 'max:255', + ], + 'body' => [ + 'required', + 'string', + ], + 'priority_id' => [ + 'required', + 'integer', + Rule::exists('helpdesk_priorities', 'id'), + ], + 'category_id' => [ + 'nullable', + 'integer', + Rule::exists('helpdesk_categories', 'id'), + ], + 'project_id' => [ + 'nullable', + 'integer', + Rule::exists('helpdesk_projects', 'id'), + ], + 'product_id' => [ + 'nullable', + 'integer', + Rule::exists('helpdesk_products', 'id'), + ], + 'team_id' => [ + 'nullable', + 'integer', + Rule::exists('helpdesk_teams', 'id'), + ], + 'board_id' => [ + 'nullable', + 'integer', + Rule::exists('helpdesk_kanban_boards', 'id'), + ], + 'due_at' => [ + 'nullable', + 'date', + ], + 'tag_ids' => [ + 'sometimes', + 'array', + ], + 'tag_ids.*' => [ + 'integer', + 'distinct', + Rule::exists('helpdesk_tags', 'id'), + ], + 'attachments' => [ + 'sometimes', + 'array', + ], + 'attachments.*' => [ + 'file', + 'max:10240', + ], + ]); - private function authorizeTicket(Request $request, Ticket $ticket): void - { - abort_unless($request->user()->can('helpdesk.admin') || (int) $ticket->user_id === (int) $request->user()->getKey() || (int) $ticket->agent_id === (int) $request->user()->getKey(), 403); + $files = $request->file('attachments', []); + + unset($data['attachments']); + + $ticket = $this->tickets->create( + $request->user(), + $data, + $files + ); + + return response()->json( + $ticket, + 201 + ); } } diff --git a/Modules/Helpdesk/Providers/HelpdeskServiceProvider.php b/Modules/Helpdesk/Providers/HelpdeskServiceProvider.php index db4d37e..15c25c2 100755 --- a/Modules/Helpdesk/Providers/HelpdeskServiceProvider.php +++ b/Modules/Helpdesk/Providers/HelpdeskServiceProvider.php @@ -20,7 +20,6 @@ use Modules\Helpdesk\Livewire\TicketTimer; use Modules\Helpdesk\Models\Ticket; use Modules\Helpdesk\Policies\TicketPolicy; use Modules\Helpdesk\Support\ModuleInfo; - final class HelpdeskServiceProvider extends ServiceProvider { /** diff --git a/Modules/Helpdesk/Providers/RouteServiceProvider.php b/Modules/Helpdesk/Providers/RouteServiceProvider.php index e34613a..2a9ccdc 100755 --- a/Modules/Helpdesk/Providers/RouteServiceProvider.php +++ b/Modules/Helpdesk/Providers/RouteServiceProvider.php @@ -9,6 +9,7 @@ use Illuminate\Support\Facades\Route; use Modules\Helpdesk\Http\Controllers\GitWebhookController; use Modules\Helpdesk\Http\Controllers\WorkflowOpenController; use Modules\Helpdesk\Support\ModuleInfo; +use Modules\Helpdesk\Http\Controllers\NativeAuthorizationController; final class RouteServiceProvider extends ServiceProvider { @@ -42,6 +43,17 @@ final class RouteServiceProvider extends ServiceProvider ->prefix(config('helpdesk.route_prefix', 'helpdesk')) ->name('helpdesk.') ->group(module_path(ModuleInfo::name(), 'Routes/web.php')); + // Native macOS authentication through Laravel Fortify. + Route::middleware(['web', 'auth:web']) + ->prefix('helpdesk/native') + ->name('helpdesk.native.') + ->group(function (): void { + Route::get('/authorize', [NativeAuthorizationController::class, 'authorize']) + ->name('authorize'); + + Route::post('/authorize', [NativeAuthorizationController::class, 'approve']) + ->name('approve'); + }); } /** diff --git a/Modules/Helpdesk/Routes/api.php b/Modules/Helpdesk/Routes/api.php index 4685c80..c08be01 100755 --- a/Modules/Helpdesk/Routes/api.php +++ b/Modules/Helpdesk/Routes/api.php @@ -7,6 +7,7 @@ use Modules\Helpdesk\Http\Controllers\GitWebhookController; use Modules\Helpdesk\Http\Controllers\HelpdeskSyncController; use Modules\Helpdesk\Http\Controllers\NativeTicketController; use Modules\Helpdesk\Http\Middleware\VerifyHelpdeskSyncToken; +use Modules\Helpdesk\Http\Controllers\NativeAuthController; Route::get('/status', static fn (): array => [ 'module' => 'helpdesk', @@ -34,3 +35,26 @@ Route::middleware('auth:sanctum')->prefix('native/v1')->group(function () { Route::get('tickets/{ticket}', [NativeTicketController::class, 'show']); Route::post('tickets', [NativeTicketController::class, 'store']); }); + +Route::prefix('native/v1') + ->name('native.') + ->group(function (): void { + + Route::middleware('auth:sanctum')->group(function (): void { + + Route::get('auth/user', [NativeAuthController::class, 'user']) + ->name('auth.user'); + + Route::post('auth/logout', [NativeAuthController::class, 'logout']) + ->name('auth.logout'); + + Route::get('tickets', [NativeTicketController::class, 'index']) + ->name('tickets.index'); + + Route::get('tickets/{ticket}', [NativeTicketController::class, 'show']) + ->name('tickets.show'); + + Route::post('tickets', [NativeTicketController::class, 'store']) + ->name('tickets.store'); + }); + }); diff --git a/app/Models/User.php b/app/Models/User.php index 320acaf..9ac7122 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -15,6 +15,7 @@ use Laravel\Fortify\Contracts\PasskeyUser; use Laravel\Fortify\PasskeyAuthenticatable; use Laravel\Fortify\TwoFactorAuthenticatable; use Modules\UserManagement\Models\Role; +use Laravel\Sanctum\HasApiTokens; /** * @property int $id @@ -28,9 +29,7 @@ use Modules\UserManagement\Models\Role; #[Hidden(['password', 'two_factor_secret', 'two_factor_recovery_codes', 'remember_token'])] class User extends Authenticatable implements PasskeyUser { - /** @use HasFactory */ - use HasFactory, Notifiable, PasskeyAuthenticatable, TwoFactorAuthenticatable; - + use HasApiTokens, HasFactory, Notifiable, PasskeyAuthenticatable, TwoFactorAuthenticatable; protected function casts(): array { return [ @@ -101,4 +100,5 @@ class User extends Authenticatable implements PasskeyUser { return $this->isHelpdeskAgent(); } + } diff --git a/composer.json b/composer.json index eb7bcc5..e055655 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "laravel/chisel": "^0.1.0", "laravel/fortify": "^1.37.2", "laravel/framework": "^13.17", + "laravel/sanctum": "^4.3", "laravel/tinker": "^3.0", "livewire/blaze": "^1.0", "livewire/flux": "^2.13.1", diff --git a/composer.lock b/composer.lock index 7f4d0c7..99d7a1e 100755 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0baf4a77af5bdd712ea8c8ae0dd072ca", + "content-hash": "3ba979872819ad8b9e06875e8c2231b0", "packages": [ { "name": "bacon/bacon-qr-code", @@ -1680,6 +1680,69 @@ }, "time": "2026-08-04T14:50:50+00:00" }, + { + "name": "laravel/sanctum", + "version": "v4.3.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "fee27a573d1a013af3721d86153a65e0b11927e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/fee27a573d1a013af3721d86153a65e0b11927e6", + "reference": "fee27a573d1a013af3721d86153a65e0b11927e6", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/database": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2", + "symfony/console": "^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sanctum\\SanctumServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sanctum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": [ + "auth", + "laravel", + "sanctum" + ], + "support": { + "issues": "https://github.com/laravel/sanctum/issues", + "source": "https://github.com/laravel/sanctum" + }, + "time": "2026-06-23T18:26:55+00:00" + }, { "name": "laravel/serializable-closure", "version": "v2.0.15", diff --git a/config/sanctum.php b/config/sanctum.php new file mode 100644 index 0000000..cde73cf --- /dev/null +++ b/config/sanctum.php @@ -0,0 +1,87 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + Sanctum::currentApplicationUrlWithPort(), + // Sanctum::currentRequestHost(), + ))), + + /* + |-------------------------------------------------------------------------- + | Sanctum Guards + |-------------------------------------------------------------------------- + | + | This array contains the authentication guards that will be checked when + | Sanctum is trying to authenticate a request. If none of these guards + | are able to authenticate the request, Sanctum will use the bearer + | token that's present on an incoming request for authentication. + | + */ + + 'guard' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. This will override any values set in the token's + | "expires_at" attribute, but first-party sessions are not affected. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Token Prefix + |-------------------------------------------------------------------------- + | + | Sanctum can prefix new tokens in order to take advantage of numerous + | security scanning initiatives maintained by open source platforms + | that notify developers if they commit tokens into repositories. + | + | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning + | + */ + + 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'authenticate_session' => AuthenticateSession::class, + 'encrypt_cookies' => EncryptCookies::class, + 'validate_csrf_token' => ValidateCsrfToken::class, + ], + +]; diff --git a/database/migrations/2026_10_14_150457_create_personal_access_tokens_table.php b/database/migrations/2026_10_14_150457_create_personal_access_tokens_table.php new file mode 100644 index 0000000..40ff706 --- /dev/null +++ b/database/migrations/2026_10_14_150457_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->morphs('tokenable'); + $table->text('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +};