Merge pull request #1 from subhajit9328/feature/test-saml-integration
Feature/test saml integration
This commit is contained in:
commit
6ff2b803bb
@ -1,81 +0,0 @@
|
||||
---
|
||||
name: fluxui-development
|
||||
description: "Use this skill for Flux UI development in Livewire applications only. Trigger when working with <flux:*> components, building or customizing Livewire component UIs, creating forms, modals, tables, or other interactive elements. Covers: flux: components (buttons, inputs, modals, forms, tables, date-pickers, kanban, badges, tooltips, etc.), component composition, Tailwind CSS styling, Heroicons/Lucide icon integration, validation patterns, responsive design, and theming. Do not use for non-Livewire frameworks or non-component styling."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Flux UI Development
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Flux UI patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
This project uses the free edition of Flux UI, which includes all free components and variants but not Pro components.
|
||||
|
||||
Flux UI is a component library for Livewire built with Tailwind CSS. It provides components that are easy to use and customize.
|
||||
|
||||
Use Flux UI components when available. Fall back to standard Blade components when no Flux component exists for your needs.
|
||||
|
||||
<!-- Basic Button -->
|
||||
```blade
|
||||
<flux:button variant="primary">Click me</flux:button>
|
||||
```
|
||||
|
||||
## Available Components (Free Edition)
|
||||
|
||||
Available: avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown, field, heading, icon, input, modal, navbar, otp-input, profile, radio, select, separator, skeleton, switch, text, textarea, tooltip
|
||||
|
||||
## Icons
|
||||
|
||||
Flux includes [Heroicons](https://heroicons.com/) as its default icon set. Search for exact icon names on the Heroicons site - do not guess or invent icon names.
|
||||
|
||||
<!-- Icon Button -->
|
||||
```blade
|
||||
<flux:button icon="arrow-down-tray">Export</flux:button>
|
||||
```
|
||||
|
||||
For icons not available in Heroicons, use [Lucide](https://lucide.dev/). Import the icons you need with the Artisan command:
|
||||
|
||||
```bash
|
||||
php artisan flux:icon crown grip-vertical github
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Form Fields
|
||||
|
||||
<!-- Form Field -->
|
||||
```blade
|
||||
<flux:field>
|
||||
<flux:label>Email</flux:label>
|
||||
<flux:input type="email" wire:model="email" />
|
||||
<flux:error name="email" />
|
||||
</flux:field>
|
||||
```
|
||||
|
||||
### Modals
|
||||
|
||||
<!-- Modal -->
|
||||
```blade
|
||||
<flux:modal wire:model="showModal">
|
||||
<flux:heading>Title</flux:heading>
|
||||
<p>Content</p>
|
||||
</flux:modal>
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
1. Check component renders correctly
|
||||
2. Test interactive states
|
||||
3. Verify mobile responsiveness
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Trying to use Pro-only components in the free edition
|
||||
- Not checking if a Flux component exists before creating custom implementations
|
||||
- Forgetting to use the `search-docs` tool for component-specific documentation
|
||||
- Not following existing project patterns for Flux usage
|
||||
@ -63,3 +63,12 @@ AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
# Microsoft Graph API credentials for Domain Federation
|
||||
MICROSOFT_GRAPH_TENANT_ID=
|
||||
MICROSOFT_GRAPH_CLIENT_ID=
|
||||
MICROSOFT_GRAPH_CLIENT_SECRET=
|
||||
MICROSOFT_GRAPH_DOMAIN_NAME=
|
||||
MICROSOFT_GRAPH_DISPLAY_NAME="Company SSO"
|
||||
MICROSOFT_GRAPH_MFA_BEHAVIOR="acceptIfMfaDoneByFederatedIdp"
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -23,3 +23,4 @@ yarn-error.log
|
||||
/.vscode
|
||||
/.zed
|
||||
/storage/framework/views/*
|
||||
laradumps.yaml
|
||||
|
||||
@ -15,6 +15,7 @@ ## Foundational Context
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- livewire/flux (FLUXUI_FREE) - v2
|
||||
- livewire/livewire (LIVEWIRE) - v4
|
||||
- larastan/larastan (LARASTAN) - v3
|
||||
- laravel/boost (BOOST) - v2
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/pail (PAIL) - v1
|
||||
|
||||
@ -15,6 +15,7 @@ ## Foundational Context
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- livewire/flux (FLUXUI_FREE) - v2
|
||||
- livewire/livewire (LIVEWIRE) - v4
|
||||
- larastan/larastan (LARASTAN) - v3
|
||||
- laravel/boost (BOOST) - v2
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/pail (PAIL) - v1
|
||||
|
||||
2
CONTRIBUTING.MD
Normal file
2
CONTRIBUTING.MD
Normal file
@ -0,0 +1,2 @@
|
||||
Always use `#[NoDiscard]` attribute in function which fetches data from DB. This ensures that
|
||||
db does not called unnecessarily.
|
||||
7
DATABASE.md
Normal file
7
DATABASE.md
Normal file
@ -0,0 +1,7 @@
|
||||
## DB Diagram
|
||||

|
||||
|
||||
## ER Diagram
|
||||
### Diagram is not complete, Please use it as a reference with the previous DB Diagram.
|
||||

|
||||

|
||||
@ -4,8 +4,7 @@
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Concerns\PasswordValidationRules;
|
||||
use App\Concerns\ProfileValidationRules;
|
||||
use App\Concerns\{PasswordValidationRules, ProfileValidationRules};
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Laravel\Fortify\Contracts\CreatesNewUsers;
|
||||
|
||||
65
app/Concerns/Confirmation.php
Normal file
65
app/Concerns/Confirmation.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Concerns;
|
||||
|
||||
use Livewire\Attributes\On;
|
||||
|
||||
trait Confirmation
|
||||
{
|
||||
/**
|
||||
* Shows a confirmation modal before executing the given method.
|
||||
*
|
||||
* @param string $method Method name that should be executed if the user confirms.
|
||||
* @param mixed|null $params Any parameters that should be passed to the method.
|
||||
* @param string $title Title of the confirmation modal.
|
||||
* @param string $subtitle Subtitle of the confirmation modal.
|
||||
* @param string|null $description Description of the confirmation modal.
|
||||
* @param string $descriptionClass Classes to apply to the description element. (!, !important to overide)
|
||||
* @param string $acceptBtnClass Classes to apply to the accept button. (!, !important to overide)
|
||||
* @param string $icon Icon to display in the confirmation modal.
|
||||
*/
|
||||
public function requireConfirmation(
|
||||
string $method,
|
||||
mixed $params = null,
|
||||
string $title = 'Are you sure?',
|
||||
string $subtitle = 'This action cannot be undone.',
|
||||
?string $description = null,
|
||||
string $descriptionClass = '',
|
||||
string $acceptBtnClass = '',
|
||||
string $icon = 'lucide.triangle-alert'
|
||||
): void {
|
||||
$this->dispatch(
|
||||
'open-confirmation',
|
||||
componentId: $this->getId(),
|
||||
action: $method,
|
||||
params: $params,
|
||||
title: $title,
|
||||
subtitle: $subtitle,
|
||||
description: $description,
|
||||
descriptionClass: $descriptionClass,
|
||||
acceptBtnClass: $acceptBtnClass,
|
||||
icon: $icon
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen globally for the acceptance, but only process if the ID matches.
|
||||
*/
|
||||
#[On('confirmation-accepted')]
|
||||
public function onConfirmationAccepted(string $action, mixed $params, string $componentId): void
|
||||
{
|
||||
// Ignore the event if it wasn't requested by this specific component instance
|
||||
if ($this->getId() !== $componentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify the method actually exists on the component
|
||||
if (method_exists($this, $action)) {
|
||||
|
||||
// Execute the requested method, passing any provided parameters
|
||||
$this->{$action}($params);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
app/Concerns/EnumValuesAsArray.php
Normal file
13
app/Concerns/EnumValuesAsArray.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Concerns;
|
||||
|
||||
trait EnumValuesAsArray
|
||||
{
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
}
|
||||
43
app/Concerns/HandlesOperations.php
Normal file
43
app/Concerns/HandlesOperations.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Concerns;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Mary\Traits\Toast;
|
||||
use Throwable;
|
||||
|
||||
trait HandlesOperations
|
||||
{
|
||||
use Toast;
|
||||
|
||||
public function attempt(callable $action, ?callable $onError = null, string $successMessage = 'Success !', string $errorMessage = 'Something gone wrong!', bool $showSuccess = true): void
|
||||
{
|
||||
try {
|
||||
// call the action callback
|
||||
$action();
|
||||
if ($showSuccess) {
|
||||
$this->success($successMessage, css: 'alert-success alert-dismissible alert-soft');
|
||||
}
|
||||
} catch (ValidationException $exception) {
|
||||
|
||||
/**
|
||||
* We are catching this exception intentionally so that, generic throwable catch block
|
||||
* doesn't catch this, which will lead to close the modal.
|
||||
*
|
||||
* But, we are rethrowing this so that validation errors can be handled by others.
|
||||
*/
|
||||
|
||||
throw $exception;
|
||||
} catch (Throwable $exception) {
|
||||
if (null !== $onError) {
|
||||
// call the callback
|
||||
$onError();
|
||||
}
|
||||
$this->error($errorMessage, css: 'alert-error alert-dismissible alert-soft');
|
||||
Log::error($exception->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,7 @@ trait PasswordValidationRules
|
||||
/**
|
||||
* Get the validation rules used to validate passwords.
|
||||
*
|
||||
* @return array<int, ValidationRule|array<mixed>|string>
|
||||
* @return array<int, ValidationRule|Password|string>
|
||||
*/
|
||||
protected function passwordRules(): array
|
||||
{
|
||||
|
||||
@ -7,13 +7,14 @@
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Rules\Unique;
|
||||
|
||||
trait ProfileValidationRules
|
||||
{
|
||||
/**
|
||||
* Get the validation rules used to validate user profiles.
|
||||
*
|
||||
* @return array<string, array<int, ValidationRule|array<mixed>|string>>
|
||||
* @return array<string, array<int, ValidationRule|Unique|string>>
|
||||
*/
|
||||
protected function profileRules(?int $userId = null): array
|
||||
{
|
||||
@ -36,7 +37,7 @@ protected function nameRules(): array
|
||||
/**
|
||||
* Get the validation rules used to validate user emails.
|
||||
*
|
||||
* @return array<int, ValidationRule|array<mixed>|string>
|
||||
* @return array<int, ValidationRule|Unique|string>
|
||||
*/
|
||||
protected function emailRules(?int $userId = null): array
|
||||
{
|
||||
|
||||
115
app/Console/Commands/GenerateSamlKeysCommand.php
Normal file
115
app/Console/Commands/GenerateSamlKeysCommand.php
Normal file
@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class GenerateSamlKeysCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'saml:generate-keys {--force : Overwrite existing keys}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Generate self-signed SAML private key and public certificate for the IdP';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$directory = storage_path('app/saml');
|
||||
$keyPath = "{$directory}/idp.key";
|
||||
$certPath = "{$directory}/idp.crt";
|
||||
|
||||
if (! File::exists($directory)) {
|
||||
File::makeDirectory($directory, 0755, true);
|
||||
}
|
||||
|
||||
if (File::exists($keyPath) && File::exists($certPath) && ! $this->option('force')) {
|
||||
$this->info('SAML keys already exist. Use --force to overwrite them.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info('Generating SAML keys...');
|
||||
|
||||
$config = [
|
||||
'digest_alg' => 'sha256',
|
||||
'private_key_bits' => 2048,
|
||||
'private_key_type' => OPENSSL_KEYTYPE_RSA,
|
||||
];
|
||||
|
||||
// Create keypair
|
||||
$res = openssl_pkey_new($config);
|
||||
if (false === $res) {
|
||||
$this->error('Failed to generate private key. Ensure OpenSSL is configured.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
// Export private key
|
||||
if (! openssl_pkey_export($res, $privKey)) {
|
||||
$this->error('Failed to export private key.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
// Generate CSR and self-signed certificate
|
||||
$dn = [
|
||||
'countryName' => 'US',
|
||||
'stateOrProvinceName' => 'California',
|
||||
'localityName' => 'San Francisco',
|
||||
'organizationName' => 'Laravel SSO',
|
||||
'organizationalUnitName' => 'IT Department',
|
||||
'commonName' => 'laravel-sso-idp',
|
||||
'emailAddress' => 'admin@sso.local',
|
||||
];
|
||||
|
||||
$csr = openssl_csr_new($dn, $res, ['digest_alg' => 'sha256']);
|
||||
if (false === $csr) {
|
||||
$this->error('Failed to create CSR.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$cert = openssl_csr_sign($csr, null, $res, 3650, ['digest_alg' => 'sha256']); // 10 years validity
|
||||
if (false === $cert) {
|
||||
$this->error('Failed to sign certificate.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if (! openssl_x509_export($cert, $certOut)) {
|
||||
$this->error('Failed to export certificate.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
// Write files
|
||||
File::put($keyPath, $privKey);
|
||||
File::put($certPath, $certOut);
|
||||
|
||||
// Update or add .gitignore inside the saml directory
|
||||
$gitignorePath = "{$directory}/.gitignore";
|
||||
if (! File::exists($gitignorePath)) {
|
||||
File::put($gitignorePath, "idp.key\n");
|
||||
}
|
||||
|
||||
$this->info('Keys generated successfully:');
|
||||
$this->line("- Private Key: {$keyPath}");
|
||||
$this->line("- Certificate: {$certPath}");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
22
app/Data/ConnectedApp/ConnectAppRequest.php
Normal file
22
app/Data/ConnectedApp/ConnectAppRequest.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\ConnectedApp;
|
||||
|
||||
use Spatie\LaravelData\Attributes\MapOutputName;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
||||
#[MapOutputName(SnakeCaseMapper::class)]
|
||||
class ConnectAppRequest extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public int $connectionProviderId,
|
||||
public int $connectionProtocolId,
|
||||
public ?string $slug = null,
|
||||
public ?int $connectionStatusId = null,
|
||||
public ?array $settings = null,
|
||||
) {}
|
||||
}
|
||||
24
app/Data/ConnectedApp/ConnectedAppData.php
Normal file
24
app/Data/ConnectedApp/ConnectedAppData.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\ConnectedApp;
|
||||
|
||||
use Spatie\LaravelData\Attributes\MapInputName;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
final class ConnectedAppData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public string $name,
|
||||
#[MapInputName('connection_protocol_id')]
|
||||
public int $protocolId,
|
||||
#[MapInputName('connection_provider_id')]
|
||||
public int $providerId,
|
||||
#[MapInputName('connection_status_id')]
|
||||
public int $statusId,
|
||||
public ?string $slug = null,
|
||||
public ?array $settings = null,
|
||||
) {}
|
||||
}
|
||||
20
app/Data/ConnectedApp/ConnectionProviderData.php
Normal file
20
app/Data/ConnectedApp/ConnectionProviderData.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\ConnectedApp;
|
||||
|
||||
use Spatie\LaravelData\Attributes\MapInputName;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
||||
#[MapInputName(SnakeCaseMapper::class)]
|
||||
class ConnectionProviderData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public string $name,
|
||||
public string $slug
|
||||
) {}
|
||||
|
||||
}
|
||||
22
app/Data/ConnectedApp/ConnectionStatusData.php
Normal file
22
app/Data/ConnectedApp/ConnectionStatusData.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\ConnectedApp;
|
||||
|
||||
use App\Enums\ConnectionStatusEnum;
|
||||
use Spatie\LaravelData\Attributes\{MapInputName, WithCast};
|
||||
use Spatie\LaravelData\Casts\EnumCast;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
||||
#[MapInputName(SnakeCaseMapper::class)]
|
||||
class ConnectionStatusData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public int $id,
|
||||
#[WithCast(EnumCast::class)]
|
||||
public ConnectionStatusEnum $name,
|
||||
) {}
|
||||
|
||||
}
|
||||
21
app/Data/ConnectedApp/StoreConnectionProviderRequestData.php
Normal file
21
app/Data/ConnectedApp/StoreConnectionProviderRequestData.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\ConnectedApp;
|
||||
|
||||
use Spatie\LaravelData\Attributes\MapInputName;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
||||
#[MapInputName(SnakeCaseMapper::class)]
|
||||
class StoreConnectionProviderRequestData extends Data
|
||||
{
|
||||
public string $slug;
|
||||
|
||||
public function __construct(
|
||||
public string $name,
|
||||
) {
|
||||
$this->slug = str($name)->slug()->toString();
|
||||
}
|
||||
}
|
||||
23
app/Data/MicrosoftGraph/MicrosoftGraphCredentialsData.php
Normal file
23
app/Data/MicrosoftGraph/MicrosoftGraphCredentialsData.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\MicrosoftGraph;
|
||||
|
||||
use App\Enums\FederatedIdpMfaBehaviorEnum;
|
||||
use Spatie\LaravelData\Attributes\MapOutputName;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
||||
#[MapOutputName(SnakeCaseMapper::class)]
|
||||
final class MicrosoftGraphCredentialsData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public string $tenantId,
|
||||
public string $clientId,
|
||||
public string $clientSecret,
|
||||
public string $domainName,
|
||||
public string $displayName = 'Company SSO',
|
||||
public FederatedIdpMfaBehaviorEnum $mfaBehavior = FederatedIdpMfaBehaviorEnum::AcceptIfMfaDoneByFederatedIdp,
|
||||
) {}
|
||||
}
|
||||
15
app/Data/Permissions/DeactivatedPermissionData.php
Normal file
15
app/Data/Permissions/DeactivatedPermissionData.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Permissions;
|
||||
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
final class DeactivatedPermissionData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public int $roleId,
|
||||
public int $permissionId,
|
||||
) {}
|
||||
}
|
||||
33
app/Data/Permissions/PermissionData.php
Normal file
33
app/Data/Permissions/PermissionData.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Permissions;
|
||||
|
||||
use App\Enums\ConnectedAppPermissonEnum;
|
||||
use App\Helpers\AppPermission;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
class PermissionData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public string $name,
|
||||
public ?ConnectedAppPermissonEnum $type,
|
||||
public string $resource = 'general',
|
||||
) {}
|
||||
|
||||
public static function fromModel(Permission $permission): self
|
||||
{
|
||||
$parts = explode(':', $permission->name);
|
||||
$resource = count($parts) > 1 ? $parts[0] : 'general';
|
||||
|
||||
return new self(
|
||||
id: $permission->id,
|
||||
name: $permission->name,
|
||||
type: AppPermission::type($permission->name),
|
||||
resource: $resource,
|
||||
);
|
||||
}
|
||||
}
|
||||
17
app/Data/Role/AssignAppsToRoleData.php
Normal file
17
app/Data/Role/AssignAppsToRoleData.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Role;
|
||||
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
final class AssignAppsToRoleData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public int $roleId,
|
||||
public string $validUpto,
|
||||
/** @var int[] */
|
||||
public array $appIds
|
||||
) {}
|
||||
}
|
||||
16
app/Data/Role/AssignUsersToRoleData.php
Normal file
16
app/Data/Role/AssignUsersToRoleData.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Role;
|
||||
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
final class AssignUsersToRoleData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public int $roleId,
|
||||
/** @var int[] */
|
||||
public array $userIds
|
||||
) {}
|
||||
}
|
||||
15
app/Data/Role/CreateRoleData.php
Normal file
15
app/Data/Role/CreateRoleData.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Role;
|
||||
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
final class CreateRoleData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public int $priority,
|
||||
) {}
|
||||
}
|
||||
31
app/Data/Role/RoleAppData.php
Normal file
31
app/Data/Role/RoleAppData.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Role;
|
||||
|
||||
use App\Models\ConnectedApp;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
final class RoleAppData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public int $appId,
|
||||
public string $appName,
|
||||
public string $duration,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Intercept the Eloquent model and map it to the DTO properties.
|
||||
*/
|
||||
public static function fromModel(ConnectedApp $app): self
|
||||
{
|
||||
return new self(
|
||||
id: $app->pivot->id,
|
||||
appId: $app->id,
|
||||
appName: $app->name,
|
||||
duration: $app->pivot->duration
|
||||
);
|
||||
}
|
||||
}
|
||||
24
app/Data/Role/RoleData.php
Normal file
24
app/Data/Role/RoleData.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Role;
|
||||
|
||||
use App\Data\Permissions\PermissionData;
|
||||
use Spatie\LaravelData\Attributes\DataCollectionOf;
|
||||
use Spatie\LaravelData\{Data, DataCollection};
|
||||
|
||||
final class RoleData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public string $name,
|
||||
public int $priority = 0,
|
||||
#[DataCollectionOf(class: RoleAppData::class)]
|
||||
public ?DataCollection $apps = null,
|
||||
#[DataCollectionOf(class: RoleUserData::class)]
|
||||
public ?DataCollection $users = null,
|
||||
#[DataCollectionOf(PermissionData::class)]
|
||||
public ?DataCollection $permissions = null,
|
||||
) {}
|
||||
}
|
||||
29
app/Data/Role/RoleUserData.php
Normal file
29
app/Data/Role/RoleUserData.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Role;
|
||||
|
||||
use App\Models\User;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
final class RoleUserData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public string $name,
|
||||
public string $initials = '',
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Intercept the Eloquent model and map it to the DTO properties.
|
||||
*/
|
||||
public static function fromModel(User $user): self
|
||||
{
|
||||
return new self(
|
||||
id: $user->id,
|
||||
name: $user->name,
|
||||
initials: $user->initials()
|
||||
);
|
||||
}
|
||||
}
|
||||
15
app/Data/Role/UpdateRolePriorityData.php
Normal file
15
app/Data/Role/UpdateRolePriorityData.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Role;
|
||||
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
final class UpdateRolePriorityData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public int $roleId,
|
||||
public int $priority,
|
||||
) {}
|
||||
}
|
||||
19
app/Data/Role/UserSelectData.php
Normal file
19
app/Data/Role/UserSelectData.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Role;
|
||||
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
/**
|
||||
* Lightweight DTO for populating the user search/select dropdown.
|
||||
* Only carries the fields the UI choices component needs.
|
||||
*/
|
||||
final class UserSelectData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public string $name,
|
||||
) {}
|
||||
}
|
||||
23
app/Data/Ticket/CreateTicketRequest.php
Normal file
23
app/Data/Ticket/CreateTicketRequest.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Ticket;
|
||||
|
||||
use Spatie\LaravelData\Attributes\MapOutputName;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
||||
#[MapOutputName(SnakeCaseMapper::class)]
|
||||
class CreateTicketRequest extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public int $userId,
|
||||
public ?int $connectedAppId,
|
||||
public string $issueCategory,
|
||||
public ?string $description = null,
|
||||
public array $notifiedRoleIds = [],
|
||||
public array $notifiedUserIds = [],
|
||||
public mixed $attachment = null,
|
||||
) {}
|
||||
}
|
||||
19
app/Data/Ticket/PostReplyRequest.php
Normal file
19
app/Data/Ticket/PostReplyRequest.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Ticket;
|
||||
|
||||
use Spatie\LaravelData\Attributes\MapOutputName;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
||||
#[MapOutputName(SnakeCaseMapper::class)]
|
||||
class PostReplyRequest extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public int $ticketId,
|
||||
public int $userId,
|
||||
public string $message,
|
||||
) {}
|
||||
}
|
||||
31
app/Data/Ui/ConnectedApps/ConnectedAppData.php
Normal file
31
app/Data/Ui/ConnectedApps/ConnectedAppData.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Ui\ConnectedApps;
|
||||
|
||||
use App\Enums\{ConnectionProtocolEnum, ConnectionStatusEnum};
|
||||
use App\Models\ConnectedApp;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
final class ConnectedAppData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public string $name,
|
||||
public ConnectionProtocolEnum $protocol,
|
||||
public ?string $provider,
|
||||
public ConnectionStatusEnum $status,
|
||||
) {}
|
||||
|
||||
public static function fromModel(ConnectedApp $app): self
|
||||
{
|
||||
return new self(
|
||||
id: $app->id,
|
||||
name: $app->name,
|
||||
protocol: ConnectionProtocolEnum::tryFrom($app->protocol->name),
|
||||
provider: $app->provider?->name,
|
||||
status: ConnectionStatusEnum::tryFrom($app->status->name)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Ui\ConnectedServices;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* A collection of connected services.
|
||||
*
|
||||
* @extends Collection<int, ConnectedServiceData>
|
||||
*/
|
||||
final class ConnectedServiceCollectionData extends Collection {}
|
||||
@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Ui\ConnectedServices;
|
||||
|
||||
use App\Enums\ConnectionStatus;
|
||||
use App\Enums\ConnectionTechTypes;
|
||||
use Livewire\Wireable;
|
||||
use Spatie\LaravelData\Concerns\WireableData;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
final class ConnectedServiceData extends Data implements Wireable
|
||||
{
|
||||
use WireableData;
|
||||
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $icon,
|
||||
public ConnectionTechTypes $type,
|
||||
public string $connectionService,
|
||||
public ConnectionStatus $status,
|
||||
) {}
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Ui\ConnectedServices;
|
||||
|
||||
use Livewire\Wireable;
|
||||
use Spatie\LaravelData\Concerns\WireableData;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
final class ConnectedServicesData extends Data implements Wireable
|
||||
{
|
||||
use WireableData;
|
||||
|
||||
public function __construct(
|
||||
public ConnectedServiceCollectionData $services,
|
||||
) {}
|
||||
}
|
||||
19
app/Data/Ui/Dashboard/ServiceAppDto.php
Normal file
19
app/Data/Ui/Dashboard/ServiceAppDto.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Ui\Dashboard;
|
||||
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
class ServiceAppDto extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public string $name,
|
||||
public string $slug,
|
||||
public string $duration,
|
||||
public int $days_remaining,
|
||||
public bool $is_warning,
|
||||
) {}
|
||||
}
|
||||
18
app/Data/Ui/ManageRoles/PermissionData.php
Normal file
18
app/Data/Ui/ManageRoles/PermissionData.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Ui\ManageRoles;
|
||||
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
final class PermissionData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public bool $view,
|
||||
public bool $create,
|
||||
public bool $update,
|
||||
public bool $delete,
|
||||
) {}
|
||||
}
|
||||
@ -4,19 +4,23 @@
|
||||
|
||||
namespace App\Data\Ui\ManageRoles;
|
||||
|
||||
use App\Data\Users\UsersData;
|
||||
use App\Enums\RolesStatus;
|
||||
use Livewire\Wireable;
|
||||
use Spatie\LaravelData\Concerns\WireableData;
|
||||
use Spatie\LaravelData\Data;
|
||||
use App\Data\Users\UserData;
|
||||
use App\Enums\RolesStatusEnum;
|
||||
use Spatie\LaravelData\Attributes\DataCollectionOf;
|
||||
use Spatie\LaravelData\{Data, DataCollection};
|
||||
|
||||
final class RoleData extends Data implements Wireable
|
||||
final class RoleData extends Data
|
||||
{
|
||||
use WireableData;
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public UsersData $users,
|
||||
public ServiceCollection $services,
|
||||
public RolesStatus $status,
|
||||
|
||||
/** @var DataCollection<int, UserData> */
|
||||
#[DataCollectionOf(class: UserData::class), ]
|
||||
public DataCollection $users,
|
||||
|
||||
/** @var DataCollection<int, ServiceData> */
|
||||
#[DataCollectionOf(class: ServiceData::class), ]
|
||||
public DataCollection $services,
|
||||
public RolesStatusEnum $status,
|
||||
) {}
|
||||
}
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Ui\ManageRoles;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @extends Collection<int, RoleData>
|
||||
*/
|
||||
final class RolesCollection extends Collection {}
|
||||
@ -1,17 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Ui\ManageRoles;
|
||||
|
||||
use Livewire\Wireable;
|
||||
use Spatie\LaravelData\Concerns\WireableData;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
final class RolesData extends Data implements Wireable
|
||||
{
|
||||
use WireableData;
|
||||
public function __construct(
|
||||
public RolesCollection $roles,
|
||||
) {}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Ui\ManageRoles;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @extends Collection<int, ServiceData>
|
||||
*/
|
||||
final class ServiceCollection extends Collection {}
|
||||
@ -4,15 +4,9 @@
|
||||
|
||||
namespace App\Data\Ui\ManageRoles;
|
||||
|
||||
use Livewire\Wireable;
|
||||
use Spatie\LaravelData\Concerns\WireableData;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
final class ServiceData extends Data implements Wireable
|
||||
final class ServiceData extends Data
|
||||
{
|
||||
use WireableData;
|
||||
|
||||
public function __construct(
|
||||
public string $name,
|
||||
) {}
|
||||
public function __construct(public string $name) {}
|
||||
}
|
||||
|
||||
3
app/Data/Ui/README.md
Normal file
3
app/Data/Ui/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
This folder includes DTO that are shared to UI. This DTO should not contains any sensitive data, like model Id if the
|
||||
UI
|
||||
doesn't need it.
|
||||
20
app/Data/Ui/Sidebar/NavItemData.php
Normal file
20
app/Data/Ui/Sidebar/NavItemData.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Ui\Sidebar;
|
||||
|
||||
use Spatie\LaravelData\Attributes\DataCollectionOf;
|
||||
use Spatie\LaravelData\{Data, DataCollection};
|
||||
|
||||
final class NavItemData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public string $label,
|
||||
public string $icon,
|
||||
public string $active_pattern,
|
||||
public ?string $route = null,
|
||||
#[DataCollectionOf(NavSubItemData::class)]
|
||||
public ?DataCollection $sub_menu = null,
|
||||
) {}
|
||||
}
|
||||
18
app/Data/Ui/Sidebar/NavSubItemData.php
Normal file
18
app/Data/Ui/Sidebar/NavSubItemData.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Ui\Sidebar;
|
||||
|
||||
use BackedEnum;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
final class NavSubItemData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public string $label,
|
||||
public string $route,
|
||||
public string $active_pattern,
|
||||
public ?BackedEnum $permission = null
|
||||
) {}
|
||||
}
|
||||
16
app/Data/Ui/TableHeader.php
Normal file
16
app/Data/Ui/TableHeader.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Ui;
|
||||
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
final class TableHeader extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public string $key,
|
||||
public string $label,
|
||||
public ?array $format = null,
|
||||
) {}
|
||||
}
|
||||
34
app/Data/Ui/Ticket/ReplyData.php
Normal file
34
app/Data/Ui/Ticket/ReplyData.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Ui\Ticket;
|
||||
|
||||
use App\Models\TicketReply;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
final class ReplyData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public int $userId,
|
||||
public string $userName,
|
||||
public string $userInitials,
|
||||
public array $userRoles,
|
||||
public string $message,
|
||||
public string $createdAt,
|
||||
) {}
|
||||
|
||||
public static function fromModel(TicketReply $reply): self
|
||||
{
|
||||
return new self(
|
||||
id: $reply->id,
|
||||
userId: $reply->user_id,
|
||||
userName: $reply->user->name,
|
||||
userInitials: method_exists($reply->user, 'initials') ? $reply->user->initials() : mb_strtoupper(mb_substr($reply->user->name, 0, 2)),
|
||||
userRoles: $reply->user->roles->pluck('name')->toArray(),
|
||||
message: $reply->message,
|
||||
createdAt: $reply->created_at->diffForHumans(),
|
||||
);
|
||||
}
|
||||
}
|
||||
67
app/Data/Ui/Ticket/TicketData.php
Normal file
67
app/Data/Ui/Ticket/TicketData.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Ui\Ticket;
|
||||
|
||||
use App\Enums\TicketStatusEnum;
|
||||
use App\Models\Ticket;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
final class TicketData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public string $ticketId,
|
||||
public int $userId,
|
||||
public string $userName,
|
||||
public ?int $connectedAppId,
|
||||
public ?string $appName,
|
||||
public string $issueCategory,
|
||||
public ?string $description,
|
||||
public int $statusId,
|
||||
public TicketStatusEnum $status,
|
||||
public array $notifiedRoleNames,
|
||||
public array $notifiedUserNames,
|
||||
public string $createdAt,
|
||||
public array $attachments = [],
|
||||
public ?int $assignedUserId = null,
|
||||
public ?string $assignedUserName = null,
|
||||
public array $replies = [],
|
||||
) {}
|
||||
|
||||
public static function fromModel(Ticket $ticket): self
|
||||
{
|
||||
$attachments = $ticket->attachments->map(fn ($attachment) => [
|
||||
'id' => $attachment->id,
|
||||
'file_name' => $attachment->file_name,
|
||||
'file_path' => $attachment->file_path,
|
||||
'file_size' => $attachment->file_size,
|
||||
'mime_type' => $attachment->mime_type,
|
||||
])->toArray();
|
||||
|
||||
$replies = $ticket->relationLoaded('replies')
|
||||
? $ticket->replies->map(fn ($reply) => ReplyData::fromModel($reply))->toArray()
|
||||
: [];
|
||||
|
||||
return new self(
|
||||
id: $ticket->id,
|
||||
ticketId: $ticket->ticket_id,
|
||||
userId: $ticket->user_id,
|
||||
userName: $ticket->user->name,
|
||||
connectedAppId: $ticket->connected_app_id,
|
||||
appName: $ticket->connectedApp?->name,
|
||||
issueCategory: $ticket->issue_category,
|
||||
description: $ticket->description,
|
||||
statusId: $ticket->status_id,
|
||||
status: TicketStatusEnum::tryFrom($ticket->status->name),
|
||||
notifiedRoleNames: $ticket->notifiedRoles->pluck('name')->toArray(),
|
||||
notifiedUserNames: $ticket->notifiedUsers->pluck('name')->toArray(),
|
||||
createdAt: $ticket->created_at->toDateTimeString(),
|
||||
attachments: $attachments,
|
||||
assignedUserId: $ticket->assigned_user_id,
|
||||
assignedUserName: $ticket->assignedUser?->name,
|
||||
replies: $replies,
|
||||
);
|
||||
}
|
||||
}
|
||||
39
app/Data/Ui/Ticket/TicketListData.php
Normal file
39
app/Data/Ui/Ticket/TicketListData.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Ui\Ticket;
|
||||
|
||||
use App\Enums\TicketStatusEnum;
|
||||
use App\Models\Ticket;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
final class TicketListData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public string $ticketId,
|
||||
public string $userName,
|
||||
public ?string $appName,
|
||||
public string $issueCategory,
|
||||
public string $createdAt,
|
||||
public TicketStatusEnum $status,
|
||||
public string $statusSlug,
|
||||
public ?int $assignedUserId = null,
|
||||
) {}
|
||||
|
||||
public static function fromModel(Ticket $ticket): self
|
||||
{
|
||||
return new self(
|
||||
id: $ticket->id,
|
||||
ticketId: $ticket->ticket_id,
|
||||
userName: $ticket->user->name,
|
||||
appName: $ticket->connectedApp?->name,
|
||||
issueCategory: $ticket->issue_category,
|
||||
createdAt: $ticket->created_at->toDateTimeString(),
|
||||
status: TicketStatusEnum::from($ticket->status->name),
|
||||
statusSlug: $ticket->status->name,
|
||||
assignedUserId: $ticket->assigned_user_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
37
app/Data/User/UserIndexData.php
Normal file
37
app/Data/User/UserIndexData.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\User;
|
||||
|
||||
use App\Data\Role\RoleData;
|
||||
use App\Models\User;
|
||||
use Spatie\LaravelData\Attributes\DataCollectionOf;
|
||||
use Spatie\LaravelData\{Data, DataCollection};
|
||||
|
||||
final class UserIndexData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public string $name,
|
||||
public string $email,
|
||||
public string $initials,
|
||||
#[DataCollectionOf(class: RoleData::class)]
|
||||
public ?DataCollection $roles = null,
|
||||
public ?array $restrictedPermissionIds = null,
|
||||
public ?string $immutableId = null,
|
||||
) {}
|
||||
|
||||
public static function fromModel(User $user)
|
||||
{
|
||||
return new self(
|
||||
id: $user->id,
|
||||
name: $user->name,
|
||||
email: $user->email,
|
||||
initials: $user->initials(),
|
||||
roles: RoleData::collect($user->roles, DataCollection::class),
|
||||
restrictedPermissionIds: $user->restrictedPermissions->pluck('id')->toArray(),
|
||||
immutableId: $user->immutable_id
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Users;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @extends Collection<int, UserData>
|
||||
*/
|
||||
final class UserCollection extends Collection {}
|
||||
@ -4,13 +4,19 @@
|
||||
|
||||
namespace App\Data\Users;
|
||||
|
||||
use Spatie\LaravelData\Data;
|
||||
use App\Data\Ui\ManageRoles\RoleData;
|
||||
use Spatie\LaravelData\Attributes\DataCollectionOf;
|
||||
use Spatie\LaravelData\{Data, DataCollection};
|
||||
|
||||
final class UserData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $email,
|
||||
) {}
|
||||
public bool $active = false,
|
||||
|
||||
/** @var DataCollection<int, RoleData>|null */
|
||||
#[DataCollectionOf(class: RoleData::class)]
|
||||
public ?DataCollection $roles = null,
|
||||
) {}
|
||||
}
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Users;
|
||||
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
final class UsersData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public UserCollection $users,
|
||||
) {}
|
||||
}
|
||||
10
app/Enums/ConnectedAppPermissonEnum.php
Normal file
10
app/Enums/ConnectedAppPermissonEnum.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum ConnectedAppPermissonEnum: string
|
||||
{
|
||||
case Use = 'use';
|
||||
}
|
||||
16
app/Enums/ConnectionProtocolEnum.php
Normal file
16
app/Enums/ConnectionProtocolEnum.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Concerns\EnumValuesAsArray;
|
||||
|
||||
enum ConnectionProtocolEnum: string
|
||||
{
|
||||
use EnumValuesAsArray;
|
||||
|
||||
case OIDC = 'oidc';
|
||||
case SAML = 'saml';
|
||||
case OAUTH2 = 'oauth2';
|
||||
}
|
||||
@ -4,8 +4,12 @@
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum ConnectionStatus: string
|
||||
use App\Concerns\EnumValuesAsArray;
|
||||
|
||||
enum ConnectionStatusEnum: string
|
||||
{
|
||||
use EnumValuesAsArray;
|
||||
|
||||
case Connected = 'connected';
|
||||
case Disconnected = 'disconnected';
|
||||
case Pending = 'pending';
|
||||
@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum ConnectionTechTypes: string
|
||||
{
|
||||
case OIDC = 'oidc';
|
||||
case SAML = 'saml';
|
||||
}
|
||||
14
app/Enums/DefaultUserRole.php
Normal file
14
app/Enums/DefaultUserRole.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Concerns\EnumValuesAsArray;
|
||||
|
||||
enum DefaultUserRole: string
|
||||
{
|
||||
use EnumValuesAsArray;
|
||||
case Admin = 'admin';
|
||||
case User = 'user';
|
||||
}
|
||||
19
app/Enums/FederatedIdpMfaBehaviorEnum.php
Normal file
19
app/Enums/FederatedIdpMfaBehaviorEnum.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Concerns\EnumValuesAsArray;
|
||||
|
||||
/**
|
||||
* Entra ID Federated IdP MFA behavior configuration values.
|
||||
*/
|
||||
enum FederatedIdpMfaBehaviorEnum: string
|
||||
{
|
||||
use EnumValuesAsArray;
|
||||
|
||||
case AcceptIfMfaDoneByFederatedIdp = 'acceptIfMfaDoneByFederatedIdp';
|
||||
case EnforceMfaByFederatedIdp = 'enforceMfaByFederatedIdp';
|
||||
case RejectMfaByFederatedIdp = 'rejectMfaByFederatedIdp';
|
||||
}
|
||||
14
app/Enums/Permissions/AppPermissionEnum.php
Normal file
14
app/Enums/Permissions/AppPermissionEnum.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums\Permissions;
|
||||
|
||||
enum AppPermissionEnum: string
|
||||
{
|
||||
case Access = 'app:access';
|
||||
case Create = 'app:create';
|
||||
case Read = 'app:read';
|
||||
case Update = 'app:update';
|
||||
case Delete = 'app:delete';
|
||||
}
|
||||
14
app/Enums/Permissions/ConnectionProviderPermissionEnum.php
Normal file
14
app/Enums/Permissions/ConnectionProviderPermissionEnum.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums\Permissions;
|
||||
|
||||
enum ConnectionProviderPermissionEnum: string
|
||||
{
|
||||
case Access = 'connection-provider:access';
|
||||
case Create = 'connection-provider:create';
|
||||
case Read = 'connection-provider:read';
|
||||
case Update = 'connection-provider:update';
|
||||
case Delete = 'connection-provider:delete';
|
||||
}
|
||||
14
app/Enums/Permissions/PermissionPermissionEnum.php
Normal file
14
app/Enums/Permissions/PermissionPermissionEnum.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums\Permissions;
|
||||
|
||||
enum PermissionPermissionEnum: string
|
||||
{
|
||||
case Access = 'permission:access';
|
||||
case Create = 'permission:create';
|
||||
case Read = 'permission:read';
|
||||
case Update = 'permission:update';
|
||||
case Delete = 'permission:delete';
|
||||
}
|
||||
14
app/Enums/Permissions/ProfilePermissionEnum.php
Normal file
14
app/Enums/Permissions/ProfilePermissionEnum.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums\Permissions;
|
||||
|
||||
enum ProfilePermissionEnum: string
|
||||
{
|
||||
case Access = 'profile:access';
|
||||
case Create = 'profile:create';
|
||||
case Read = 'profile:read';
|
||||
case Update = 'profile:update';
|
||||
case Delete = 'profile:delete';
|
||||
}
|
||||
34
app/Enums/Permissions/README.md
Normal file
34
app/Enums/Permissions/README.md
Normal file
@ -0,0 +1,34 @@
|
||||
# Permissions Enums
|
||||
|
||||
This directory contains all the permissions enums.
|
||||
Spatie/laravel-permission uses these enums to check permissions.
|
||||
Any Permission enum added here will be automatically added to the database, when the
|
||||
[PermissionSeeder](../../../database/seeders/PermissionSeeder.php) is run, no need to add it manually.
|
||||
This is done via Reflection API.
|
||||
|
||||
`Only string backed are supported.`
|
||||
|
||||
## Value format of Enum
|
||||
|
||||
We are following the convention of `scope:resource:action` .
|
||||
|
||||
**File name** of the enum should be same as the **scope name**, and Enum *value* should be in the format of
|
||||
*resource:action*.
|
||||
|
||||
Example:
|
||||
UserPermissionEnum.php
|
||||
|
||||
```php
|
||||
namespace App\Enums\Permissions;
|
||||
enum UserPermissionEnum:string
|
||||
{
|
||||
case Create = 'user:create';
|
||||
case Read = 'user:read';
|
||||
case Update = 'user:update';
|
||||
case Delete = 'user:delete';
|
||||
}
|
||||
|
||||
// using the enum makes straight forward permission check
|
||||
$user->can(UserPermissionEnum::Create);
|
||||
@can(UserPermissionEnum::Create);
|
||||
```
|
||||
15
app/Enums/Permissions/RoleAndAppsPermissionEnum.php
Normal file
15
app/Enums/Permissions/RoleAndAppsPermissionEnum.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums\Permissions;
|
||||
|
||||
enum RoleAndAppsPermissionEnum: string
|
||||
{
|
||||
case Access = 'roles-and-apps:access';
|
||||
case Create = 'roles-and-apps:create';
|
||||
case Read = 'roles-and-apps:read';
|
||||
case Update = 'roles-and-apps:update';
|
||||
case Delete = 'roles-and-apps:delete';
|
||||
case ConnectApps = 'roles-and-apps:connect-apps';
|
||||
}
|
||||
14
app/Enums/Permissions/SecurityPermissionEnum.php
Normal file
14
app/Enums/Permissions/SecurityPermissionEnum.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums\Permissions;
|
||||
|
||||
enum SecurityPermissionEnum: string
|
||||
{
|
||||
case Access = 'security:access';
|
||||
case Create = 'security:create';
|
||||
case Read = 'security:read';
|
||||
case Update = 'security:update';
|
||||
case Delete = 'security:delete';
|
||||
}
|
||||
14
app/Enums/Permissions/TicketPermissionEnum.php
Normal file
14
app/Enums/Permissions/TicketPermissionEnum.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums\Permissions;
|
||||
|
||||
enum TicketPermissionEnum: string
|
||||
{
|
||||
case Access = 'ticket:access';
|
||||
case Create = 'ticket:create';
|
||||
case Read = 'ticket:read';
|
||||
case Update = 'ticket:update';
|
||||
case Delete = 'ticket:delete';
|
||||
}
|
||||
14
app/Enums/Permissions/UserPermissionEnum.php
Normal file
14
app/Enums/Permissions/UserPermissionEnum.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums\Permissions;
|
||||
|
||||
enum UserPermissionEnum: string
|
||||
{
|
||||
case Access = 'user:access';
|
||||
case Create = 'user:create';
|
||||
case Read = 'user:read';
|
||||
case Update = 'user:update';
|
||||
case Delete = 'user:delete';
|
||||
}
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum RolesStatus: string
|
||||
enum RolesStatusEnum: string
|
||||
{
|
||||
case Active = 'active';
|
||||
case Inactive = 'inactive';
|
||||
34
app/Enums/TicketStatusEnum.php
Normal file
34
app/Enums/TicketStatusEnum.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Concerns\EnumValuesAsArray;
|
||||
|
||||
enum TicketStatusEnum: string
|
||||
{
|
||||
use EnumValuesAsArray;
|
||||
|
||||
case Open = 'open';
|
||||
case InProgress = 'in-progress';
|
||||
case Hold = 'hold';
|
||||
case Resolved = 'resolved';
|
||||
case Closed = 'closed';
|
||||
|
||||
public static function toLabels()
|
||||
{
|
||||
return array_map(fn ($status) => $status->label(), self::cases());
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Open => 'Open',
|
||||
self::InProgress => 'In Progress',
|
||||
self::Hold => 'Hold',
|
||||
self::Resolved => 'Resolved',
|
||||
self::Closed => 'Closed',
|
||||
};
|
||||
}
|
||||
}
|
||||
68
app/Helpers/AppPermission.php
Normal file
68
app/Helpers/AppPermission.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use App\Data\ConnectedApp\ConnectedAppData;
|
||||
use App\Enums\ConnectedAppPermissonEnum;
|
||||
|
||||
class AppPermission
|
||||
{
|
||||
/**
|
||||
* The standard scope name for connected app permissions.
|
||||
*/
|
||||
public const SCOPE = 'app';
|
||||
|
||||
/**
|
||||
* Generates the standard permission string following the scope:resource:action convention.
|
||||
* Format: app:{slug}:{action}
|
||||
*
|
||||
* @param ConnectedAppPermissonEnum $permission The action to perform (e.g., 'use')
|
||||
* @param string|ConnectedAppData $app The connected app object or its slug
|
||||
*
|
||||
* @return string The formatted permission name
|
||||
*/
|
||||
public static function name(ConnectedAppPermissonEnum $permission, string|ConnectedAppData $app): string
|
||||
{
|
||||
$slug = is_string($app) ? $app : $app->slug;
|
||||
|
||||
return sprintf('%s:%s:%s', self::SCOPE, $slug, $permission->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the permission string to extract the ConnectedAppPermissonEnum type (action).
|
||||
*
|
||||
* @param string $permissionName The constructed permission name
|
||||
*
|
||||
* @return ?ConnectedAppPermissonEnum The matching enum or null
|
||||
*/
|
||||
public static function type(string $permissionName): ?ConnectedAppPermissonEnum
|
||||
{
|
||||
$parts = explode(':', $permissionName);
|
||||
|
||||
if (3 === count($parts) && self::SCOPE === $parts[0]) {
|
||||
return ConnectedAppPermissonEnum::tryFrom($parts[2]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the permission string to extract the connected app slug (resource).
|
||||
*
|
||||
* @param string $permissionName The constructed permission name
|
||||
*
|
||||
* @return ?string The app slug or null
|
||||
*/
|
||||
public static function slug(string $permissionName): ?string
|
||||
{
|
||||
$parts = explode(':', $permissionName);
|
||||
|
||||
if (3 === count($parts) && self::SCOPE === $parts[0]) {
|
||||
return $parts[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
23
app/Helpers/DashboardRoute.php
Normal file
23
app/Helpers/DashboardRoute.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use App\Enums\DefaultUserRole;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class DashboardRoute
|
||||
{
|
||||
/**
|
||||
* This function will resolve the dashboard route based on the user role.
|
||||
*/
|
||||
public static function resolve(Collection $roles): string
|
||||
{
|
||||
if ($roles->contains(DefaultUserRole::Admin->value)) {
|
||||
return 'dashboard.admin';
|
||||
}
|
||||
|
||||
return 'dashboard.user';
|
||||
}
|
||||
}
|
||||
90
app/Helpers/EnumExtractor.php
Normal file
90
app/Helpers/EnumExtractor.php
Normal file
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use FilesystemIterator;
|
||||
use Generator;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use SplFileInfo;
|
||||
|
||||
class EnumExtractor
|
||||
{
|
||||
private string $namespace;
|
||||
|
||||
private string $directory;
|
||||
|
||||
public function __construct(string $namespace, string $directory)
|
||||
{
|
||||
$this->namespace = mb_trim($namespace, '\\').'\\';
|
||||
$this->directory = mb_rtrim($directory, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the extraction process and returns an array of actual Enum objects.
|
||||
*/
|
||||
public function extract(): array
|
||||
{
|
||||
$enumObjects = [];
|
||||
|
||||
foreach ($this->getPhpFiles() as $file) {
|
||||
$className = $this->resolveClassName($file->getPathname());
|
||||
|
||||
$this->loadClassIfNeeded($className, $file->getPathname());
|
||||
|
||||
if (enum_exists($className)) {
|
||||
// $className::cases() returns an array of the actual Enum objects
|
||||
// e.g., [App\Enums\Status::Active, App\Enums\Status::Inactive]
|
||||
$enumObjects[$className] = $className::cases();
|
||||
}
|
||||
}
|
||||
|
||||
return $enumObjects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Yields only PHP files from the target directory.
|
||||
*/
|
||||
private function getPhpFiles(): Generator
|
||||
{
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($this->directory, FilesystemIterator::SKIP_DOTS)
|
||||
);
|
||||
|
||||
/** @var SplFileInfo $file */
|
||||
foreach ($iterator as $file) {
|
||||
if ('php' === $file->getExtension()) {
|
||||
yield $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a physical file path to a fully qualified PSR-4 class name.
|
||||
*/
|
||||
private function resolveClassName(string $filePath): string
|
||||
{
|
||||
// Strip the base directory from the path
|
||||
$relativePath = str_replace($this->directory, '', $filePath);
|
||||
|
||||
// Normalize slashes and remove the .php extension
|
||||
$normalizedPath = str_replace([DIRECTORY_SEPARATOR, '/', '.php'], ['\\', '\\', ''], $relativePath);
|
||||
|
||||
return $this->namespace.mb_ltrim($normalizedPath, '\\');
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires the file into memory if it hasn't been autoloaded yet.
|
||||
*/
|
||||
private function loadClassIfNeeded(string $className, string $filePath): void
|
||||
{
|
||||
if (! class_exists($className, false)
|
||||
&& ! enum_exists($className, false)
|
||||
&& ! interface_exists($className, false)
|
||||
) {
|
||||
require_once $filePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
app/Http/Controllers/ResolveDashboardRouteController.php
Normal file
23
app/Http/Controllers/ResolveDashboardRouteController.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Helpers\DashboardRoute;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ResolveDashboardRouteController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
if (session()->has('saml_pending_request')) {
|
||||
return redirect()->route('saml.sso');
|
||||
}
|
||||
|
||||
$user = $request->user();
|
||||
$route = DashboardRoute::resolve($user->getRoleNames());
|
||||
|
||||
return to_route($route);
|
||||
}
|
||||
}
|
||||
39
app/Http/Controllers/ResolveNotificationRouteController.php
Normal file
39
app/Http/Controllers/ResolveNotificationRouteController.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Notifications\{TicketAssignedNotification, TicketCommentedNotification, TicketRaisedNotification, TicketStatusChangedNotification};
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ResolveNotificationRouteController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, string $id)
|
||||
{
|
||||
$user = $request->user();
|
||||
if (! $user) {
|
||||
return to_route('login');
|
||||
}
|
||||
|
||||
$notification = $user->notifications()->findOrFail($id);
|
||||
$notification->markAsRead();
|
||||
|
||||
// Modular pattern: Match notification type to determine where to redirect
|
||||
$url = match ($notification->type) {
|
||||
TicketRaisedNotification::class,
|
||||
TicketAssignedNotification::class,
|
||||
TicketCommentedNotification::class,
|
||||
TicketStatusChangedNotification::class => route('tickets.index', [
|
||||
'ticket' => data_get($notification->data, 'ticket_id'),
|
||||
]),
|
||||
|
||||
// Future expansion:
|
||||
// \App\Notifications\AppExpiredNotification::class => route('apps.index', ['app' => data_get($notification->data, 'app_id')]),
|
||||
|
||||
default => route('dashboard'),
|
||||
};
|
||||
|
||||
return redirect($url);
|
||||
}
|
||||
}
|
||||
150
app/Http/Controllers/SamlIdpController.php
Normal file
150
app/Http/Controllers/SamlIdpController.php
Normal file
@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\SamlIdpService;
|
||||
use Illuminate\Http\{Request, Response};
|
||||
use Illuminate\Support\Facades\{Auth, Log};
|
||||
use Throwable;
|
||||
|
||||
class SamlIdpController extends Controller
|
||||
{
|
||||
public function __construct(private readonly SamlIdpService $samlService) {}
|
||||
|
||||
/**
|
||||
* SAML 2.0 Single Sign-On (SSO) Endpoint.
|
||||
* Handles both GET (Redirect binding) and POST (POST binding) requests.
|
||||
*/
|
||||
public function sso(Request $request)
|
||||
{
|
||||
// 1. Retrieve SAMLRequest and RelayState (from request parameters or session fallback)
|
||||
$samlRequest =
|
||||
$request->input('SAMLRequest') ?? session('saml_pending_request');
|
||||
$relayState =
|
||||
$request->input('RelayState') ??
|
||||
session('saml_pending_relay_state');
|
||||
|
||||
if (empty($samlRequest)) {
|
||||
Log::warning('SAML SSO accessed without a SAMLRequest.');
|
||||
abort(400, 'Missing SAMLRequest parameter.');
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. Parse the SAMLRequest XML
|
||||
$parsed = $this->samlService->parseRequest($samlRequest);
|
||||
} catch (Throwable $e) {
|
||||
Log::error('Failed to parse SAMLRequest: '.$e->getMessage());
|
||||
abort(400, 'Invalid SAMLRequest: '.$e->getMessage());
|
||||
}
|
||||
|
||||
// 3. Find corresponding Connected App by issuer (SAML Entity ID)
|
||||
$connectedApp = $this->samlService->findConnectedApp($parsed['issuer']);
|
||||
|
||||
if (! $connectedApp) {
|
||||
Log::warning(
|
||||
"SAML SP Issuer [{$parsed['issuer']}] is not registered.",
|
||||
);
|
||||
abort(
|
||||
403,
|
||||
"The Service Provider [{$parsed['issuer']}] is not authorized in this system.",
|
||||
);
|
||||
}
|
||||
|
||||
if ('disconnected' === $connectedApp->status?->name) {
|
||||
Log::warning(
|
||||
"SAML SP Issuer [{$parsed['issuer']}] is registered but disconnected.",
|
||||
);
|
||||
abort(
|
||||
403,
|
||||
"The Service Provider [{$connectedApp->name}] is currently deactivated.",
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @var array{
|
||||
* saml?: array{
|
||||
* entity_id?: string,
|
||||
* acs_url?: string
|
||||
* }
|
||||
* } $settings
|
||||
*/
|
||||
$settings = $connectedApp->settings;
|
||||
// Use the ACS URL from the request, falling back to the registered ACS URL
|
||||
$acsUrl = $parsed['acsUrl'] ?: $settings['saml']['acs_url'] ?? null;
|
||||
|
||||
if (empty($acsUrl)) {
|
||||
Log::error(
|
||||
"No Assertion Consumer Service (ACS) URL defined for [{$connectedApp->name}].",
|
||||
);
|
||||
abort(400, 'Assertion Consumer Service (ACS) URL is not defined.');
|
||||
}
|
||||
|
||||
// 4. Authenticate the User
|
||||
if (! Auth::check()) {
|
||||
// Store the SAML details in session to complete flow post-login
|
||||
session([
|
||||
'saml_pending_request' => $samlRequest,
|
||||
'saml_pending_relay_state' => $relayState,
|
||||
]);
|
||||
|
||||
return redirect()->route('login');
|
||||
}
|
||||
|
||||
// 5. Generate Signed SAML Response
|
||||
$user = Auth::user();
|
||||
|
||||
try {
|
||||
/** @var \App\Models\User $user */
|
||||
$samlResponse = $this->samlService->generateResponse(
|
||||
$user,
|
||||
$connectedApp,
|
||||
$parsed['requestId'],
|
||||
$acsUrl,
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
Log::error('Failed to generate SAML Response: '.$e->getMessage());
|
||||
abort(500, 'Cryptographic signing of SAML Response failed.');
|
||||
}
|
||||
|
||||
// 6. Clear pending SAML request from session
|
||||
session()->forget(['saml_pending_request', 'saml_pending_relay_state']);
|
||||
|
||||
// 7. Return HTTP-POST Auto-Submission Page
|
||||
return view('saml.post_response', [
|
||||
'appName' => $connectedApp->name,
|
||||
'acsUrl' => $acsUrl,
|
||||
'samlResponse' => $samlResponse,
|
||||
'relayState' => $relayState,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* SAML 2.0 Identity Provider (IdP) Metadata Endpoint.
|
||||
* Exposes public keys and bindings to Service Providers like Microsoft Entra ID.
|
||||
*/
|
||||
public function metadata(): Response
|
||||
{
|
||||
$idpEntityId = route('saml.metadata');
|
||||
$ssoUrl = route('saml.sso');
|
||||
|
||||
try {
|
||||
$metadataXml = $this->samlService->generateMetadata(
|
||||
$idpEntityId,
|
||||
$ssoUrl,
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
Log::error(
|
||||
'Failed to generate SAML IdP Metadata: '.$e->getMessage(),
|
||||
);
|
||||
abort(
|
||||
500,
|
||||
'Failed to retrieve Identity Provider cryptographic identity.',
|
||||
);
|
||||
}
|
||||
|
||||
return response($metadataXml, 200, [
|
||||
'Content-Type' => 'application/xml; charset=utf-8',
|
||||
]);
|
||||
}
|
||||
}
|
||||
28
app/Http/Responses/LoginResponse.php
Normal file
28
app/Http/Responses/LoginResponse.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Responses;
|
||||
|
||||
use Laravel\Fortify\Contracts\LoginResponse as LoginResponseContract;
|
||||
|
||||
/**
|
||||
* This class is used overide the fortify LoginResponse class, so that
|
||||
* we can redirect the user to the intended page after login.
|
||||
*/
|
||||
class LoginResponse implements LoginResponseContract
|
||||
{
|
||||
public function toResponse($request)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
activity()
|
||||
->causedBy($user)
|
||||
->withProperties([
|
||||
'roles' => $user->getRoleNames(),
|
||||
])
|
||||
->log('User logged in}');
|
||||
|
||||
return to_route('dashboard');
|
||||
}
|
||||
}
|
||||
@ -4,8 +4,7 @@
|
||||
|
||||
namespace App\Livewire\Actions;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Illuminate\Support\Facades\{Auth, Session};
|
||||
|
||||
final class Logout
|
||||
{
|
||||
|
||||
29
app/Livewire/Forms/AssignAppToRoleForm.php
Normal file
29
app/Livewire/Forms/AssignAppToRoleForm.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Livewire\Forms;
|
||||
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Form;
|
||||
|
||||
class AssignAppToRoleForm extends Form
|
||||
{
|
||||
#[Validate([
|
||||
'appIds' => 'required|array|min:1',
|
||||
'appIds.*' => 'integer|exists:connected_apps,id',
|
||||
])]
|
||||
public array $appIds = [];
|
||||
|
||||
#[Validate('exclude_if:isUnlimited,true|required|date|after_or_equal:today')]
|
||||
public ?string $validUpto = null;
|
||||
|
||||
public bool $isUnlimited = false;
|
||||
|
||||
public function reset(mixed ...$properties): void
|
||||
{
|
||||
$this->resetErrorBag();
|
||||
parent::reset(...$properties);
|
||||
$this->validUpto = null;
|
||||
}
|
||||
}
|
||||
33
app/Livewire/Forms/AssignRolesForm.php
Normal file
33
app/Livewire/Forms/AssignRolesForm.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Livewire\Forms;
|
||||
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Form;
|
||||
|
||||
class AssignRolesForm extends Form
|
||||
{
|
||||
/**
|
||||
* Holds the roles that the user has
|
||||
*
|
||||
* @var array <int, int>
|
||||
*/
|
||||
#[Validate('required|array')]
|
||||
public array $roleIds = [];
|
||||
|
||||
/**
|
||||
* Holds the direct permissions toggled for the user (from all selected roles).
|
||||
*
|
||||
* @var array<int, int>
|
||||
*/
|
||||
#[Validate('nullable|array')]
|
||||
public array $permissions = [];
|
||||
|
||||
/**
|
||||
* Holds the SSO Immutable ID for the user.
|
||||
*/
|
||||
#[Validate('nullable|string|max:255')]
|
||||
public string $immutableId = '';
|
||||
}
|
||||
16
app/Livewire/Forms/CreateRoleForm.php
Normal file
16
app/Livewire/Forms/CreateRoleForm.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Livewire\Forms;
|
||||
|
||||
use Livewire\Attributes\Validate;
|
||||
|
||||
class CreateRoleForm extends AssignAppToRoleForm
|
||||
{
|
||||
#[Validate('required|string|max:255|min:3')]
|
||||
public string $name = '';
|
||||
|
||||
#[Validate('required|integer|min:0')]
|
||||
public int $priority = 0;
|
||||
}
|
||||
62
app/Livewire/Forms/CreateTicketForm.php
Normal file
62
app/Livewire/Forms/CreateTicketForm.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Livewire\Forms;
|
||||
|
||||
use Livewire\Form;
|
||||
|
||||
class CreateTicketForm extends Form
|
||||
{
|
||||
public ?int $connectedAppId = null;
|
||||
|
||||
public string $issueCategory = 'Billing Query';
|
||||
|
||||
public string $description = '';
|
||||
|
||||
public string $routingType = 'none';
|
||||
|
||||
public array $notifiedRoleIds = [];
|
||||
|
||||
public array $notifiedUserIds = [];
|
||||
|
||||
public mixed $attachment = null;
|
||||
|
||||
/**
|
||||
* Define the validation rules dynamically.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'connectedAppId' => 'nullable|exists:connected_apps,id',
|
||||
'issueCategory' => 'required|string',
|
||||
'routingType' => 'nullable|in:role,user,none',
|
||||
'notifiedRoleIds' => 'role' === $this->routingType ? 'required|array|min:1' : 'nullable|array',
|
||||
'notifiedRoleIds.*' => 'role' === $this->routingType ? 'exists:roles,id' : 'nullable',
|
||||
'notifiedUserIds' => 'user' === $this->routingType ? 'required|array|min:1' : 'nullable|array',
|
||||
'notifiedUserIds.*' => 'user' === $this->routingType ? 'exists:users,id' : 'nullable',
|
||||
'description' => 'Others' === $this->issueCategory ? 'required|string|min:20' : 'nullable|string',
|
||||
'attachment' => 'Others' === $this->issueCategory ? 'nullable|file|mimes:jpg,jpeg,png,pdf|max:5120' : 'nullable',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Define validation attributes.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function validationAttributes(): array
|
||||
{
|
||||
return [
|
||||
'connectedAppId' => 'service',
|
||||
'issueCategory' => 'issue category',
|
||||
'routingType' => 'routing type',
|
||||
'notifiedRoleIds' => 'notified roles',
|
||||
'notifiedUserIds' => 'notified users',
|
||||
'description' => 'description',
|
||||
'attachment' => 'file attachment',
|
||||
];
|
||||
}
|
||||
}
|
||||
11
app/Livewire/Forms/README.md
Normal file
11
app/Livewire/Forms/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
All the forms used in the application are stored here.
|
||||
|
||||
Each form should validate the data before sending it to the server.
|
||||
This is done by using the `validate` method.
|
||||
|
||||
Validations that require the db, should not be live (realtime),
|
||||
they should be done on the only once, preferably after the form is submitted.
|
||||
|
||||
Forms should not be used to create, update or any database operation.
|
||||
|
||||
Livewire form docs: https://livewire.laravel.com/docs/4.x/forms
|
||||
68
app/Livewire/Forms/StoreConnectedAppFrom.php
Normal file
68
app/Livewire/Forms/StoreConnectedAppFrom.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Livewire\Forms;
|
||||
|
||||
use App\Data\ConnectedApp\ConnectedAppData;
|
||||
use App\Services\ConnectedAppService;
|
||||
use Livewire\Form;
|
||||
|
||||
class StoreConnectedAppFrom extends Form
|
||||
{
|
||||
public string $name = '';
|
||||
|
||||
public int $providerId = 0;
|
||||
|
||||
public int $protocolId = 0;
|
||||
|
||||
public int $statusId = 0;
|
||||
|
||||
public string $samlEntityId = '';
|
||||
|
||||
public string $samlAcsUrl = '';
|
||||
|
||||
protected ConnectedAppService $service;
|
||||
|
||||
public function boot(ConnectedAppService $service): void
|
||||
{
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dynamic validation rules for the form.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'name' => 'required|string|max:255|min:3',
|
||||
'providerId' => 'required|numeric|min:1|exists:connection_providers,id',
|
||||
'protocolId' => 'required|numeric|min:1|exists:connection_protocols,id',
|
||||
'statusId' => 'required|numeric|min:1|exists:connection_statuses,id',
|
||||
];
|
||||
|
||||
$protocol = \App\Models\ConnectionProtocol::find($this->protocolId);
|
||||
if ($protocol && 'saml' === mb_strtolower($protocol->name)) {
|
||||
$rules['samlEntityId'] = 'required|string|min:3';
|
||||
$rules['samlAcsUrl'] = 'required|url';
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* This sets the form to edit mode, after fetching the connected app of
|
||||
* the given id.
|
||||
*/
|
||||
public function set(ConnectedAppData $data): void
|
||||
{
|
||||
$this->name = $data->name;
|
||||
$this->protocolId = $data->protocolId;
|
||||
$this->providerId = $data->providerId;
|
||||
$this->statusId = $data->statusId;
|
||||
$this->samlEntityId = $data->settings['saml']['entity_id'] ?? '';
|
||||
$this->samlAcsUrl = $data->settings['saml']['acs_url'] ?? '';
|
||||
}
|
||||
}
|
||||
50
app/Livewire/Forms/StoreConnectionProviderForm.php
Normal file
50
app/Livewire/Forms/StoreConnectionProviderForm.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Livewire\Forms;
|
||||
|
||||
use App\Data\ConnectedApp\ConnectionProviderData;
|
||||
use App\Models\ConnectionProvider;
|
||||
use Closure;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Form;
|
||||
|
||||
class StoreConnectionProviderForm extends Form
|
||||
{
|
||||
public string $name = '';
|
||||
|
||||
public ?int $connectionProviderId = null;
|
||||
|
||||
public function set(ConnectionProviderData $provider): void
|
||||
{
|
||||
$this->connectionProviderId = $provider->id;
|
||||
$this->name = $provider->name;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
'min:3',
|
||||
'max:255',
|
||||
function (string $attribute, mixed $value, Closure $fail): void {
|
||||
$slug = Str::slug($value);
|
||||
|
||||
$query = ConnectionProvider::query()
|
||||
->where('slug', $slug);
|
||||
|
||||
if ($this->connectionProviderId) {
|
||||
$query->where('id', '!=', $this->connectionProviderId);
|
||||
}
|
||||
|
||||
if ($query->exists()) {
|
||||
$fail("The name '{$value}' is already taken.");
|
||||
}
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,8 @@ final class DeleteUserForm extends Component
|
||||
|
||||
public string $password = '';
|
||||
|
||||
public bool $confirmingUserDeletion = false;
|
||||
|
||||
/**
|
||||
* Delete the currently authenticated user.
|
||||
*/
|
||||
|
||||
@ -8,8 +8,7 @@
|
||||
use Flux\Flux;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Attributes\{Computed, Title};
|
||||
use Livewire\Component;
|
||||
|
||||
#[Title('Profile settings')]
|
||||
@ -71,13 +70,18 @@ public function resendVerificationNotification(): void
|
||||
#[Computed]
|
||||
public function hasUnverifiedEmail(): bool
|
||||
{
|
||||
return Auth::user() instanceof MustVerifyEmail && ! Auth::user()->hasVerifiedEmail();
|
||||
/** @var \App\Models\User|MustVerifyEmail|null $user */
|
||||
$user = Auth::user();
|
||||
|
||||
return $user instanceof MustVerifyEmail && ! $user->hasVerifiedEmail();
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function showDeleteUser(): bool
|
||||
{
|
||||
return ! Auth::user() instanceof MustVerifyEmail
|
||||
|| (Auth::user() instanceof MustVerifyEmail && Auth::user()->hasVerifiedEmail());
|
||||
/** @var \App\Models\User|MustVerifyEmail|null $user */
|
||||
$user = Auth::user();
|
||||
|
||||
return ! $user instanceof MustVerifyEmail || $user->hasVerifiedEmail();
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,15 +9,9 @@
|
||||
use Flux\Flux;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Fortify\Actions\ConfirmTwoFactorAuthentication;
|
||||
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
|
||||
use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
|
||||
use Laravel\Fortify\Features;
|
||||
use Laravel\Fortify\Fortify;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Laravel\Fortify\Actions\{ConfirmTwoFactorAuthentication, DisableTwoFactorAuthentication, EnableTwoFactorAuthentication};
|
||||
use Laravel\Fortify\{Features, Fortify};
|
||||
use Livewire\Attributes\{Computed, Locked, Title, Validate};
|
||||
use Livewire\Component;
|
||||
|
||||
#[Title('Security settings')]
|
||||
|
||||
39
app/Models/AppRole.php
Normal file
39
app/Models/AppRole.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\{Relations\BelongsTo, Relations\Pivot};
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $app_id
|
||||
* @property int $role_id
|
||||
* @property string $duration Validity of the role
|
||||
*/
|
||||
#[Fillable(['app_id', 'role_id', 'duration'])]
|
||||
class AppRole extends Pivot
|
||||
{
|
||||
public $timestamps = false;
|
||||
|
||||
protected $table = 'app_roles';
|
||||
|
||||
/**
|
||||
* @return BelongsTo<ConnectedApp, $this>
|
||||
*/
|
||||
public function app(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ConnectedApp::class, 'app_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Role, $this>
|
||||
*/
|
||||
public function role(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Role::class, 'role_id');
|
||||
}
|
||||
}
|
||||
90
app/Models/ConnectedApp.php
Normal file
90
app/Models/ConnectedApp.php
Normal file
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\{Model, Relations\BelongsTo, Relations\BelongsToMany, SoftDeletes};
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Spatie\Activitylog\Models\Concerns\LogsActivity;
|
||||
use Spatie\Activitylog\Support\LogOptions;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property AppRole $pivot
|
||||
*/
|
||||
#[Fillable([
|
||||
'name',
|
||||
'slug',
|
||||
'connection_protocol_id',
|
||||
'connection_provider_id',
|
||||
'connection_status_id',
|
||||
'settings',
|
||||
])]
|
||||
class ConnectedApp extends Model
|
||||
{
|
||||
use HasFactory, LogsActivity, SoftDeletes;
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'settings' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasOne<ConnectionProtocol, $this>
|
||||
*/
|
||||
public function protocol(): HasOne
|
||||
{
|
||||
return $this->hasOne(ConnectionProtocol::class, 'id', 'connection_protocol_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<ConnectionProvider, $this>
|
||||
*/
|
||||
public function provider(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ConnectionProvider::class, 'connection_provider_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasOne<ConnectionStatus, $this>
|
||||
*/
|
||||
public function status(): HasOne
|
||||
{
|
||||
return $this->hasOne(ConnectionStatus::class, 'id', 'connection_status_id');
|
||||
}
|
||||
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logFillable() // Log all fillable attributes
|
||||
->logOnlyDirty() // Log only attributes that actually changed
|
||||
->dontLogIfAttributesChangedOnly(['updated_at']) // Skip if only updated_at changed
|
||||
->dontLogEmptyChanges() // Prevent saving logs with no changed attributes
|
||||
->setDescriptionForEvent(fn (
|
||||
string $eventName
|
||||
) => ":causer.name {$eventName} connected app ':subject.name'");
|
||||
}
|
||||
|
||||
public function roles(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
Role::class,
|
||||
'app_roles',
|
||||
'app_id',
|
||||
'role_id',
|
||||
)
|
||||
->using(AppRole::class)
|
||||
->withPivot('duration');
|
||||
}
|
||||
}
|
||||
17
app/Models/ConnectionProtocol.php
Normal file
17
app/Models/ConnectionProtocol.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\{Model, SoftDeletes};
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property string $slug
|
||||
*/
|
||||
final class ConnectionProtocol extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
}
|
||||
33
app/Models/ConnectionProvider.php
Normal file
33
app/Models/ConnectionProvider.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\{Model, Relations\HasMany, SoftDeletes};
|
||||
use Spatie\Activitylog\Models\Concerns\LogsActivity;
|
||||
use Spatie\Activitylog\Support\LogOptions;
|
||||
|
||||
#[Fillable(['name', 'slug'])]
|
||||
final class ConnectionProvider extends Model
|
||||
{
|
||||
use LogsActivity, SoftDeletes;
|
||||
|
||||
public function connectedApps(): HasMany
|
||||
{
|
||||
return $this->hasMany(ConnectedApp::class);
|
||||
}
|
||||
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logFillable() // Log all fillable attributes
|
||||
->logOnlyDirty() // Log only attributes that actually changed
|
||||
->dontLogIfAttributesChangedOnly(['updated_at']) // Skip if only updated_at changed
|
||||
->dontLogEmptyChanges() // Prevent saving logs with no changed attributes
|
||||
->setDescriptionForEvent(fn (
|
||||
string $eventName
|
||||
) => ":causer.name {$eventName} connection provider ':subject.name'");
|
||||
}
|
||||
}
|
||||
14
app/Models/ConnectionStatus.php
Normal file
14
app/Models/ConnectionStatus.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Table;
|
||||
use Illuminate\Database\Eloquent\{Model, SoftDeletes};
|
||||
|
||||
#[Table('connection_statuses')]
|
||||
class ConnectionStatus extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
}
|
||||
39
app/Models/RestrictedUserPermission.php
Normal file
39
app/Models/RestrictedUserPermission.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Relations\{BelongsTo, Pivot};
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
#[Fillable(['user_id', 'role_id', 'permission_id'])]
|
||||
class RestrictedUserPermission extends Pivot
|
||||
{
|
||||
protected $table = 'restricted_user_permissions';
|
||||
|
||||
/**
|
||||
* @return BelongsTo<User, $this>
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Role, $this>
|
||||
*/
|
||||
public function role(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Role::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Permission, $this>
|
||||
*/
|
||||
public function permission(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Permission::class);
|
||||
}
|
||||
}
|
||||
69
app/Models/Role.php
Normal file
69
app/Models/Role.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Spatie\Activitylog\Models\Concerns\LogsActivity;
|
||||
use Spatie\Activitylog\Support\LogOptions;
|
||||
use Spatie\Permission\Models\{Permission, Role as SpatieRole};
|
||||
|
||||
/**
|
||||
* @property int $priority
|
||||
* @property AppRole $pivot
|
||||
* @property ConnectedApp $apps
|
||||
*
|
||||
* @method static Builder<Role> visible()
|
||||
*/
|
||||
#[Fillable(['name', 'guard_name', 'priority'])]
|
||||
class Role extends SpatieRole
|
||||
{
|
||||
use LogsActivity;
|
||||
|
||||
public function apps(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
ConnectedApp::class,
|
||||
'app_roles',
|
||||
'role_id',
|
||||
'app_id'
|
||||
)
|
||||
->using(AppRole::class)
|
||||
->withPivot('id', 'duration');
|
||||
}
|
||||
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logOnly(['name', 'priority'])
|
||||
->logOnlyDirty()
|
||||
->useLogName('role_management');
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the Spatie permissions relationship to provide precise type-hinting for Larastan.
|
||||
*
|
||||
* @return BelongsToMany<Permission, Role>
|
||||
*/
|
||||
public function permissions(): BelongsToMany
|
||||
{
|
||||
return parent::permissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include roles with priority strictly greater than the logged in user's priority (lower privilege).
|
||||
*/
|
||||
public function scopeVisible(Builder $query): Builder
|
||||
{
|
||||
if (! auth()->check()) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
$userPriority = auth()->user()?->roles()->min('priority') ?? 999999;
|
||||
|
||||
return $query->where('priority', '>', $userPriority);
|
||||
}
|
||||
}
|
||||
111
app/Models/Ticket.php
Normal file
111
app/Models/Ticket.php
Normal file
@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\{Model, SoftDeletes};
|
||||
use Illuminate\Database\Eloquent\Relations\{BelongsTo, BelongsToMany, HasMany};
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
#[Fillable([
|
||||
'ticket_id',
|
||||
'user_id',
|
||||
'connected_app_id',
|
||||
'issue_category',
|
||||
'description',
|
||||
'status_id',
|
||||
'assigned_user_id',
|
||||
])]
|
||||
class Ticket extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(function (Ticket $ticket): void {
|
||||
if (empty($ticket->ticket_id)) {
|
||||
do {
|
||||
$ticketId = 'TKT-'.mb_strtoupper(Str::random(8));
|
||||
} while (static::query()->where('ticket_id', $ticketId)->exists());
|
||||
|
||||
$ticket->ticket_id = $ticketId;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<User, $this>
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<ConnectedApp, $this>
|
||||
*/
|
||||
public function connectedApp(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ConnectedApp::class, 'connected_app_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<TicketStatus, $this>
|
||||
*/
|
||||
public function status(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(TicketStatus::class, 'status_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<User, $this>
|
||||
*/
|
||||
public function assignedUser(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'assigned_user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsToMany<Role, $this>
|
||||
*/
|
||||
public function notifiedRoles(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
Role::class,
|
||||
'ticket_notified_roles',
|
||||
'ticket_id',
|
||||
'role_id'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsToMany<User, $this>
|
||||
*/
|
||||
public function notifiedUsers(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
User::class,
|
||||
'ticket_notified_users',
|
||||
'ticket_id',
|
||||
'user_id'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<TicketAttachment, $this>
|
||||
*/
|
||||
public function attachments(): HasMany
|
||||
{
|
||||
return $this->hasMany(TicketAttachment::class, 'ticket_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<TicketReply, $this>
|
||||
*/
|
||||
public function replies(): HasMany
|
||||
{
|
||||
return $this->hasMany(TicketReply::class, 'ticket_id');
|
||||
}
|
||||
}
|
||||
30
app/Models/TicketAttachment.php
Normal file
30
app/Models/TicketAttachment.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
#[Fillable([
|
||||
'ticket_id',
|
||||
'file_path',
|
||||
'file_name',
|
||||
'file_size',
|
||||
'mime_type',
|
||||
])]
|
||||
class TicketAttachment extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Ticket, $this>
|
||||
*/
|
||||
public function ticket(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Ticket::class, 'ticket_id');
|
||||
}
|
||||
}
|
||||
40
app/Models/TicketReply.php
Normal file
40
app/Models/TicketReply.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
#[Fillable(['ticket_id', 'user_id', 'message'])]
|
||||
class TicketReply extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'ticket_replies';
|
||||
|
||||
/**
|
||||
* Get the ticket that this reply belongs to.
|
||||
*
|
||||
* @return BelongsTo<Ticket, $this>
|
||||
*/
|
||||
public function ticket(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Ticket::class, 'ticket_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user who posted this reply.
|
||||
*
|
||||
* @return BelongsTo<User, $this>
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
}
|
||||
15
app/Models/TicketStatus.php
Normal file
15
app/Models/TicketStatus.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
#[Fillable(['name', 'slug'])]
|
||||
class TicketStatus extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user