Initial commit: CABM-IT (ItGlue-style MSP documentation platform)
Laravel 13 + Livewire/Volt app for managing client organizations, documents, passwords, flexible assets, virtual machines, contacts, domains, todos, shortcuts, and role-based user administration.
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Fortify\Contracts\CreatesNewUsers;
|
||||
|
||||
class CreateNewUser implements CreatesNewUsers
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and create a newly registered user.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function create(array $input): User
|
||||
{
|
||||
Validator::make($input, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique(User::class),
|
||||
],
|
||||
'password' => $this->passwordRules(),
|
||||
])->validate();
|
||||
|
||||
return User::create([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
'password' => Hash::make($input['password']),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
trait PasswordValidationRules
|
||||
{
|
||||
/**
|
||||
* Get the validation rules used to validate passwords.
|
||||
*
|
||||
* @return array<int, Rule|array<mixed>|string>
|
||||
*/
|
||||
protected function passwordRules(): array
|
||||
{
|
||||
return ['required', 'string', Password::default(), 'confirmed'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Fortify\Contracts\ResetsUserPasswords;
|
||||
|
||||
class ResetUserPassword implements ResetsUserPasswords
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and reset the user's forgotten password.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function reset(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'password' => $this->passwordRules(),
|
||||
])->validate();
|
||||
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($input['password']),
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
|
||||
|
||||
class UpdateUserPassword implements UpdatesUserPasswords
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and update the user's password.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function update(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'current_password' => ['required', 'string', 'current_password:web'],
|
||||
'password' => $this->passwordRules(),
|
||||
], [
|
||||
'current_password.current_password' => __('The provided password does not match your current password.'),
|
||||
])->validateWithBag('updatePassword');
|
||||
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($input['password']),
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
|
||||
|
||||
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
||||
{
|
||||
/**
|
||||
* Validate and update the given user's profile information.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function update(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique('users')->ignore($user->id),
|
||||
],
|
||||
])->validateWithBag('updateProfileInformation');
|
||||
|
||||
if ($input['email'] !== $user->email &&
|
||||
$user instanceof MustVerifyEmail) {
|
||||
$this->updateVerifiedUser($user, $input);
|
||||
} else {
|
||||
$user->forceFill([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the given verified user's profile information.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
protected function updateVerifiedUser(User $user, array $input): void
|
||||
{
|
||||
$user->forceFill([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
'email_verified_at' => null,
|
||||
])->save();
|
||||
|
||||
$user->sendEmailVerificationNotification();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Casts;
|
||||
|
||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
/**
|
||||
* AES-256-GCM encrypt/decrypt via the app's Crypt facade.
|
||||
*
|
||||
* Simplification: uses the single app-wide key as the encryption key (KEK == DEK).
|
||||
* Production hardening: wrap a per-organization DEK with a KMS-held KEK (envelope
|
||||
* encryption) so a single leaked app key doesn't expose every tenant's secrets.
|
||||
*/
|
||||
class EncryptedField implements CastsAttributes
|
||||
{
|
||||
public function get($model, string $key, $value, array $attributes)
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Crypt::decryptString($value);
|
||||
} catch (\Illuminate\Contracts\Encryption\DecryptException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function set($model, string $key, $value, array $attributes)
|
||||
{
|
||||
return $value === null ? null : Crypt::encryptString($value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
/**
|
||||
* Implemented by any model that can appear in the Relationship Mapping panel
|
||||
* (related_items). Lets the panel render a link + label without a type switch.
|
||||
*/
|
||||
interface Linkable
|
||||
{
|
||||
public function linkableKind(): string;
|
||||
|
||||
public function linkableLabel(): string;
|
||||
|
||||
public function linkableUrl(): string;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Document;
|
||||
use App\Models\Organization;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class DocumentDownloadController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, Organization $organization, Document $document): StreamedResponse
|
||||
{
|
||||
abort_unless($document->organization_id === $organization->id, 404);
|
||||
abort_unless($document->isUpload() && $document->file_path, 404);
|
||||
|
||||
if ($request->boolean('inline')) {
|
||||
return Storage::response($document->file_path, $document->name);
|
||||
}
|
||||
|
||||
return Storage::download($document->file_path, $document->name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Document;
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class OnlyOfficeController extends Controller
|
||||
{
|
||||
public function download(Request $request, Document $document): StreamedResponse
|
||||
{
|
||||
// Relative signature: the download URL is built with a host the document
|
||||
// server can reach (host.docker.internal, ...), which may differ from
|
||||
// the host the browser/this server sees — only path+query are signed.
|
||||
abort_unless($request->hasValidRelativeSignature(), 403);
|
||||
abort_unless($document->isOffice() && $document->file_path, 404);
|
||||
|
||||
return Storage::response($document->file_path);
|
||||
}
|
||||
|
||||
public function callback(Request $request, Document $document): JsonResponse
|
||||
{
|
||||
abort_unless($document->isOffice(), 404);
|
||||
|
||||
$payload = $this->verifiedPayload($request);
|
||||
|
||||
// OnlyOffice callback statuses: 2 = ready to save, 6 = force-saved while editing.
|
||||
if (in_array($payload['status'] ?? null, [2, 6], true) && filled($payload['url'] ?? null)) {
|
||||
$response = Http::get($payload['url']);
|
||||
abort_unless($response->successful(), 502);
|
||||
|
||||
Storage::put($document->file_path, $response->body());
|
||||
|
||||
$document->increment('current_version');
|
||||
$document->update(['office_key' => Str::random(20)]);
|
||||
|
||||
$document->versions()->create([
|
||||
'version' => $document->current_version,
|
||||
'created_by' => $payload['users'][0] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json(['error' => 0]);
|
||||
}
|
||||
|
||||
protected function verifiedPayload(Request $request): array
|
||||
{
|
||||
$secret = config('onlyoffice.jwt_secret');
|
||||
|
||||
if (blank($secret)) {
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
$token = $request->bearerToken()
|
||||
?? $request->input('token')
|
||||
?? str($request->header('Authorization', ''))->after('Bearer ')->toString();
|
||||
|
||||
abort_if(blank($token), SymfonyResponse::HTTP_UNAUTHORIZED);
|
||||
|
||||
$decoded = JWT::decode($token, new Key($secret, 'HS256'));
|
||||
|
||||
return (array) ($decoded->payload ?? $decoded);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\IpUtils;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CheckOrganizationIpAccess
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Closure(Request): (Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$organization = $request->route('organization');
|
||||
|
||||
if (! $organization || empty($organization->allowed_ip_ranges)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if (IpUtils::checkIp($request->ip(), $organization->allowed_ip_ranges)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
abort(403, 'Your IP address is not permitted to access this organization.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
#[Fillable([
|
||||
'organization_id', 'location_id', 'first_name', 'last_name',
|
||||
'title', 'email', 'phone', 'mobile', 'is_important', 'notes',
|
||||
])]
|
||||
class Contact extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_important' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
protected function fullName(): Attribute
|
||||
{
|
||||
return Attribute::get(fn () => trim("{$this->first_name} {$this->last_name}"));
|
||||
}
|
||||
|
||||
public function organization()
|
||||
{
|
||||
return $this->belongsTo(Organization::class);
|
||||
}
|
||||
|
||||
public function location()
|
||||
{
|
||||
return $this->belongsTo(Location::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Contracts\Linkable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
|
||||
#[Fillable(['organization_id', 'folder_id', 'name', 'type', 'is_folder', 'content', 'file_path', 'office_extension', 'office_key', 'current_version', 'created_by'])]
|
||||
class Document extends Model implements Linkable
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_folder' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function isOffice(): bool
|
||||
{
|
||||
return $this->type === 'office';
|
||||
}
|
||||
|
||||
public function isMarkdown(): bool
|
||||
{
|
||||
return $this->type === 'markdown';
|
||||
}
|
||||
|
||||
public function isUpload(): bool
|
||||
{
|
||||
return $this->type === 'upload';
|
||||
}
|
||||
|
||||
public function renderedMarkdown(): string
|
||||
{
|
||||
return (new CommonMarkConverter())->convert($this->content ?? '')->getContent();
|
||||
}
|
||||
|
||||
public function isFolder(): bool
|
||||
{
|
||||
return $this->is_folder || $this->children()->exists();
|
||||
}
|
||||
|
||||
public function organization()
|
||||
{
|
||||
return $this->belongsTo(Organization::class);
|
||||
}
|
||||
|
||||
public function folder()
|
||||
{
|
||||
return $this->belongsTo(Document::class, 'folder_id');
|
||||
}
|
||||
|
||||
public function children()
|
||||
{
|
||||
return $this->hasMany(Document::class, 'folder_id');
|
||||
}
|
||||
|
||||
public function versions()
|
||||
{
|
||||
return $this->hasMany(DocumentVersion::class)->orderByDesc('version');
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function relatedItems()
|
||||
{
|
||||
return RelatedItem::forItem($this);
|
||||
}
|
||||
|
||||
public function linkableKind(): string
|
||||
{
|
||||
return 'Document';
|
||||
}
|
||||
|
||||
public function linkableLabel(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function linkableUrl(): string
|
||||
{
|
||||
return route('organizations.documents.edit', [$this->organization_id, $this]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class DocumentVersion extends Model
|
||||
{
|
||||
const UPDATED_AT = null;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function document()
|
||||
{
|
||||
return $this->belongsTo(Document::class);
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
#[Fillable(['organization_id', 'name', 'registrar', 'nameservers', 'expires_at', 'auto_renew', 'last_checked_at', 'notes'])]
|
||||
class Domain extends Model
|
||||
{
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'expires_at' => 'date',
|
||||
'auto_renew' => 'boolean',
|
||||
'last_checked_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function organization()
|
||||
{
|
||||
return $this->belongsTo(Organization::class);
|
||||
}
|
||||
|
||||
public function sslCertificates()
|
||||
{
|
||||
return $this->hasMany(SslCertificate::class);
|
||||
}
|
||||
|
||||
public function tld(): ?string
|
||||
{
|
||||
$parts = explode('.', $this->name);
|
||||
|
||||
return count($parts) > 1 ? '.'.end($parts) : null;
|
||||
}
|
||||
|
||||
public function expiresInDays(): ?int
|
||||
{
|
||||
return $this->expires_at ? now()->startOfDay()->diffInDays($this->expires_at, false) : null;
|
||||
}
|
||||
|
||||
public function expiryStatus(): string
|
||||
{
|
||||
$days = $this->expiresInDays();
|
||||
|
||||
return match (true) {
|
||||
$days === null => 'unknown',
|
||||
$days < 0 => 'expired',
|
||||
$days <= 30 => 'soon',
|
||||
default => 'ok',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Contracts\Linkable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
#[Fillable(['flexible_asset_type_id', 'organization_id', 'name', 'data', 'is_archived', 'created_by'])]
|
||||
class FlexibleAsset extends Model implements Linkable
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'data' => 'array',
|
||||
'is_archived' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function type()
|
||||
{
|
||||
return $this->belongsTo(FlexibleAssetType::class, 'flexible_asset_type_id');
|
||||
}
|
||||
|
||||
public function organization()
|
||||
{
|
||||
return $this->belongsTo(Organization::class);
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function relatedItems()
|
||||
{
|
||||
return RelatedItem::forItem($this);
|
||||
}
|
||||
|
||||
public function logs()
|
||||
{
|
||||
return $this->hasMany(FlexibleAssetLog::class)->orderByDesc('created_at');
|
||||
}
|
||||
|
||||
public function linkableKind(): string
|
||||
{
|
||||
return $this->type?->name ?? 'Flexible Asset';
|
||||
}
|
||||
|
||||
public function linkableLabel(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function linkableUrl(): string
|
||||
{
|
||||
return route('organizations.assets.edit', [$this->organization_id, $this->flexible_asset_type_id, $this]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
#[Fillable([
|
||||
'flexible_asset_type_id', 'name', 'label', 'field_type',
|
||||
'options', 'is_required', 'show_in_list', 'sort_order',
|
||||
])]
|
||||
class FlexibleAssetField extends Model
|
||||
{
|
||||
const FIELD_TYPES = ['text', 'textarea', 'password', 'checkbox', 'date', 'tag', 'select', 'related_asset'];
|
||||
|
||||
/**
|
||||
* Built-in record lists a "related_asset" field can point to.
|
||||
* Keys are stored in options.target; values map to an Organization relation
|
||||
* name and the attribute used as the display label.
|
||||
*/
|
||||
const RELATED_TARGETS = [
|
||||
'contact' => ['relation' => 'contacts', 'label' => 'full_name'],
|
||||
'location' => ['relation' => 'locations', 'label' => 'name'],
|
||||
'domain' => ['relation' => 'domains', 'label' => 'name'],
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'options' => 'array',
|
||||
'is_required' => 'boolean',
|
||||
'show_in_list' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function type()
|
||||
{
|
||||
return $this->belongsTo(FlexibleAssetType::class, 'flexible_asset_type_id');
|
||||
}
|
||||
|
||||
public function relatedTarget(): ?string
|
||||
{
|
||||
return $this->options['target'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records available for this field's related_asset target, scoped to the organization.
|
||||
* @return Collection<int, array{id: int, label: string}>
|
||||
*/
|
||||
public function relatedOptions(Organization $organization): Collection
|
||||
{
|
||||
$target = $this->relatedTarget();
|
||||
|
||||
if (! $target) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
if (str_starts_with($target, 'flexible_asset:')) {
|
||||
$typeId = (int) substr($target, strlen('flexible_asset:'));
|
||||
|
||||
return $organization->flexibleAssets()
|
||||
->where('flexible_asset_type_id', $typeId)
|
||||
->orderBy('name')
|
||||
->get()
|
||||
->map(fn (FlexibleAsset $asset) => ['id' => $asset->id, 'label' => $asset->name]);
|
||||
}
|
||||
|
||||
$config = self::RELATED_TARGETS[$target] ?? null;
|
||||
|
||||
if (! $config) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return $organization->{$config['relation']}()
|
||||
->orderBy($config['label'] === 'full_name' ? 'last_name' : $config['label'])
|
||||
->get()
|
||||
->map(fn (Model $record) => ['id' => $record->id, 'label' => (string) $record->{$config['label']}]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class FlexibleAssetLog extends Model
|
||||
{
|
||||
const UPDATED_AT = null;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function flexibleAsset()
|
||||
{
|
||||
return $this->belongsTo(FlexibleAsset::class);
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
#[Fillable(['name', 'icon', 'description', 'enabled_for_all_organizations'])]
|
||||
class FlexibleAssetType extends Model
|
||||
{
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'enabled_for_all_organizations' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function fields()
|
||||
{
|
||||
return $this->hasMany(FlexibleAssetField::class)->orderBy('sort_order');
|
||||
}
|
||||
|
||||
public function assets()
|
||||
{
|
||||
return $this->hasMany(FlexibleAsset::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
#[Fillable([
|
||||
'organization_id', 'name', 'address_line_1', 'address_line_2',
|
||||
'city', 'region', 'postal_code', 'country', 'phone', 'is_primary',
|
||||
])]
|
||||
class Location extends Model
|
||||
{
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_primary' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function organization()
|
||||
{
|
||||
return $this->belongsTo(Organization::class);
|
||||
}
|
||||
|
||||
public function contacts()
|
||||
{
|
||||
return $this->hasMany(Contact::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
#[Fillable(['name', 'slug', 'org_type', 'status', 'description', 'allowed_ip_ranges', 'logo_path'])]
|
||||
class Organization extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'allowed_ip_ranges' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
protected function logoUrl(): Attribute
|
||||
{
|
||||
return Attribute::get(
|
||||
fn () => $this->logo_path ? Storage::disk('public')->url($this->logo_path) : asset('images/default-organization-logo.svg')
|
||||
);
|
||||
}
|
||||
|
||||
public function users()
|
||||
{
|
||||
return $this->belongsToMany(User::class);
|
||||
}
|
||||
|
||||
public function locations()
|
||||
{
|
||||
return $this->hasMany(Location::class);
|
||||
}
|
||||
|
||||
public function contacts()
|
||||
{
|
||||
return $this->hasMany(Contact::class);
|
||||
}
|
||||
|
||||
public function passwords()
|
||||
{
|
||||
return $this->hasMany(Password::class);
|
||||
}
|
||||
|
||||
public function flexibleAssets()
|
||||
{
|
||||
return $this->hasMany(FlexibleAsset::class);
|
||||
}
|
||||
|
||||
public function documents()
|
||||
{
|
||||
return $this->hasMany(Document::class);
|
||||
}
|
||||
|
||||
public function domains()
|
||||
{
|
||||
return $this->hasMany(Domain::class);
|
||||
}
|
||||
|
||||
public function sslCertificates()
|
||||
{
|
||||
return $this->hasMany(SslCertificate::class);
|
||||
}
|
||||
|
||||
public function virtualMachines()
|
||||
{
|
||||
return $this->hasMany(VirtualMachine::class);
|
||||
}
|
||||
|
||||
public function logEntries()
|
||||
{
|
||||
return $this->hasMany(OrganizationLogEntry::class)->orderByDesc('created_at');
|
||||
}
|
||||
|
||||
public function todos()
|
||||
{
|
||||
return $this->hasMany(Todo::class);
|
||||
}
|
||||
|
||||
public function shortcuts()
|
||||
{
|
||||
return $this->hasMany(Shortcut::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class OrganizationLogEntry extends Model
|
||||
{
|
||||
const UPDATED_AT = null;
|
||||
|
||||
const CATEGORIES = [
|
||||
'project' => 'Projet en cours',
|
||||
'attention' => "Point d'attention",
|
||||
'intervention' => 'Intervention prévue',
|
||||
'info' => 'Information générale',
|
||||
];
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function organization()
|
||||
{
|
||||
return $this->belongsTo(Organization::class);
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function categoryLabel(): string
|
||||
{
|
||||
return self::CATEGORIES[$this->category] ?? $this->category;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Casts\EncryptedField;
|
||||
use App\Contracts\Linkable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
#[Fillable([
|
||||
'organization_id', 'resource_type', 'resource_id', 'name', 'username',
|
||||
'password_ciphertext', 'url', 'notes', 'otp_secret', 'is_favorite',
|
||||
'password_updated_at', 'rotation_due_at', 'created_by',
|
||||
])]
|
||||
class Password extends Model implements Linkable
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'password_ciphertext' => EncryptedField::class,
|
||||
'otp_secret' => EncryptedField::class,
|
||||
'is_favorite' => 'boolean',
|
||||
'password_updated_at' => 'datetime',
|
||||
'rotation_due_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function organization()
|
||||
{
|
||||
return $this->belongsTo(Organization::class);
|
||||
}
|
||||
|
||||
public function resource()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function auditLogs()
|
||||
{
|
||||
return $this->hasMany(PasswordAuditLog::class);
|
||||
}
|
||||
|
||||
public function logAction(string $action): void
|
||||
{
|
||||
$this->auditLogs()->create([
|
||||
'user_id' => auth()->id(),
|
||||
'action' => $action,
|
||||
'ip_address' => request()->ip(),
|
||||
'user_agent' => (string) request()->userAgent(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function relatedItems()
|
||||
{
|
||||
return RelatedItem::forItem($this);
|
||||
}
|
||||
|
||||
public function linkableKind(): string
|
||||
{
|
||||
return 'Password';
|
||||
}
|
||||
|
||||
public function linkableLabel(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function linkableUrl(): string
|
||||
{
|
||||
return route('organizations.passwords.edit', [$this->organization_id, $this]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PasswordAuditLog extends Model
|
||||
{
|
||||
const UPDATED_AT = null;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function password()
|
||||
{
|
||||
return $this->belongsTo(Password::class);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
class RelatedItem extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
public function item(): MorphTo
|
||||
{
|
||||
return $this->morphTo(__FUNCTION__, 'item_type', 'item_id');
|
||||
}
|
||||
|
||||
public function relatedItem(): MorphTo
|
||||
{
|
||||
return $this->morphTo(__FUNCTION__, 'related_item_type', 'related_item_id');
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* All edges touching a given model, either as owner or as target (symmetric view).
|
||||
*/
|
||||
public static function forItem(Model $model)
|
||||
{
|
||||
return static::where(function ($query) use ($model) {
|
||||
$query->where('item_type', $model->getMorphClass())->where('item_id', $model->getKey());
|
||||
})->orWhere(function ($query) use ($model) {
|
||||
$query->where('related_item_type', $model->getMorphClass())->where('related_item_id', $model->getKey());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The model on the "other side" of this edge, relative to the given model.
|
||||
*/
|
||||
public function otherItem(Model $relativeTo): ?Model
|
||||
{
|
||||
$isOwner = $this->item_type === $relativeTo->getMorphClass() && (string) $this->item_id === (string) $relativeTo->getKey();
|
||||
|
||||
return $isOwner ? $this->relatedItem : $this->item;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
#[Fillable(['organization_id', 'label', 'url', 'icon', 'created_by'])]
|
||||
class Shortcut extends Model
|
||||
{
|
||||
public function organization()
|
||||
{
|
||||
return $this->belongsTo(Organization::class);
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function isGlobal(): bool
|
||||
{
|
||||
return $this->organization_id === null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
#[Fillable(['organization_id', 'domain_id', 'name', 'issuer', 'issued_at', 'expires_at', 'last_checked_at'])]
|
||||
class SslCertificate extends Model
|
||||
{
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'issued_at' => 'date',
|
||||
'expires_at' => 'date',
|
||||
'last_checked_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function organization()
|
||||
{
|
||||
return $this->belongsTo(Organization::class);
|
||||
}
|
||||
|
||||
public function domain()
|
||||
{
|
||||
return $this->belongsTo(Domain::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Contracts\Linkable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
#[Fillable([
|
||||
'organization_id', 'title', 'description', 'category', 'criticality',
|
||||
'due_date', 'assigned_to', 'is_done', 'completed_at', 'created_by',
|
||||
])]
|
||||
class Todo extends Model implements Linkable
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
const CATEGORIES = [
|
||||
'infra' => 'Infra',
|
||||
'support' => 'Support',
|
||||
'metier' => 'Métier',
|
||||
'autre' => 'Autre',
|
||||
];
|
||||
|
||||
const CRITICALITIES = [
|
||||
'low' => 'Basse',
|
||||
'medium' => 'Moyenne',
|
||||
'high' => 'Haute',
|
||||
'urgent' => 'Urgente',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'due_date' => 'date',
|
||||
'is_done' => 'boolean',
|
||||
'completed_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function organization()
|
||||
{
|
||||
return $this->belongsTo(Organization::class);
|
||||
}
|
||||
|
||||
public function assignee()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'assigned_to');
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function categoryLabel(): string
|
||||
{
|
||||
return self::CATEGORIES[$this->category] ?? $this->category;
|
||||
}
|
||||
|
||||
public function criticalityLabel(): string
|
||||
{
|
||||
return self::CRITICALITIES[$this->criticality] ?? $this->criticality;
|
||||
}
|
||||
|
||||
public function isOverdue(): bool
|
||||
{
|
||||
return ! $this->is_done && $this->due_date && $this->due_date->isPast();
|
||||
}
|
||||
|
||||
public function linkableKind(): string
|
||||
{
|
||||
return 'Todo';
|
||||
}
|
||||
|
||||
public function linkableLabel(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function linkableUrl(): string
|
||||
{
|
||||
return route('organizations.todos.edit', [$this->organization_id, $this]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
#[Fillable(['name', 'email', 'password', 'is_msp_staff'])]
|
||||
#[Hidden(['password', 'remember_token', 'two_factor_secret', 'two_factor_recovery_codes'])]
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable, HasApiTokens, HasRoles, TwoFactorAuthenticatable;
|
||||
|
||||
public function organizations()
|
||||
{
|
||||
return $this->belongsToMany(Organization::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'is_msp_staff' => 'boolean',
|
||||
'last_login_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Contracts\Linkable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
#[Fillable([
|
||||
'organization_id', 'name', 'role', 'memory_gb', 'cpu_count',
|
||||
'operating_system', 'notes', 'created_by',
|
||||
])]
|
||||
class VirtualMachine extends Model implements Linkable
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
public function organization()
|
||||
{
|
||||
return $this->belongsTo(Organization::class);
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function logs()
|
||||
{
|
||||
return $this->hasMany(VirtualMachineLog::class)->orderByDesc('created_at');
|
||||
}
|
||||
|
||||
public function linkableKind(): string
|
||||
{
|
||||
return 'VirtualMachine';
|
||||
}
|
||||
|
||||
public function linkableLabel(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function linkableUrl(): string
|
||||
{
|
||||
return route('organizations.virtual-machines.edit', [$this->organization_id, $this]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class VirtualMachineLog extends Model
|
||||
{
|
||||
const UPDATED_AT = null;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function virtualMachine()
|
||||
{
|
||||
return $this->belongsTo(VirtualMachine::class);
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Actions\Fortify\CreateNewUser;
|
||||
use App\Actions\Fortify\ResetUserPassword;
|
||||
use App\Actions\Fortify\UpdateUserPassword;
|
||||
use App\Actions\Fortify\UpdateUserProfileInformation;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Fortify\Actions\RedirectIfTwoFactorAuthenticatable;
|
||||
use Laravel\Fortify\Fortify;
|
||||
|
||||
class FortifyServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
Fortify::createUsersUsing(CreateNewUser::class);
|
||||
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
|
||||
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
|
||||
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
|
||||
Fortify::redirectUserForTwoFactorAuthenticationUsing(RedirectIfTwoFactorAuthenticatable::class);
|
||||
|
||||
Fortify::loginView(fn () => view('auth.login'));
|
||||
Fortify::requestPasswordResetLinkView(fn () => view('auth.forgot-password'));
|
||||
Fortify::resetPasswordView(fn (Request $request) => view('auth.reset-password', ['request' => $request]));
|
||||
Fortify::twoFactorChallengeView(fn () => view('auth.two-factor-challenge'));
|
||||
|
||||
RateLimiter::for('login', function (Request $request) {
|
||||
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
|
||||
|
||||
return Limit::perMinute(5)->by($throttleKey);
|
||||
});
|
||||
|
||||
RateLimiter::for('two-factor', function (Request $request) {
|
||||
return Limit::perMinute(5)->by($request->session()->get('login.id'));
|
||||
});
|
||||
|
||||
RateLimiter::for('passkeys', function (Request $request) {
|
||||
$credentialId = $request->input('credential.id');
|
||||
|
||||
return Limit::perMinute(10)->by(
|
||||
($credentialId ?: $request->session()->getId()).'|'.$request->ip()
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\OnlyOffice;
|
||||
|
||||
use App\Models\Document;
|
||||
use App\Models\User;
|
||||
use Firebase\JWT\JWT;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
class DocumentConfigBuilder
|
||||
{
|
||||
public function build(Document $document, User $user, bool $editable = true): array
|
||||
{
|
||||
$config = [
|
||||
'document' => [
|
||||
'fileType' => $document->office_extension,
|
||||
'key' => $document->office_key,
|
||||
'title' => $document->name.'.'.$document->office_extension,
|
||||
'url' => $this->downloadUrl($document),
|
||||
],
|
||||
'documentType' => $this->documentType($document->office_extension),
|
||||
'editorConfig' => [
|
||||
'mode' => $editable ? 'edit' : 'view',
|
||||
'callbackUrl' => $this->callbackUrl($document),
|
||||
'user' => [
|
||||
'id' => (string) $user->id,
|
||||
'name' => $user->name,
|
||||
],
|
||||
'lang' => 'fr',
|
||||
],
|
||||
];
|
||||
|
||||
$secret = config('onlyoffice.jwt_secret');
|
||||
|
||||
if (filled($secret)) {
|
||||
$config['token'] = JWT::encode($config, $secret, 'HS256');
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
protected function downloadUrl(Document $document): string
|
||||
{
|
||||
$signed = URL::temporarySignedRoute(
|
||||
'onlyoffice.download',
|
||||
now()->addMinutes(5),
|
||||
['document' => $document->id],
|
||||
absolute: false
|
||||
);
|
||||
|
||||
return $this->baseUrl().$signed;
|
||||
}
|
||||
|
||||
protected function callbackUrl(Document $document): string
|
||||
{
|
||||
return $this->baseUrl().route('onlyoffice.callback', $document, absolute: false);
|
||||
}
|
||||
|
||||
protected function baseUrl(): string
|
||||
{
|
||||
return rtrim(config('onlyoffice.callback_base_url'), '/');
|
||||
}
|
||||
|
||||
protected function documentType(?string $extension): string
|
||||
{
|
||||
return match ($extension) {
|
||||
'xlsx' => 'cell',
|
||||
'pptx' => 'slide',
|
||||
default => 'word',
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user