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
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
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
|
/.vscode
|
||||||
/.zed
|
/.zed
|
||||||
/storage/framework/views/*
|
/storage/framework/views/*
|
||||||
|
laradumps.yaml
|
||||||
|
|||||||
@ -15,6 +15,7 @@ ## Foundational Context
|
|||||||
- laravel/prompts (PROMPTS) - v0
|
- laravel/prompts (PROMPTS) - v0
|
||||||
- livewire/flux (FLUXUI_FREE) - v2
|
- livewire/flux (FLUXUI_FREE) - v2
|
||||||
- livewire/livewire (LIVEWIRE) - v4
|
- livewire/livewire (LIVEWIRE) - v4
|
||||||
|
- larastan/larastan (LARASTAN) - v3
|
||||||
- laravel/boost (BOOST) - v2
|
- laravel/boost (BOOST) - v2
|
||||||
- laravel/mcp (MCP) - v0
|
- laravel/mcp (MCP) - v0
|
||||||
- laravel/pail (PAIL) - v1
|
- laravel/pail (PAIL) - v1
|
||||||
|
|||||||
@ -15,6 +15,7 @@ ## Foundational Context
|
|||||||
- laravel/prompts (PROMPTS) - v0
|
- laravel/prompts (PROMPTS) - v0
|
||||||
- livewire/flux (FLUXUI_FREE) - v2
|
- livewire/flux (FLUXUI_FREE) - v2
|
||||||
- livewire/livewire (LIVEWIRE) - v4
|
- livewire/livewire (LIVEWIRE) - v4
|
||||||
|
- larastan/larastan (LARASTAN) - v3
|
||||||
- laravel/boost (BOOST) - v2
|
- laravel/boost (BOOST) - v2
|
||||||
- laravel/mcp (MCP) - v0
|
- laravel/mcp (MCP) - v0
|
||||||
- laravel/pail (PAIL) - v1
|
- 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;
|
namespace App\Actions\Fortify;
|
||||||
|
|
||||||
use App\Concerns\PasswordValidationRules;
|
use App\Concerns\{PasswordValidationRules, ProfileValidationRules};
|
||||||
use App\Concerns\ProfileValidationRules;
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
use Laravel\Fortify\Contracts\CreatesNewUsers;
|
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.
|
* 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
|
protected function passwordRules(): array
|
||||||
{
|
{
|
||||||
|
|||||||
@ -7,13 +7,14 @@
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Contracts\Validation\ValidationRule;
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Validation\Rules\Unique;
|
||||||
|
|
||||||
trait ProfileValidationRules
|
trait ProfileValidationRules
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get the validation rules used to validate user profiles.
|
* 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
|
protected function profileRules(?int $userId = null): array
|
||||||
{
|
{
|
||||||
@ -36,7 +37,7 @@ protected function nameRules(): array
|
|||||||
/**
|
/**
|
||||||
* Get the validation rules used to validate user emails.
|
* 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
|
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;
|
namespace App\Data\Ui\ManageRoles;
|
||||||
|
|
||||||
use App\Data\Users\UsersData;
|
use App\Data\Users\UserData;
|
||||||
use App\Enums\RolesStatus;
|
use App\Enums\RolesStatusEnum;
|
||||||
use Livewire\Wireable;
|
use Spatie\LaravelData\Attributes\DataCollectionOf;
|
||||||
use Spatie\LaravelData\Concerns\WireableData;
|
use Spatie\LaravelData\{Data, DataCollection};
|
||||||
use Spatie\LaravelData\Data;
|
|
||||||
|
|
||||||
final class RoleData extends Data implements Wireable
|
final class RoleData extends Data
|
||||||
{
|
{
|
||||||
use WireableData;
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public string $name,
|
public string $name,
|
||||||
public UsersData $users,
|
|
||||||
public ServiceCollection $services,
|
/** @var DataCollection<int, UserData> */
|
||||||
public RolesStatus $status,
|
#[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;
|
namespace App\Data\Ui\ManageRoles;
|
||||||
|
|
||||||
use Livewire\Wireable;
|
|
||||||
use Spatie\LaravelData\Concerns\WireableData;
|
|
||||||
use Spatie\LaravelData\Data;
|
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;
|
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
|
final class UserData extends Data
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public string $name,
|
public string $name,
|
||||||
public string $email,
|
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;
|
namespace App\Enums;
|
||||||
|
|
||||||
enum ConnectionStatus: string
|
use App\Concerns\EnumValuesAsArray;
|
||||||
|
|
||||||
|
enum ConnectionStatusEnum: string
|
||||||
{
|
{
|
||||||
|
use EnumValuesAsArray;
|
||||||
|
|
||||||
case Connected = 'connected';
|
case Connected = 'connected';
|
||||||
case Disconnected = 'disconnected';
|
case Disconnected = 'disconnected';
|
||||||
case Pending = 'pending';
|
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;
|
namespace App\Enums;
|
||||||
|
|
||||||
enum RolesStatus: string
|
enum RolesStatusEnum: string
|
||||||
{
|
{
|
||||||
case Active = 'active';
|
case Active = 'active';
|
||||||
case Inactive = 'inactive';
|
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;
|
namespace App\Livewire\Actions;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\{Auth, Session};
|
||||||
use Illuminate\Support\Facades\Session;
|
|
||||||
|
|
||||||
final class Logout
|
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 string $password = '';
|
||||||
|
|
||||||
|
public bool $confirmingUserDeletion = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete the currently authenticated user.
|
* Delete the currently authenticated user.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -8,8 +8,7 @@
|
|||||||
use Flux\Flux;
|
use Flux\Flux;
|
||||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Livewire\Attributes\Computed;
|
use Livewire\Attributes\{Computed, Title};
|
||||||
use Livewire\Attributes\Title;
|
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
#[Title('Profile settings')]
|
#[Title('Profile settings')]
|
||||||
@ -71,13 +70,18 @@ public function resendVerificationNotification(): void
|
|||||||
#[Computed]
|
#[Computed]
|
||||||
public function hasUnverifiedEmail(): bool
|
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]
|
#[Computed]
|
||||||
public function showDeleteUser(): bool
|
public function showDeleteUser(): bool
|
||||||
{
|
{
|
||||||
return ! Auth::user() instanceof MustVerifyEmail
|
/** @var \App\Models\User|MustVerifyEmail|null $user */
|
||||||
|| (Auth::user() instanceof MustVerifyEmail && Auth::user()->hasVerifiedEmail());
|
$user = Auth::user();
|
||||||
|
|
||||||
|
return ! $user instanceof MustVerifyEmail || $user->hasVerifiedEmail();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,15 +9,9 @@
|
|||||||
use Flux\Flux;
|
use Flux\Flux;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
use Laravel\Fortify\Actions\ConfirmTwoFactorAuthentication;
|
use Laravel\Fortify\Actions\{ConfirmTwoFactorAuthentication, DisableTwoFactorAuthentication, EnableTwoFactorAuthentication};
|
||||||
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
|
use Laravel\Fortify\{Features, Fortify};
|
||||||
use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
|
use Livewire\Attributes\{Computed, Locked, Title, Validate};
|
||||||
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 Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
#[Title('Security settings')]
|
#[Title('Security settings')]
|
||||||
@ -102,7 +96,7 @@ public function enable(EnableTwoFactorAuthentication $enableTwoFactorAuthenticat
|
|||||||
{
|
{
|
||||||
$enableTwoFactorAuthentication(auth()->user());
|
$enableTwoFactorAuthentication(auth()->user());
|
||||||
|
|
||||||
if ( ! $this->requiresConfirmation) {
|
if (! $this->requiresConfirmation) {
|
||||||
$this->twoFactorEnabled = auth()->user()->hasEnabledTwoFactorAuthentication();
|
$this->twoFactorEnabled = auth()->user()->hasEnabledTwoFactorAuthentication();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,7 +170,7 @@ public function closeModal(): void
|
|||||||
|
|
||||||
$this->resetErrorBag();
|
$this->resetErrorBag();
|
||||||
|
|
||||||
if ( ! $this->requiresConfirmation) {
|
if (! $this->requiresConfirmation) {
|
||||||
$this->twoFactorEnabled = auth()->user()->hasEnabledTwoFactorAuthentication();
|
$this->twoFactorEnabled = auth()->user()->hasEnabledTwoFactorAuthentication();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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