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');
+ }
+};