added support for legacy tickets, fixed switching of modes and also style updates
tests / ci (push) Canceled after 0s
tests / ci (push) Canceled after 0s
This commit is contained in:
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
@@ -0,0 +1,168 @@
|
|||||||
|
<laravel-boost-guidelines>
|
||||||
|
=== 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 <vendor/package>` 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.
|
||||||
|
|
||||||
|
</laravel-boost-guidelines>
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Modules\Helpdesk\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Routing\Controller;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Laravel\Sanctum\PersonalAccessToken;
|
||||||
|
|
||||||
|
final class NativeAuthController extends Controller
|
||||||
|
{
|
||||||
|
public function login(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$data = $request->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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Modules\Helpdesk\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Routing\Controller;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
final class NativeAuthorizationController extends Controller
|
||||||
|
{
|
||||||
|
public function authorize(Request $request): View
|
||||||
|
{
|
||||||
|
$request->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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,44 +7,144 @@ namespace Modules\Helpdesk\Http\Controllers;
|
|||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Routing\Controller;
|
use Illuminate\Routing\Controller;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Validation\Rule;
|
||||||
use Modules\Helpdesk\Models\Ticket;
|
use Modules\Helpdesk\Models\Ticket;
|
||||||
|
use Modules\Helpdesk\Services\TicketService;
|
||||||
|
|
||||||
final class NativeTicketController extends Controller
|
final class NativeTicketController extends Controller
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly TicketService $tickets,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function index(Request $request): JsonResponse
|
public function index(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$query = Ticket::query()->orderByDesc('id');
|
|
||||||
if (!$user->can('helpdesk.admin')) {
|
$query = Ticket::query()
|
||||||
$query->where(function ($q) use ($user) { $q->where('user_id', $user->getKey())->orWhere('agent_id', $user->getKey()); });
|
->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
|
public function show(Request $request, Ticket $ticket): JsonResponse
|
||||||
{
|
{
|
||||||
$this->authorizeTicket($request, $ticket);
|
$user = $request->user();
|
||||||
return response()->json($ticket->load(['comments', 'status', 'priority', 'agent']));
|
|
||||||
|
$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
|
public function store(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$data = $request->validate(['subject' => ['required','string','max:255'], 'body' => ['required','string']]);
|
$data = $request->validate([
|
||||||
$ticket = DB::transaction(function () use ($request, $data) {
|
'subject' => [
|
||||||
return Ticket::create($data + [
|
'required',
|
||||||
'reference' => 'MAC-'.strtoupper(bin2hex(random_bytes(6))),
|
'string',
|
||||||
'user_id' => $request->user()->getKey(),
|
'max:255',
|
||||||
]);
|
],
|
||||||
});
|
'body' => [
|
||||||
return response()->json($ticket, 201);
|
'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
|
$files = $request->file('attachments', []);
|
||||||
{
|
|
||||||
abort_unless($request->user()->can('helpdesk.admin') || (int) $ticket->user_id === (int) $request->user()->getKey() || (int) $ticket->agent_id === (int) $request->user()->getKey(), 403);
|
unset($data['attachments']);
|
||||||
|
|
||||||
|
$ticket = $this->tickets->create(
|
||||||
|
$request->user(),
|
||||||
|
$data,
|
||||||
|
$files
|
||||||
|
);
|
||||||
|
|
||||||
|
return response()->json(
|
||||||
|
$ticket,
|
||||||
|
201
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ use Modules\Helpdesk\Livewire\TicketTimer;
|
|||||||
use Modules\Helpdesk\Models\Ticket;
|
use Modules\Helpdesk\Models\Ticket;
|
||||||
use Modules\Helpdesk\Policies\TicketPolicy;
|
use Modules\Helpdesk\Policies\TicketPolicy;
|
||||||
use Modules\Helpdesk\Support\ModuleInfo;
|
use Modules\Helpdesk\Support\ModuleInfo;
|
||||||
|
|
||||||
final class HelpdeskServiceProvider extends ServiceProvider
|
final class HelpdeskServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use Illuminate\Support\Facades\Route;
|
|||||||
use Modules\Helpdesk\Http\Controllers\GitWebhookController;
|
use Modules\Helpdesk\Http\Controllers\GitWebhookController;
|
||||||
use Modules\Helpdesk\Http\Controllers\WorkflowOpenController;
|
use Modules\Helpdesk\Http\Controllers\WorkflowOpenController;
|
||||||
use Modules\Helpdesk\Support\ModuleInfo;
|
use Modules\Helpdesk\Support\ModuleInfo;
|
||||||
|
use Modules\Helpdesk\Http\Controllers\NativeAuthorizationController;
|
||||||
|
|
||||||
final class RouteServiceProvider extends ServiceProvider
|
final class RouteServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@@ -42,6 +43,17 @@ final class RouteServiceProvider extends ServiceProvider
|
|||||||
->prefix(config('helpdesk.route_prefix', 'helpdesk'))
|
->prefix(config('helpdesk.route_prefix', 'helpdesk'))
|
||||||
->name('helpdesk.')
|
->name('helpdesk.')
|
||||||
->group(module_path(ModuleInfo::name(), 'Routes/web.php'));
|
->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');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use Modules\Helpdesk\Http\Controllers\GitWebhookController;
|
|||||||
use Modules\Helpdesk\Http\Controllers\HelpdeskSyncController;
|
use Modules\Helpdesk\Http\Controllers\HelpdeskSyncController;
|
||||||
use Modules\Helpdesk\Http\Controllers\NativeTicketController;
|
use Modules\Helpdesk\Http\Controllers\NativeTicketController;
|
||||||
use Modules\Helpdesk\Http\Middleware\VerifyHelpdeskSyncToken;
|
use Modules\Helpdesk\Http\Middleware\VerifyHelpdeskSyncToken;
|
||||||
|
use Modules\Helpdesk\Http\Controllers\NativeAuthController;
|
||||||
|
|
||||||
Route::get('/status', static fn (): array => [
|
Route::get('/status', static fn (): array => [
|
||||||
'module' => 'helpdesk',
|
'module' => 'helpdesk',
|
||||||
@@ -34,3 +35,26 @@ Route::middleware('auth:sanctum')->prefix('native/v1')->group(function () {
|
|||||||
Route::get('tickets/{ticket}', [NativeTicketController::class, 'show']);
|
Route::get('tickets/{ticket}', [NativeTicketController::class, 'show']);
|
||||||
Route::post('tickets', [NativeTicketController::class, 'store']);
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+3
-3
@@ -15,6 +15,7 @@ use Laravel\Fortify\Contracts\PasskeyUser;
|
|||||||
use Laravel\Fortify\PasskeyAuthenticatable;
|
use Laravel\Fortify\PasskeyAuthenticatable;
|
||||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||||
use Modules\UserManagement\Models\Role;
|
use Modules\UserManagement\Models\Role;
|
||||||
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property int $id
|
* @property int $id
|
||||||
@@ -28,9 +29,7 @@ use Modules\UserManagement\Models\Role;
|
|||||||
#[Hidden(['password', 'two_factor_secret', 'two_factor_recovery_codes', 'remember_token'])]
|
#[Hidden(['password', 'two_factor_secret', 'two_factor_recovery_codes', 'remember_token'])]
|
||||||
class User extends Authenticatable implements PasskeyUser
|
class User extends Authenticatable implements PasskeyUser
|
||||||
{
|
{
|
||||||
/** @use HasFactory<UserFactory> */
|
use HasApiTokens, HasFactory, Notifiable, PasskeyAuthenticatable, TwoFactorAuthenticatable;
|
||||||
use HasFactory, Notifiable, PasskeyAuthenticatable, TwoFactorAuthenticatable;
|
|
||||||
|
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@@ -101,4 +100,5 @@ class User extends Authenticatable implements PasskeyUser
|
|||||||
{
|
{
|
||||||
return $this->isHelpdeskAgent();
|
return $this->isHelpdeskAgent();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"laravel/chisel": "^0.1.0",
|
"laravel/chisel": "^0.1.0",
|
||||||
"laravel/fortify": "^1.37.2",
|
"laravel/fortify": "^1.37.2",
|
||||||
"laravel/framework": "^13.17",
|
"laravel/framework": "^13.17",
|
||||||
|
"laravel/sanctum": "^4.3",
|
||||||
"laravel/tinker": "^3.0",
|
"laravel/tinker": "^3.0",
|
||||||
"livewire/blaze": "^1.0",
|
"livewire/blaze": "^1.0",
|
||||||
"livewire/flux": "^2.13.1",
|
"livewire/flux": "^2.13.1",
|
||||||
|
|||||||
Generated
+64
-1
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "0baf4a77af5bdd712ea8c8ae0dd072ca",
|
"content-hash": "3ba979872819ad8b9e06875e8c2231b0",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "bacon/bacon-qr-code",
|
"name": "bacon/bacon-qr-code",
|
||||||
@@ -1680,6 +1680,69 @@
|
|||||||
},
|
},
|
||||||
"time": "2026-08-04T14:50:50+00:00"
|
"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",
|
"name": "laravel/serializable-closure",
|
||||||
"version": "v2.0.15",
|
"version": "v2.0.15",
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||||
|
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
|
||||||
|
use Laravel\Sanctum\Http\Middleware\AuthenticateSession;
|
||||||
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Stateful Domains
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Requests from the following domains / hosts will receive stateful API
|
||||||
|
| authentication cookies. Typically, these should include your local
|
||||||
|
| and production domains which access your API via a frontend SPA.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'stateful' => 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,
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -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('personal_access_tokens', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user