Compare commits

..
1 Commits
Author SHA1 Message Date
jeremy 348c0672c1 Initial commit 2026-08-16 11:25:53 +00:00
242 changed files with 29 additions and 26239 deletions
-11
View File
@@ -1,11 +0,0 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "itglue-clone",
"runtimeExecutable": "php",
"runtimeArgs": ["artisan", "serve", "--port=8123"],
"port": 8123
}
]
}
-18
View File
@@ -1,18 +0,0 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[{compose,docker-compose}.{yml,yaml}]
indent_size = 4
-76
View File
@@ -1,76 +0,0 @@
APP_NAME=ITGlueClone
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
# PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
# OnlyOffice Document Server (see docker-compose.yml for a local dev instance).
# ONLYOFFICE_DS_URL must be reachable from the browser.
# ONLYOFFICE_CALLBACK_BASE_URL must be reachable from *inside* the document
# server's Docker container to call back into this app. When the document
# server runs in Docker and this app runs on the host (e.g. `php artisan serve`),
# use http://host.docker.internal:<port> — localhost from inside the container
# refers to the container itself, not your machine.
ONLYOFFICE_DS_URL=http://localhost:8080
ONLYOFFICE_CALLBACK_BASE_URL=http://host.docker.internal:8000
ONLYOFFICE_JWT_SECRET=
-11
View File
@@ -1,11 +0,0 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore
+28 -23
View File
@@ -1,27 +1,32 @@
*.log # ---> Laravel
.DS_Store /vendor/
node_modules/
npm-debug.log
yarn-error.log
# Laravel 4 specific
bootstrap/compiled.php
app/storage/
# Laravel 5 & Lumen specific
public/storage
public/hot
# Laravel 5 & Lumen specific with changed public path
public_html/storage
public_html/hot
storage/*.key
.env .env
Homestead.yaml
Homestead.json
/.vagrant
.phpunit.result.cache
/public/build
/storage/pail
.env.backup .env.backup
.env.production .env.production
.phpactor.json .phpactor.json
.phpunit.result.cache auth.json
/.codex
/.cursor/
/.idea
/.nova
/.phpunit.cache
/.vscode
/.zed
/auth.json
/node_modules
/public/build
/public/fonts-manifest.dev.json
/public/hot
/public/storage
/storage/*.key
/storage/pail
/vendor
_ide_helper.php
Homestead.json
Homestead.yaml
Thumbs.db
-2
View File
@@ -1,2 +0,0 @@
ignore-scripts=true
audit=true
+1 -70
View File
@@ -1,72 +1,3 @@
# CABM-IT # CABM-IT
Application de documentation IT pour prestataires infogérants (type ItGlue), permettant de centraliser toutes les informations techniques et administratives des organisations clientes : mots de passe, actifs, documents, contacts, tâches et bien plus. Gestion d'un service IT
Développée avec [Laravel](https://laravel.com), [Livewire](https://livewire.laravel.com)/Volt et [Tailwind CSS](https://tailwindcss.com).
## Fonctionnalités
### Organisations
- Gestion multi-clients avec fiche par organisation (type, statut, description, logo avec recadrage intégré, plages IP autorisées).
- **Tableau de bord** par organisation : compteurs clés, domaines à surveiller (expiration proche), activité récente (créations/modifications), tâches ouvertes, et journal de bord.
- **Journal de bord** : notes libres catégorisées (projet en cours, point d'attention, intervention prévue, information générale) pour le suivi au fil de l'eau.
### Contenu métier
- **Contacts** et **Sites** par organisation.
- **Domaines** avec suivi d'expiration et alertes.
- **Mots de passe** chiffrés, avec journal d'audit.
- **Documents** organisés en arborescence de dossiers (glisser-déposer pour réorganiser) :
- Documents texte simples.
- Documents **Markdown** avec aperçu rendu et aide-mémoire de syntaxe.
- Documents **Office** (Word/Excel) créés ou importés, édités en ligne via **OnlyOffice**.
- Fichiers importés (PDF, TXT, MD) avec visionneuse PDF intégrée.
- Historique de versions sur chaque document.
- **Actifs flexibles** : types d'actifs personnalisables par un administrateur (champs sur mesure : texte, liste déroulante, case à cocher, date, mot de passe, tag, liaison vers un contact/site/domaine/autre actif...). Chaque actif dispose d'un **journal MCO** alimenté automatiquement à chaque modification de champ.
- **Machines virtuelles** : fiche dédiée (rôle, mémoire, CPU, OS) avec journal MCO.
- **ToDo** : tâches par organisation avec catégorie (Infra, Support, Métier, Autre), criticité, échéance et affectation à un technicien ou administrateur ; remontées sur le tableau de bord.
- **Raccourcis** : liens web rapides, globaux (toutes organisations, gérés par un administrateur) ou propres à une organisation.
- **Éléments liés** : liaison libre entre mots de passe, actifs, documents et autres enregistrements pour naviguer rapidement entre informations connexes.
### Utilisateurs et sécurité
- Comptes utilisateurs créés uniquement par un administrateur (pas d'auto-inscription).
- Rôles : **Administrateur**, **Technicien**, **Lecture seule**.
- Authentification renforcée : mots de passe, authentification à deux facteurs, passkeys.
- Restriction d'accès par organisation via plages IP autorisées.
## Stack technique
- **Backend** : Laravel 13, Livewire 4 / Volt, Laravel Fortify (authentification), Spatie Permission (rôles).
- **Frontend** : Tailwind CSS 4, Alpine.js.
- **Édition documentaire** : OnlyOffice Document Server (via Docker).
- **Base de données** : SQLite par défaut (configurable).
## Installation
```bash
composer install
npm install
cp .env.example .env
php artisan key:generate
php artisan migrate --seed
php artisan storage:link
npm run build
php artisan serve
```
Un compte administrateur est créé par le seeder par défaut (voir `database/seeders/DatabaseSeeder.php`).
### OnlyOffice (édition de documents Word/Excel)
L'édition en ligne des documents Office nécessite une instance OnlyOffice Document Server. Un exemple de configuration est fourni dans `docker-compose.yml`. Voir `config/onlyoffice.php` pour les variables d'environnement associées (`ONLYOFFICE_DS_URL`, `ONLYOFFICE_CALLBACK_BASE_URL`, `ONLYOFFICE_JWT_SECRET`).
## Tests
```bash
php artisan test
```
-43
View File
@@ -1,43 +0,0 @@
<?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']),
]);
}
}
@@ -1,19 +0,0 @@
<?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'];
}
}
-32
View File
@@ -1,32 +0,0 @@
<?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();
}
}
@@ -1,35 +0,0 @@
<?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();
}
}
@@ -1,61 +0,0 @@
<?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();
}
}
-34
View File
@@ -1,34 +0,0 @@
<?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);
}
}
-16
View File
@@ -1,16 +0,0 @@
<?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;
}
-8
View File
@@ -1,8 +0,0 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}
@@ -1,24 +0,0 @@
<?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);
}
}
@@ -1,72 +0,0 @@
<?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);
}
}
@@ -1,31 +0,0 @@
<?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.');
}
}
-39
View File
@@ -1,39 +0,0 @@
<?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);
}
}
-102
View File
@@ -1,102 +0,0 @@
<?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', 'location_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 isDiagram(): bool
{
return $this->type === 'diagram';
}
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 location()
{
return $this->belongsTo(Location::class);
}
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]);
}
}
-22
View File
@@ -1,22 +0,0 @@
<?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');
}
}
-53
View File
@@ -1,53 +0,0 @@
<?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',
};
}
}
-62
View File
@@ -1,62 +0,0 @@
<?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]);
}
}
-80
View File
@@ -1,80 +0,0 @@
<?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']}]);
}
}
-22
View File
@@ -1,22 +0,0 @@
<?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');
}
}
-27
View File
@@ -1,27 +0,0 @@
<?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);
}
}
-33
View File
@@ -1,33 +0,0 @@
<?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',
'latitude', 'longitude',
])]
class Location extends Model
{
protected function casts(): array
{
return [
'is_primary' => 'boolean',
'latitude' => 'float',
'longitude' => 'float',
];
}
public function organization()
{
return $this->belongsTo(Organization::class);
}
public function contacts()
{
return $this->hasMany(Contact::class);
}
}
-102
View File
@@ -1,102 +0,0 @@
<?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', 'glpi_entity_id', 'technician_user_id', 'isp_name', 'public_ips'])]
class Organization extends Model
{
use SoftDeletes;
protected function casts(): array
{
return [
'allowed_ip_ranges' => 'array',
'public_ips' => '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);
}
public function technician()
{
return $this->belongsTo(User::class, 'technician_user_id');
}
public function providers()
{
return $this->belongsToMany(Provider::class, 'organization_provider')
->withPivot('id', 'label')
->withTimestamps();
}
}
-34
View File
@@ -1,34 +0,0 @@
<?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;
}
}
-80
View File
@@ -1,80 +0,0 @@
<?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]);
}
}
-22
View File
@@ -1,22 +0,0 @@
<?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);
}
}
-34
View File
@@ -1,34 +0,0 @@
<?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', 'logo_path', 'phone', 'email', 'website', 'notes'])]
class Provider extends Model
{
use SoftDeletes;
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 organizations()
{
return $this->belongsToMany(Organization::class, 'organization_provider')
->withPivot('id', 'label')
->withTimestamps();
}
public function logEntries()
{
return $this->hasMany(ProviderLogEntry::class)->orderByDesc('created_at');
}
}
-34
View File
@@ -1,34 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ProviderLogEntry extends Model
{
const UPDATED_AT = null;
const CATEGORIES = [
'contact' => 'Échange (mail / appel)',
'observation' => 'Observation',
'incident' => 'Incident',
'info' => 'Information générale',
];
protected $guarded = [];
public function provider()
{
return $this->belongsTo(Provider::class);
}
public function creator()
{
return $this->belongsTo(User::class, 'created_by');
}
public function categoryLabel(): string
{
return self::CATEGORIES[$this->category] ?? $this->category;
}
}
-48
View File
@@ -1,48 +0,0 @@
<?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;
}
}
-15
View File
@@ -1,15 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
/**
* A key/value override for a whitelisted config path — see SettingsRegistry.
* Absence of a row (or a null value) means "use the .env/config default".
*/
#[Fillable(['key', 'value'])]
class Setting extends Model
{
}
-25
View File
@@ -1,25 +0,0 @@
<?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;
}
}
-29
View File
@@ -1,29 +0,0 @@
<?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);
}
}
-85
View File
@@ -1,85 +0,0 @@
<?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]);
}
}
-42
View File
@@ -1,42 +0,0 @@
<?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',
];
}
}
-47
View File
@@ -1,47 +0,0 @@
<?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]);
}
}
-22
View File
@@ -1,22 +0,0 @@
<?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');
}
}
-56
View File
@@ -1,56 +0,0 @@
<?php
namespace App\Providers;
use App\Models\Setting;
use App\Services\SettingsRegistry;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\ServiceProvider;
use Throwable;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
$this->applySettingOverrides();
}
/**
* Overlay database-stored settings (see SettingsRegistry) onto config,
* so the rest of the app keeps reading config('mail.from.address') etc.
* without knowing whether the value came from .env or the settings UI.
* Silently does nothing if the settings table doesn't exist yet (e.g.
* during the very first `migrate` on a fresh install).
*/
protected function applySettingOverrides(): void
{
try {
$overrides = Cache::remember(
SettingsRegistry::CACHE_KEY,
300,
fn () => Setting::query()->whereNotNull('value')->pluck('value', 'key')->all(),
);
} catch (Throwable) {
return;
}
$map = SettingsRegistry::configMap();
foreach ($overrides as $key => $value) {
if (isset($map[$key]) && $value !== '') {
config([$map[$key] => $value]);
}
}
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}
-61
View File
@@ -1,61 +0,0 @@
<?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()
);
});
}
}
-361
View File
@@ -1,361 +0,0 @@
<?php
namespace App\Services\Glpi;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Throwable;
class GlpiClient
{
public const STATUSES = [
1 => 'Nouveau',
2 => 'En cours (assigné)',
3 => 'En cours (planifié)',
4 => 'En attente',
5 => 'Résolu',
6 => 'Clos',
];
public const STATUS_CLOSED = 6;
public const PRIORITIES = [
1 => 'Très basse',
2 => 'Basse',
3 => 'Moyenne',
4 => 'Haute',
5 => 'Très haute',
6 => 'Majeure',
];
protected string $baseUrl;
protected string $appToken;
protected string $userToken;
public function __construct()
{
$this->baseUrl = rtrim((string) config('glpi.api_url'), '/');
$this->appToken = (string) config('glpi.app_token');
$this->userToken = (string) config('glpi.user_token');
}
public function isConfigured(): bool
{
return $this->baseUrl !== '' && $this->appToken !== '' && $this->userToken !== '';
}
/**
* The GLPI front-end base URL (for building links to tickets), derived
* from the REST API URL by dropping the trailing "apirest.php".
*/
public function frontUrl(): string
{
return preg_replace('#/apirest\.php$#', '', $this->baseUrl);
}
public function ticketUrl(int $ticketId): string
{
return $this->frontUrl()."/front/ticket.form.php?id={$ticketId}";
}
/**
* Recent tickets for a GLPI entity, most recently modified first.
*
* @return array{tickets: array<int, array<string, mixed>>, total: int, error: ?string}
*/
public function recentTicketsForEntity(int $entityId, int $limit = 8): array
{
if (! $this->isConfigured()) {
return ['tickets' => [], 'total' => 0, 'error' => null];
}
return Cache::remember(
"glpi.tickets.entity.{$entityId}.{$limit}",
(int) config('glpi.cache_ttl_seconds', 60),
function () use ($entityId, $limit) {
try {
return $this->fetchRecentTickets($entityId, $limit);
} catch (Throwable $e) {
Log::warning('GLPI: échec de récupération des tickets', [
'entity_id' => $entityId,
'message' => $e->getMessage(),
]);
return ['tickets' => [], 'total' => 0, 'error' => "Impossible de joindre GLPI pour le moment."];
}
}
);
}
/**
* @return array{tickets: array<int, array<string, mixed>>, total: int, error: ?string}
*/
protected function fetchRecentTickets(int $entityId, int $limit): array
{
return $this->withSession(function (string $sessionToken) use ($entityId, $limit) {
$response = Http::withHeaders($this->headers($sessionToken))
->get("{$this->baseUrl}/search/Ticket", [
'criteria' => [
['field' => 80, 'searchtype' => 'equals', 'value' => $entityId],
// GLPI's "notequals" searchtype is silently ignored by the search
// engine; excluding a value only works via a negated AND link.
['link' => 'AND NOT', 'field' => 12, 'searchtype' => 'equals', 'value' => self::STATUS_CLOSED],
],
// 4 = demandeur, 27 = nombre de suivis, 28 = nombre de tâches (les
// techniciens répondent parfois via une tâche plutôt qu'un suivi —
// les deux comptent comme des interactions), 95 = technicien
// responsable, 5 = technicien assigné (repli si 95 n'est pas renseigné).
'forcedisplay' => [2, 1, 12, 3, 4, 7, 21, 27, 28, 95, 5, 19],
'sort' => 19,
'order' => 'DESC',
'range' => '0-'.($limit - 1),
]);
if ($response->failed()) {
throw new GlpiException("GLPI a répondu avec une erreur (HTTP {$response->status()}).");
}
$rows = $response->json('data') ?? [];
$technicianIds = collect($rows)
->flatMap(fn (array $row) => [
...Arr::wrap($row['95'] ?? null),
...Arr::wrap($row['5'] ?? null),
])
->filter()
->unique()
->values();
$technicianNames = $this->resolveUserNames($sessionToken, $technicianIds);
$lastInteractionAuthors = $this->fetchLastInteractionAuthors($sessionToken, $rows);
$tickets = collect($rows)->map(function (array $row) use ($technicianNames, $lastInteractionAuthors) {
$technicianIds = filled($row['95'] ?? null) ? Arr::wrap($row['95']) : Arr::wrap($row['5'] ?? null);
$followupsCount = (int) ($row['27'] ?? 0);
$tasksCount = (int) ($row['28'] ?? 0);
$requesterIds = array_map('strval', Arr::wrap($row['4'] ?? null));
// Nothing yet means the only thing on the ticket is the
// requester's own opening message — nobody from support has
// replied (via a followup or a task). Otherwise, check who
// wrote whichever of those came in last.
$lastAuthor = ($followupsCount === 0 && $tasksCount === 0)
? Arr::first($requesterIds)
: ($lastInteractionAuthors[(string) $row['2']] ?? null);
return [
'id' => $row['2'],
'title' => $this->decodeText((string) $row['1']),
'status' => (int) $row['12'],
'status_label' => self::STATUSES[(int) $row['12']] ?? 'Inconnu',
'category' => filled($row['7'] ?? null) ? $this->decodeText((string) $row['7']) : null,
'excerpt' => $this->excerpt($row['21'] ?? null),
'priority' => (int) $row['3'],
'priority_label' => self::PRIORITIES[(int) $row['3']] ?? 'Inconnue',
'interactions_count' => $followupsCount + $tasksCount,
'awaiting_technician' => $lastAuthor !== null && in_array((string) $lastAuthor, $requesterIds, true),
'technician' => collect($technicianIds)
->map(fn ($id) => $technicianNames[$id] ?? null)
->filter()
->implode(', ') ?: null,
'date_mod' => $row['19'],
'url' => $this->ticketUrl((int) $row['2']),
];
})->all();
return [
'tickets' => $tickets,
'total' => (int) ($response->json('totalcount') ?? count($tickets)),
'error' => null,
];
});
}
/**
* The search engine can count followups (option 27) and tasks (option 28)
* but doesn't expose who wrote the most recent one of either — and
* technicians in this GLPI instance sometimes reply via a task instead of
* a followup, so both have to be checked. For every ticket that has at
* least one of either, fetch its last followup AND its last task, then
* keep whichever is more recent. Run all those lookups concurrently
* (Http::pool) so an 8-ticket dashboard panel costs one round trip, not
* up to sixteen sequential ones.
*
* @param array<int, array<string, mixed>> $rows
* @return array<string, string> ticket id => author user id
*/
protected function fetchLastInteractionAuthors(string $sessionToken, array $rows): array
{
$ticketIds = collect($rows)
->filter(fn (array $row) => (int) ($row['27'] ?? 0) > 0 || (int) ($row['28'] ?? 0) > 0)
->pluck('2')
->all();
if (empty($ticketIds)) {
return [];
}
$subItemParams = ['sort' => 'date', 'order' => 'DESC', 'range' => '0-0'];
$responses = Http::pool(fn ($pool) => collect($ticketIds)->flatMap(fn ($ticketId) => [
$pool->as("followup-{$ticketId}")->withHeaders($this->headers($sessionToken))
->get("{$this->baseUrl}/Ticket/{$ticketId}/ITILFollowup", $subItemParams),
$pool->as("task-{$ticketId}")->withHeaders($this->headers($sessionToken))
->get("{$this->baseUrl}/Ticket/{$ticketId}/TicketTask", $subItemParams),
])->all());
$latestPerTicket = [];
foreach (['followup', 'task'] as $type) {
foreach ($ticketIds as $ticketId) {
$response = $responses["{$type}-{$ticketId}"] ?? null;
if (! $response instanceof \Illuminate\Http\Client\Response || $response->failed()) {
continue;
}
$entry = $response->json('0');
if (! isset($entry['users_id'], $entry['date'])) {
continue;
}
$ticketId = (string) $ticketId;
$existing = $latestPerTicket[$ticketId] ?? null;
if ($existing === null || $entry['date'] > $existing['date']) {
$latestPerTicket[$ticketId] = $entry;
}
}
}
return collect($latestPerTicket)->map(fn ($entry) => (string) $entry['users_id'])->all();
}
/**
* Ticket search results only carry raw GLPI user IDs for the technician
* fields (unlike single-value dropdowns such as category, these aren't
* auto-resolved to a display name) — batch-resolve them by name in one
* extra call within the same session.
*
* @param Collection<int, int|string> $userIds
* @return array<string, string> id => "Firstname Lastname"
*/
protected function resolveUserNames(string $sessionToken, Collection $userIds): array
{
if ($userIds->isEmpty()) {
return [];
}
$response = Http::withHeaders($this->headers($sessionToken))
->get("{$this->baseUrl}/search/User", [
'criteria' => $this->buildOrCriteria(2, $userIds->all()),
'forcedisplay' => [2, 9, 34],
'range' => '0-'.($userIds->count() - 1),
]);
if ($response->failed()) {
return [];
}
return collect($response->json('data') ?? [])
->mapWithKeys(function (array $row) {
$name = trim(($row['9'] ?? '').' '.($row['34'] ?? ''));
return [(string) $row['2'] => $name !== '' ? $name : null];
})
->filter()
->all();
}
/**
* Builds a GLPI search "criteria" array matching any of the given values
* against a single field, e.g. field=2 equals 5 OR field=2 equals 9.
*/
protected function buildOrCriteria(int $field, array $values): array
{
$criteria = [];
foreach (array_values($values) as $index => $value) {
$criteria[] = [
...($index > 0 ? ['link' => 'OR'] : []),
'field' => $field,
'searchtype' => 'equals',
'value' => $value,
];
}
return $criteria;
}
/**
* GLPI's ticket description comes back as HTML, itself HTML-entity-encoded
* (e.g. "&#60;p&#62;" for "<p>"). Strip markup and trim to a short
* plain-text preview.
*/
protected function excerpt(?string $html, int $length = 160): ?string
{
if (blank($html)) {
return null;
}
$text = strip_tags($this->decodeText($html));
$text = trim(preg_replace('/\s+/', ' ', $text));
return $text === '' ? null : \Illuminate\Support\Str::limit($text, $length);
}
/**
* Some GLPI fields (rich-text description, titles typed via a rich editor)
* are HTML-entity-encoded twice over: once because the stored content is
* itself escaped HTML (e.g. a literal "&amp;"), and again because the
* search API encodes the whole field for its JSON response. A single
* decode leaves artifacts like "&amp;" in the output, so decode twice —
* harmless no-op on text that was only encoded once.
*/
protected function decodeText(string $value): string
{
return html_entity_decode(html_entity_decode($value, ENT_QUOTES), ENT_QUOTES);
}
/**
* Opens a GLPI session, runs the callback with the session token, and
* always closes the session afterwards — GLPI has no stateless auth mode
* for the REST API, so every call pays this round trip.
*/
protected function withSession(\Closure $callback): mixed
{
$session = Http::withHeaders([
'App-Token' => $this->appToken,
'Authorization' => 'user_token '.$this->userToken,
])->get("{$this->baseUrl}/initSession");
if ($session->failed()) {
throw new GlpiException("Échec de connexion à GLPI (HTTP {$session->status()}).");
}
$sessionToken = $session->json('session_token');
try {
return $callback($sessionToken);
} finally {
Http::withHeaders($this->headers($sessionToken))->get("{$this->baseUrl}/killSession");
}
}
/**
* @return array<string, string>
*/
protected function headers(string $sessionToken): array
{
return [
'App-Token' => $this->appToken,
'Session-Token' => $sessionToken,
];
}
}
-9
View File
@@ -1,9 +0,0 @@
<?php
namespace App\Services\Glpi;
use RuntimeException;
class GlpiException extends RuntimeException
{
}
@@ -1,72 +0,0 @@
<?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',
};
}
}
-74
View File
@@ -1,74 +0,0 @@
<?php
namespace App\Services;
/**
* Whitelist of settings administrators can override from the database instead
* of editing .env directly. Deliberately excludes anything read at the very
* start of the boot process (APP_KEY, DB_*, SESSION_DRIVER, CACHE_STORE,
* QUEUE_CONNECTION, ...): those are resolved before a database connection —
* or before this override even runs — so changing them here wouldn't
* reliably take effect, and getting DB_* wrong here could lock everyone out.
* Only settings that are safe to read lazily, at point of use, belong here.
*/
class SettingsRegistry
{
/**
* @var array<string, array<string, array{config: string, label: string}>>
*/
public const SETTINGS = [
'Application' => [
'APP_NAME' => ['config' => 'app.name', 'label' => "Nom de l'application"],
'APP_URL' => ['config' => 'app.url', 'label' => 'URL de base'],
],
'Mail' => [
'MAIL_MAILER' => ['config' => 'mail.default', 'label' => 'Pilote (mailer)'],
'MAIL_HOST' => ['config' => 'mail.mailers.smtp.host', 'label' => 'Hôte SMTP'],
'MAIL_PORT' => ['config' => 'mail.mailers.smtp.port', 'label' => 'Port SMTP'],
'MAIL_SCHEME' => ['config' => 'mail.mailers.smtp.scheme', 'label' => 'Schéma (tls, ssl…)'],
'MAIL_USERNAME' => ['config' => 'mail.mailers.smtp.username', 'label' => 'Utilisateur SMTP'],
'MAIL_PASSWORD' => ['config' => 'mail.mailers.smtp.password', 'label' => 'Mot de passe SMTP'],
'MAIL_FROM_ADDRESS' => ['config' => 'mail.from.address', 'label' => 'Adresse expéditeur'],
'MAIL_FROM_NAME' => ['config' => 'mail.from.name', 'label' => 'Nom expéditeur'],
],
'OnlyOffice' => [
'ONLYOFFICE_DS_URL' => ['config' => 'onlyoffice.document_server_url', 'label' => 'URL du serveur de documents'],
'ONLYOFFICE_CALLBACK_BASE_URL' => ['config' => 'onlyoffice.callback_base_url', 'label' => 'URL de rappel (callback)'],
'ONLYOFFICE_JWT_SECRET' => ['config' => 'onlyoffice.jwt_secret', 'label' => 'Secret JWT'],
],
'Draw.io' => [
'DRAWIO_URL' => ['config' => 'drawio.url', 'label' => "URL de l'éditeur draw.io"],
],
'GLPI' => [
'GLPI_API_URL' => ['config' => 'glpi.api_url', 'label' => "URL de l'API"],
'GLPI_APP_TOKEN' => ['config' => 'glpi.app_token', 'label' => 'Jeton application'],
'GLPI_USER_TOKEN' => ['config' => 'glpi.user_token', 'label' => 'Jeton utilisateur'],
'GLPI_CACHE_TTL' => ['config' => 'glpi.cache_ttl_seconds', 'label' => 'Durée du cache (secondes)'],
],
];
public const CACHE_KEY = 'settings.overrides';
public static function isSensitive(string $key): bool
{
return (bool) preg_match('/PASSWORD|SECRET|TOKEN|KEY/i', $key);
}
/**
* Flat env-style key => config path map, for applying overrides at boot.
*
* @return array<string, string>
*/
public static function configMap(): array
{
$map = [];
foreach (self::SETTINGS as $section) {
foreach ($section as $envKey => $meta) {
$map[$envKey] = $meta['config'];
}
}
return $map;
}
}
-18
View File
@@ -1,18 +0,0 @@
#!/usr/bin/env php
<?php
use Illuminate\Foundation\Application;
use Symfony\Component\Console\Input\ArgvInput;
define('LARAVEL_START', microtime(true));
// Register the Composer autoloader...
require __DIR__.'/vendor/autoload.php';
// Bootstrap Laravel and handle the command...
/** @var Application $app */
$app = require_once __DIR__.'/bootstrap/app.php';
$status = $app->handleCommand(new ArgvInput);
exit($status);
-37
View File
@@ -1,37 +0,0 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
// The app runs behind a reverse proxy that terminates TLS and forwards
// plain HTTP — without this, Laravel doesn't know the original request
// was HTTPS and generates asset/URL links as http://, which browsers
// then block as mixed content on an https:// page.
$middleware->trustProxies(at: '*');
$middleware->alias([
'org.ip_access' => \App\Http\Middleware\CheckOrganizationIpAccess::class,
'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class,
]);
// The OnlyOffice Document Server calls this server-to-server, without
// a session/CSRF token; it authenticates via a JWT bearer token instead.
$middleware->validateCsrfTokens(except: [
'onlyoffice/documents/*/callback',
]);
})
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->shouldRenderJsonWhen(
fn (Request $request) => $request->is('api/*'),
);
})->create();
-2
View File
@@ -1,2 +0,0 @@
*
!.gitignore
-6
View File
@@ -1,6 +0,0 @@
<?php
return [
App\Providers\AppServiceProvider::class,
App\Providers\FortifyServiceProvider::class,
];
-93
View File
@@ -1,93 +0,0 @@
{
"$schema": "https://getcomposer.org/schema.json",
"name": "laravel/laravel",
"type": "project",
"description": "The skeleton application for the Laravel framework.",
"keywords": ["laravel", "framework"],
"license": "MIT",
"require": {
"php": "^8.3",
"firebase/php-jwt": "^7.1",
"laravel/fortify": "^1.37",
"laravel/framework": "^13.8",
"laravel/sanctum": "^4.3",
"laravel/scout": "^11.5",
"laravel/tinker": "^3.0",
"livewire/livewire": "^4.3",
"spatie/laravel-activitylog": "^4.12",
"spatie/laravel-permission": "^8.3"
},
"require-dev": {
"fakerphp/faker": "^1.23",
"laravel/pail": "^1.2.5",
"laravel/pao": "^1.0.6",
"laravel/pint": "^1.27",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"phpunit/phpunit": "^12.5.12"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"setup": [
"composer install",
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
"@php artisan key:generate",
"@php artisan migrate --force",
"npm install --ignore-scripts",
"npm run build"
],
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
],
"test": [
"@php artisan config:clear --ansi @no_additional_args",
"@php artisan test"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi",
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
"@php artisan migrate --graceful --ansi"
],
"pre-package-uninstall": [
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
]
},
"extra": {
"laravel": {
"dont-discover": []
}
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"php-http/discovery": true
}
},
"minimum-stability": "stable",
"prefer-stable": true
}
Generated
-10324
View File
File diff suppressed because it is too large Load Diff
-52
View File
@@ -1,52 +0,0 @@
<?php
return [
/*
* If set to false, no activities will be saved to the database.
*/
'enabled' => env('ACTIVITY_LOGGER_ENABLED', true),
/*
* When the clean-command is executed, all recording activities older than
* the number of days specified here will be deleted.
*/
'delete_records_older_than_days' => 365,
/*
* If no log name is passed to the activity() helper
* we use this default log name.
*/
'default_log_name' => 'default',
/*
* You can specify an auth driver here that gets user models.
* If this is null we'll use the current Laravel auth driver.
*/
'default_auth_driver' => null,
/*
* If set to true, the subject returns soft deleted models.
*/
'subject_returns_soft_deleted_models' => false,
/*
* This model will be used to log activity.
* It should implement the Spatie\Activitylog\Contracts\Activity interface
* and extend Illuminate\Database\Eloquent\Model.
*/
'activity_model' => \Spatie\Activitylog\Models\Activity::class,
/*
* This is the name of the table that will be created by the migration and
* used by the Activity model shipped with this package.
*/
'table_name' => env('ACTIVITY_LOGGER_TABLE_NAME', 'activity_log'),
/*
* This is the database connection that will be used by the migration and
* the Activity model shipped with this package. In case it's not set
* Laravel's database.default will be used instead.
*/
'database_connection' => env('ACTIVITY_LOGGER_DB_CONNECTION'),
];
-126
View File
@@ -1,126 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Application Name
|--------------------------------------------------------------------------
|
| This value is the name of your application, which will be used when the
| framework needs to place the application's name in a notification or
| other UI elements where an application name needs to be displayed.
|
*/
'name' => env('APP_NAME', 'Laravel'),
/*
|--------------------------------------------------------------------------
| Application Environment
|--------------------------------------------------------------------------
|
| This value determines the "environment" your application is currently
| running in. This may determine how you prefer to configure various
| services the application utilizes. Set this in your ".env" file.
|
*/
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
|--------------------------------------------------------------------------
|
| When your application is in debug mode, detailed error messages with
| stack traces will be shown on every error that occurs within your
| application. If disabled, a simple generic error page is shown.
|
*/
'debug' => (bool) env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| the application so that it's available within Artisan commands.
|
*/
'url' => env('APP_URL', 'http://localhost'),
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. The timezone
| is set to "UTC" by default as it is suitable for most use cases.
|
*/
'timezone' => 'UTC',
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by Laravel's translation / localization methods. This option can be
| set to any locale for which you plan to have translation strings.
|
*/
'locale' => env('APP_LOCALE', 'en'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is utilized by Laravel's encryption services and should be set
| to a random, 32 character string to ensure that all encrypted values
| are secure. You should do this prior to deploying the application.
|
*/
'cipher' => 'AES-256-CBC',
'key' => env('APP_KEY'),
'previous_keys' => [
...array_filter(
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
),
],
/*
|--------------------------------------------------------------------------
| Maintenance Mode Driver
|--------------------------------------------------------------------------
|
| These configuration options determine the driver used to determine and
| manage Laravel's "maintenance mode" status. The "cache" driver will
| allow maintenance mode to be controlled across multiple machines.
|
| Supported drivers: "file", "cache"
|
*/
'maintenance' => [
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
'store' => env('APP_MAINTENANCE_STORE', 'database'),
],
];
-117
View File
@@ -1,117 +0,0 @@
<?php
use App\Models\User;
return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option defines the default authentication "guard" and password
| reset "broker" for your application. You may change these values
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => env('AUTH_GUARD', 'web'),
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| which utilizes session storage plus the Eloquent user provider.
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| Supported: "session"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| If you have multiple user tables or models you may configure multiple
| providers to represent the model / table. These providers may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => env('AUTH_MODEL', User::class),
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| These configuration options specify the behavior of Laravel's password
| reset functionality, including the table utilized for token storage
| and the user provider that is invoked to actually retrieve users.
|
| The expiry time is the number of minutes that each reset token will be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
| The throttle setting is the number of seconds a user must wait before
| generating more password reset tokens. This prevents the user from
| quickly generating a very large amount of password reset tokens.
|
*/
'passwords' => [
'users' => [
'provider' => 'users',
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
'expire' => 60,
'throttle' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Password Confirmation Timeout
|--------------------------------------------------------------------------
|
| Here you may define the number of seconds before a password confirmation
| window expires and users are asked to re-enter their password via the
| confirmation screen. By default, the timeout lasts for three hours.
|
*/
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
];
-136
View File
@@ -1,136 +0,0 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Cache Store
|--------------------------------------------------------------------------
|
| This option controls the default cache store that will be used by the
| framework. This connection is utilized if another isn't explicitly
| specified when running a cache operation inside the application.
|
*/
'default' => env('CACHE_STORE', 'database'),
/*
|--------------------------------------------------------------------------
| Cache Stores
|--------------------------------------------------------------------------
|
| Here you may define all of the cache "stores" for your application as
| well as their drivers. You may even define multiple stores for the
| same cache driver to group types of items stored in your caches.
|
| Supported drivers: "array", "database", "file", "memcached",
| "redis", "dynamodb", "storage", "octane",
| "session", "failover", "null"
|
*/
'stores' => [
'array' => [
'driver' => 'array',
'serialize' => false,
],
'database' => [
'driver' => 'database',
'connection' => env('DB_CACHE_CONNECTION'),
'table' => env('DB_CACHE_TABLE', 'cache'),
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache/data'),
'lock_path' => storage_path('framework/cache/data'),
],
'storage' => [
'driver' => 'storage',
'disk' => env('CACHE_STORAGE_DISK'),
'path' => env('CACHE_STORAGE_PATH', 'framework/cache/data'),
],
'memcached' => [
'driver' => 'memcached',
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
'sasl' => [
env('MEMCACHED_USERNAME'),
env('MEMCACHED_PASSWORD'),
],
'options' => [
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
],
'servers' => [
[
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
'port' => env('MEMCACHED_PORT', 11211),
'weight' => 100,
],
],
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
],
'dynamodb' => [
'driver' => 'dynamodb',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
'endpoint' => env('DYNAMODB_ENDPOINT'),
],
'octane' => [
'driver' => 'octane',
],
'failover' => [
'driver' => 'failover',
'stores' => [
'database',
'array',
],
],
],
/*
|--------------------------------------------------------------------------
| Cache Key Prefix
|--------------------------------------------------------------------------
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
| stores, there might be other applications using the same cache. For
| that reason, you may prefix every cache key to avoid collisions.
|
*/
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
/*
|--------------------------------------------------------------------------
| Serializable Classes
|--------------------------------------------------------------------------
|
| This value determines the classes that can be unserialized from cache
| storage. By default, no PHP classes will be unserialized from your
| cache to prevent gadget chain attacks if your APP_KEY is leaked.
|
*/
'serializable_classes' => false,
];
-184
View File
@@ -1,184 +0,0 @@
<?php
use Illuminate\Support\Str;
use Pdo\Mysql;
return [
/*
|--------------------------------------------------------------------------
| Default Database Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the database connections below you wish
| to use as your default connection for database operations. This is
| the connection which will be utilized unless another connection
| is explicitly specified when you execute a query / statement.
|
*/
'default' => env('DB_CONNECTION', 'sqlite'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
|
| Below are all of the database connections defined for your application.
| An example configuration is provided for each database system which
| is supported by Laravel. You're free to add / remove connections.
|
*/
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DB_URL'),
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
'busy_timeout' => env('DB_BUSY_TIMEOUT', 5000),
'journal_mode' => env('DB_JOURNAL_MODE', 'wal'),
'synchronous' => null,
'transaction_mode' => 'DEFERRED',
],
'mysql' => [
'driver' => 'mysql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'mariadb' => [
'driver' => 'mariadb',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => env('DB_SSLMODE', 'prefer'),
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'url' => env('DB_URL'),
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '1433'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
],
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
|
| This table keeps track of all the migrations that have already run for
| your application. Using this information, we can determine which of
| the migrations on disk haven't actually been run on the database.
|
*/
'migrations' => [
'table' => 'migrations',
'update_date_on_publish' => true,
],
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer body of commands than a typical key-value system
| such as Memcached. You may define your connection settings here.
|
*/
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
'persistent' => env('REDIS_PERSISTENT', false),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
],
];
-9
View File
@@ -1,9 +0,0 @@
<?php
return [
// Base URL of the draw.io / diagrams.net editor to embed (embed mode).
// Defaults to the public diagrams.net service; point this to a self-hosted
// instance (e.g. the official jgraph/drawio Docker image) to keep diagram
// editing fully in-house.
'url' => env('DRAWIO_URL', 'https://embed.diagrams.net'),
];
-80
View File
@@ -1,80 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Filesystem Disk
|--------------------------------------------------------------------------
|
| Here you may specify the default filesystem disk that should be used
| by the framework. The "local" disk, as well as a variety of cloud
| based disks are available to your application for file storage.
|
*/
'default' => env('FILESYSTEM_DISK', 'local'),
/*
|--------------------------------------------------------------------------
| Filesystem Disks
|--------------------------------------------------------------------------
|
| Below you may configure as many filesystem disks as necessary, and you
| may even configure multiple disks for the same driver. Examples for
| most supported storage drivers are configured here for reference.
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
*/
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app/private'),
'serve' => true,
'throw' => false,
'report' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
'visibility' => 'public',
'throw' => false,
'report' => false,
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
'report' => false,
],
],
/*
|--------------------------------------------------------------------------
| Symbolic Links
|--------------------------------------------------------------------------
|
| Here you may configure the symbolic links that will be created when the
| `storage:link` Artisan command is executed. The array keys should be
| the locations of the links and the values should be their targets.
|
*/
'links' => [
public_path('storage') => storage_path('app/public'),
],
];
-180
View File
@@ -1,180 +0,0 @@
<?php
use Laravel\Fortify\Features;
return [
/*
|--------------------------------------------------------------------------
| Fortify Guard
|--------------------------------------------------------------------------
|
| Here you may specify which authentication guard Fortify will use while
| authenticating users. This value should correspond with one of your
| guards that is already present in your "auth" configuration file.
|
*/
'guard' => 'web',
/*
|--------------------------------------------------------------------------
| Fortify Password Broker
|--------------------------------------------------------------------------
|
| Here you may specify which password broker Fortify can use when a user
| is resetting their password. This configured value should match one
| of your password brokers setup in your "auth" configuration file.
|
*/
'passwords' => 'users',
/*
|--------------------------------------------------------------------------
| Username / Email
|--------------------------------------------------------------------------
|
| This value defines which model attribute should be considered as your
| application's "username" field. Typically, this might be the email
| address of the users but you are free to change this value here.
|
| Out of the box, Fortify expects forgot password and reset password
| requests to have a field named 'email'. If the application uses
| another name for the field you may define it below as needed.
|
*/
'username' => 'email',
'email' => 'email',
/*
|--------------------------------------------------------------------------
| Lowercase Usernames
|--------------------------------------------------------------------------
|
| This value defines whether usernames should be lowercased before saving
| them in the database, as some database system string fields are case
| sensitive. You may disable this for your application if necessary.
|
*/
'lowercase_usernames' => true,
/*
|--------------------------------------------------------------------------
| Home Path
|--------------------------------------------------------------------------
|
| Here you may configure the path where users will get redirected during
| authentication or password reset when the operations are successful
| and the user is authenticated. You are free to change this value.
|
*/
'home' => '/home',
/*
|--------------------------------------------------------------------------
| Fortify Routes Prefix / Subdomain
|--------------------------------------------------------------------------
|
| Here you may specify which prefix Fortify will assign to all the routes
| that it registers with the application. If necessary, you may change
| subdomain under which all of the Fortify routes will be available.
|
*/
'prefix' => '',
'domain' => null,
/*
|--------------------------------------------------------------------------
| Fortify Routes Middleware
|--------------------------------------------------------------------------
|
| Here you may specify which middleware Fortify will assign to the routes
| that it registers with the application. If necessary, you may change
| these middleware but typically this provided default is preferred.
|
*/
'middleware' => ['web'],
/*
|--------------------------------------------------------------------------
| Rate Limiting
|--------------------------------------------------------------------------
|
| By default, Fortify will throttle logins to five requests per minute for
| every email and IP address combination. However, if you would like to
| specify a custom rate limiter to call then you may specify it here.
|
*/
'limiters' => [
'login' => 'login',
'two-factor' => 'two-factor',
'passkeys' => 'passkeys',
],
/*
|--------------------------------------------------------------------------
| Register View Routes
|--------------------------------------------------------------------------
|
| Here you may specify if the routes returning views should be disabled as
| you may not need them when building your own application. This may be
| especially true if you're writing a custom single-page application.
|
*/
'views' => true,
/*
|--------------------------------------------------------------------------
| Passkeys
|--------------------------------------------------------------------------
|
| These settings configure Fortify's passkey (WebAuthn) support. Passkeys
| allow users to sign in without needing to remember credentials since
| they use public-key cryptography - making them immune to breaches.
|
*/
'passkeys' => [
'relying_party_id' => parse_url(config('app.url'), PHP_URL_HOST),
'allowed_origins' => [config('app.url')],
'timeout' => 60000,
],
/*
|--------------------------------------------------------------------------
| Features
|--------------------------------------------------------------------------
|
| Some of the Fortify features are optional. You may disable the features
| by removing them from this array. You're free to only remove some of
| these features or you can even remove all of these if you need to.
|
*/
'features' => [
// Features::registration(), // disabled: user accounts are created by admins only
Features::resetPasswords(),
// Features::emailVerification(),
Features::updateProfileInformation(),
Features::updatePasswords(),
Features::twoFactorAuthentication([
'confirm' => true,
'confirmPassword' => true,
// 'window' => 0,
]),
Features::passkeys([
'confirmPassword' => true,
]),
],
];
-16
View File
@@ -1,16 +0,0 @@
<?php
return [
// Base REST API URL, e.g. https://hotline.example.com/glpi/apirest.php
'api_url' => env('GLPI_API_URL'),
// Identifies this application to GLPI (Setup > General > API > API clients).
'app_token' => env('GLPI_APP_TOKEN'),
// Authenticates as a GLPI account (ideally a dedicated, least-privilege
// service account) — generated from that user's "API tokens" tab.
'user_token' => env('GLPI_USER_TOKEN'),
// How long ticket listings are cached before hitting the GLPI API again.
'cache_ttl_seconds' => env('GLPI_CACHE_TTL', 60),
];
-132
View File
@@ -1,132 +0,0 @@
<?php
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
use Monolog\Processor\PsrLogMessageProcessor;
return [
/*
|--------------------------------------------------------------------------
| Default Log Channel
|--------------------------------------------------------------------------
|
| This option defines the default log channel that is utilized to write
| messages to your logs. The value provided here should match one of
| the channels present in the list of "channels" configured below.
|
*/
'default' => env('LOG_CHANNEL', 'stack'),
/*
|--------------------------------------------------------------------------
| Deprecations Log Channel
|--------------------------------------------------------------------------
|
| This option controls the log channel that should be used to log warnings
| regarding deprecated PHP and library features. This allows you to get
| your application ready for upcoming major versions of dependencies.
|
*/
'deprecations' => [
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
],
/*
|--------------------------------------------------------------------------
| Log Channels
|--------------------------------------------------------------------------
|
| Here you may configure the log channels for your application. Laravel
| utilizes the Monolog PHP logging library, which includes a variety
| of powerful log handlers and formatters that you're free to use.
|
| Available drivers: "single", "daily", "slack", "syslog",
| "errorlog", "monolog", "custom", "stack"
|
*/
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => env('LOG_DAILY_DAYS', 14),
'replace_placeholders' => true,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')),
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
'level' => env('LOG_LEVEL', 'critical'),
'replace_placeholders' => true,
],
'papertrail' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
],
'processors' => [PsrLogMessageProcessor::class],
],
'stderr' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => StreamHandler::class,
'handler_with' => [
'stream' => 'php://stderr',
],
'formatter' => env('LOG_STDERR_FORMATTER'),
'processors' => [PsrLogMessageProcessor::class],
],
'syslog' => [
'driver' => 'syslog',
'level' => env('LOG_LEVEL', 'debug'),
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
'replace_placeholders' => true,
],
'errorlog' => [
'driver' => 'errorlog',
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];
-118
View File
@@ -1,118 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Mailer
|--------------------------------------------------------------------------
|
| This option controls the default mailer that is used to send all email
| messages unless another mailer is explicitly specified when sending
| the message. All additional mailers can be configured within the
| "mailers" array. Examples of each type of mailer are provided.
|
*/
'default' => env('MAIL_MAILER', 'log'),
/*
|--------------------------------------------------------------------------
| Mailer Configurations
|--------------------------------------------------------------------------
|
| Here you may configure all of the mailers used by your application plus
| their respective settings. Several examples have been configured for
| you and you are free to add your own as your application requires.
|
| Laravel supports a variety of mail "transport" drivers that can be used
| when delivering an email. You may specify which one you're using for
| your mailers below. You may also add additional mailers if needed.
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
| "postmark", "resend", "log", "array",
| "failover", "roundrobin"
|
*/
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'scheme' => env('MAIL_SCHEME'),
'url' => env('MAIL_URL'),
'host' => env('MAIL_HOST', '127.0.0.1'),
'port' => env('MAIL_PORT', 2525),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
],
'ses' => [
'transport' => 'ses',
],
'postmark' => [
'transport' => 'postmark',
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
// 'client' => [
// 'timeout' => 5,
// ],
],
'resend' => [
'transport' => 'resend',
],
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
],
'log' => [
'transport' => 'log',
'channel' => env('MAIL_LOG_CHANNEL'),
],
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
'retry_after' => 60,
],
'roundrobin' => [
'transport' => 'roundrobin',
'mailers' => [
'ses',
'postmark',
],
'retry_after' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Global "From" Address
|--------------------------------------------------------------------------
|
| You may wish for all emails sent by your application to be sent from
| the same address. Here you may specify a name and address that is
| used globally for all emails that are sent by your application.
|
*/
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')),
],
];
-20
View File
@@ -1,20 +0,0 @@
<?php
return [
// Base URL of the OnlyOffice Document Server (e.g. http://localhost:8080).
// Must be reachable from the *browser*. The document server itself must
// separately be able to reach this Laravel app (see callback_base_url below).
'document_server_url' => env('ONLYOFFICE_DS_URL', 'http://localhost:8080'),
// Base URL the Document Server container uses to reach THIS Laravel app
// (for the download and callback requests). When the document server runs
// in Docker and Laravel runs on the host machine, APP_URL (e.g.
// http://localhost:8000) is not reachable from inside the container —
// use http://host.docker.internal:8000 instead. Falls back to APP_URL
// when unset (e.g. both running in the same Docker network).
'callback_base_url' => env('ONLYOFFICE_CALLBACK_BASE_URL', env('APP_URL', 'http://localhost')),
// Shared secret used to sign/verify JWTs exchanged with the Document Server
// (editor config sent to the browser, and callback requests received from it).
'jwt_secret' => env('ONLYOFFICE_JWT_SECRET'),
];
-219
View File
@@ -1,219 +0,0 @@
<?php
use Spatie\Permission\DefaultTeamResolver;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
return [
'models' => [
/*
* When using the "HasPermissions" trait from this package, we need to know which
* Eloquent model should be used to retrieve your permissions. Of course, it
* is often just the "Permission" model but you may use whatever you like.
*
* The model you want to use as a Permission model needs to implement the
* `Spatie\Permission\Contracts\Permission` contract.
*/
'permission' => Permission::class,
/*
* When using the "HasRoles" trait from this package, we need to know which
* Eloquent model should be used to retrieve your roles. Of course, it
* is often just the "Role" model but you may use whatever you like.
*
* The model you want to use as a Role model needs to implement the
* `Spatie\Permission\Contracts\Role` contract.
*/
'role' => Role::class,
/*
* When using the "Teams" feature from this package, we need to know which
* Eloquent model should be used to retrieve your teams. Of course, it
* is often just the "Team" model but you may use whatever you like.
*/
'team' => \App\Models\Organization::class,
/*
* When using the "HasModels" trait and passing raw IDs to syncModels,
* attachModels, or detachModels, this model class will be used to
* resolve those IDs. If null, defaults to the guard's model.
*/
'default_model' => null,
],
'table_names' => [
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your roles. We have chosen a basic
* default value but you may easily change it to any table you like.
*/
'roles' => 'roles',
/*
* When using the "HasPermissions" trait from this package, we need to know which
* table should be used to retrieve your permissions. We have chosen a basic
* default value but you may easily change it to any table you like.
*/
'permissions' => 'permissions',
/*
* When using the "HasPermissions" trait from this package, we need to know which
* table should be used to retrieve your models permissions. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'model_has_permissions' => 'model_has_permissions',
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your models roles. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'model_has_roles' => 'model_has_roles',
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your roles permissions. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'role_has_permissions' => 'role_has_permissions',
],
'column_names' => [
/*
* Change this if you want to name the related pivots other than defaults
*/
'role_pivot_key' => null, // default 'role_id',
'permission_pivot_key' => null, // default 'permission_id',
/*
* Change this if you want to name the related model primary key other than
* `model_id`.
*
* For example, this would be nice if your primary keys are all UUIDs. In
* that case, name this `model_uuid`.
*/
'model_morph_key' => 'model_id',
/*
* Change this if you want to use the teams feature and your related model's
* foreign key is other than `team_id`.
*/
'team_foreign_key' => 'organization_id',
],
/*
* When set to true, the method for checking permissions will be registered on the gate.
* Set this to false if you want to implement custom logic for checking permissions.
*/
'register_permission_check_method' => true,
/*
* When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered
* this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
* NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
*/
'register_octane_reset_listener' => false,
/*
* Events will fire when a role or permission is assigned/unassigned:
* \Spatie\Permission\Events\RoleAttachedEvent
* \Spatie\Permission\Events\RoleDetachedEvent
* \Spatie\Permission\Events\PermissionAttachedEvent
* \Spatie\Permission\Events\PermissionDetachedEvent
*
* To enable, set to true, and then create listeners to watch these events.
*/
'events_enabled' => false,
/*
* Teams Feature.
* When set to true the package implements teams using the 'team_foreign_key'.
* If you want the migrations to register the 'team_foreign_key', you must
* set this to true before doing the migration.
* If you already did the migration then you must make a new migration to also
* add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions'
* (view the latest version of this package's migration file)
*/
'teams' => false,
/*
* The class to use to resolve the permissions team id
*/
'team_resolver' => DefaultTeamResolver::class,
/*
* Passport Client Credentials Grant
* When set to true the package will use Passports Client to check permissions
*/
'use_passport_client_credentials' => false,
/*
* When set to true, the required permission names are added to exception messages.
* This could be considered an information leak in some contexts, so the default
* setting is false here for optimum safety.
*/
'display_permission_in_exception' => false,
/*
* When set to true, the required role names are added to exception messages.
* This could be considered an information leak in some contexts, so the default
* setting is false here for optimum safety.
*/
'display_role_in_exception' => false,
/*
* By default wildcard permission lookups are disabled.
* See documentation to understand supported syntax.
*/
'enable_wildcard_permission' => false,
/*
* The class to use for interpreting wildcard permissions.
* If you need to modify delimiters, override the class and specify its name here.
*/
// 'wildcard_permission' => Spatie\Permission\WildcardPermission::class,
/* Cache-specific settings */
'cache' => [
/*
* By default all permissions are cached for 24 hours to speed up performance.
* When permissions or roles are updated the cache is flushed automatically.
*/
'expiration_time' => DateInterval::createFromDateString('24 hours'),
/*
* The cache key used to store all permissions.
*/
'key' => 'spatie.permission.cache',
/*
* You may optionally indicate a specific cache driver to use for permission and
* role caching using any of the `store` drivers listed in the cache.php config
* file. Using 'default' here means to use the `default` set in cache.php.
*/
'store' => 'default',
],
];
-129
View File
@@ -1,129 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Queue Connection Name
|--------------------------------------------------------------------------
|
| Laravel's queue supports a variety of backends via a single, unified
| API, giving you convenient access to each backend using identical
| syntax for each. The default queue connection is defined below.
|
*/
'default' => env('QUEUE_CONNECTION', 'database'),
/*
|--------------------------------------------------------------------------
| Queue Connections
|--------------------------------------------------------------------------
|
| Here you may configure the connection options for every queue backend
| used by your application. An example configuration is provided for
| each backend supported by Laravel. You're also free to add more.
|
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
| "deferred", "background", "failover", "null"
|
*/
'connections' => [
'sync' => [
'driver' => 'sync',
],
'database' => [
'driver' => 'database',
'connection' => env('DB_QUEUE_CONNECTION'),
'table' => env('DB_QUEUE_TABLE', 'jobs'),
'queue' => env('DB_QUEUE', 'default'),
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
'after_commit' => false,
],
'beanstalkd' => [
'driver' => 'beanstalkd',
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
'queue' => env('BEANSTALKD_QUEUE', 'default'),
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
'block_for' => 0,
'after_commit' => false,
],
'sqs' => [
'driver' => 'sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'after_commit' => false,
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
'block_for' => null,
'after_commit' => false,
],
'deferred' => [
'driver' => 'deferred',
],
'background' => [
'driver' => 'background',
],
'failover' => [
'driver' => 'failover',
'connections' => [
'database',
'deferred',
],
],
],
/*
|--------------------------------------------------------------------------
| Job Batching
|--------------------------------------------------------------------------
|
| The following options configure the database and table that store job
| batching information. These options can be updated to any database
| connection and table which has been defined by your application.
|
*/
'batching' => [
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'job_batches',
],
/*
|--------------------------------------------------------------------------
| Failed Queue Jobs
|--------------------------------------------------------------------------
|
| These options configure the behavior of failed queue job logging so you
| can control how and where failed jobs are stored. Laravel ships with
| support for storing failed jobs in a simple file or in a database.
|
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
*/
'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'failed_jobs',
],
];
-87
View File
@@ -1,87 +0,0 @@
<?php
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
use Laravel\Sanctum\Http\Middleware\AuthenticateSession;
use Laravel\Sanctum\Sanctum;
return [
/*
|--------------------------------------------------------------------------
| Stateful Domains
|--------------------------------------------------------------------------
|
| Requests from the following domains / hosts will receive stateful API
| authentication cookies. Typically, these should include your local
| and production domains which access your API via a frontend SPA.
|
*/
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
Sanctum::currentApplicationUrlWithPort(),
// Sanctum::currentRequestHost(),
))),
/*
|--------------------------------------------------------------------------
| Sanctum Guards
|--------------------------------------------------------------------------
|
| This array contains the authentication guards that will be checked when
| Sanctum is trying to authenticate a request. If none of these guards
| are able to authenticate the request, Sanctum will use the bearer
| token that's present on an incoming request for authentication.
|
*/
'guard' => ['web'],
/*
|--------------------------------------------------------------------------
| Expiration Minutes
|--------------------------------------------------------------------------
|
| This value controls the number of minutes until an issued token will be
| considered expired. This will override any values set in the token's
| "expires_at" attribute, but first-party sessions are not affected.
|
*/
'expiration' => null,
/*
|--------------------------------------------------------------------------
| Token Prefix
|--------------------------------------------------------------------------
|
| Sanctum can prefix new tokens in order to take advantage of numerous
| security scanning initiatives maintained by open source platforms
| that notify developers if they commit tokens into repositories.
|
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
*/
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
/*
|--------------------------------------------------------------------------
| Sanctum Middleware
|--------------------------------------------------------------------------
|
| When authenticating your first-party SPA with Sanctum you may need to
| customize some of the middleware Sanctum uses while processing the
| request. You may change the middleware listed below as required.
|
*/
'middleware' => [
'authenticate_session' => AuthenticateSession::class,
'encrypt_cookies' => EncryptCookies::class,
'validate_csrf_token' => ValidateCsrfToken::class,
],
];
-210
View File
@@ -1,210 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Search Engine
|--------------------------------------------------------------------------
|
| This option controls the default search connection that gets used while
| using Laravel Scout. This connection is used when syncing all models
| to the search service. You should adjust this based on your needs.
|
| Supported: "algolia", "meilisearch", "typesense",
| "database", "collection", "null"
|
*/
'driver' => env('SCOUT_DRIVER', 'collection'),
/*
|--------------------------------------------------------------------------
| Index Prefix
|--------------------------------------------------------------------------
|
| Here you may specify a prefix that will be applied to all search index
| names used by Scout. This prefix may be useful if you have multiple
| "tenants" or applications sharing the same search infrastructure.
|
*/
'prefix' => env('SCOUT_PREFIX', ''),
/*
|--------------------------------------------------------------------------
| Queue Data Syncing
|--------------------------------------------------------------------------
|
| This option allows you to control if the operations that sync your data
| with your search engines are queued. When this is set to "true" then
| all automatic data syncing will get queued for better performance.
|
*/
'queue' => env('SCOUT_QUEUE', false),
/*
|--------------------------------------------------------------------------
| Database Transactions
|--------------------------------------------------------------------------
|
| This configuration option determines if your data will only be synced
| with your search indexes after every open database transaction has
| been committed, thus preventing any discarded data from syncing.
|
*/
'after_commit' => false,
/*
|--------------------------------------------------------------------------
| Chunk Sizes
|--------------------------------------------------------------------------
|
| These options allow you to control the maximum chunk size when you are
| mass importing data into the search engine. This allows you to fine
| tune each of these chunk sizes based on the power of the servers.
|
*/
'chunk' => [
'searchable' => 500,
'unsearchable' => 500,
],
/*
|--------------------------------------------------------------------------
| Soft Deletes
|--------------------------------------------------------------------------
|
| This option allows to control whether to keep soft deleted records in
| the search indexes. Maintaining soft deleted records can be useful
| if your application still needs to search for the records later.
|
*/
'soft_delete' => false,
/*
|--------------------------------------------------------------------------
| Identify User
|--------------------------------------------------------------------------
|
| This option allows you to control whether to notify the search engine
| of the user performing the search. This is sometimes useful if the
| engine supports any analytics based on this application's users.
|
| Supported engines: "algolia"
|
*/
'identify' => env('SCOUT_IDENTIFY', false),
/*
|--------------------------------------------------------------------------
| Algolia Configuration
|--------------------------------------------------------------------------
|
| Here you may configure your Algolia settings. Algolia is a cloud hosted
| search engine which works great with Scout out of the box. Just plug
| in your application ID and admin API key to get started searching.
|
*/
'algolia' => [
'id' => env('ALGOLIA_APP_ID', ''),
'secret' => env('ALGOLIA_SECRET', ''),
'index-settings' => [
// 'users' => [
// 'searchableAttributes' => ['id', 'name', 'email'],
// 'attributesForFaceting'=> ['filterOnly(email)'],
// ],
],
],
/*
|--------------------------------------------------------------------------
| Meilisearch Configuration
|--------------------------------------------------------------------------
|
| Here you may configure your Meilisearch settings. Meilisearch is an open
| source search engine with minimal configuration. Below, you can state
| the host and key information for your own Meilisearch installation.
|
| See: https://www.meilisearch.com/docs/learn/configuration/instance_options#all-instance-options
|
*/
'meilisearch' => [
'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
'key' => env('MEILISEARCH_KEY'),
'index-settings' => [
// 'users' => [
// 'filterableAttributes'=> ['id', 'name', 'email'],
// ],
],
],
/*
|--------------------------------------------------------------------------
| Typesense Configuration
|--------------------------------------------------------------------------
|
| Here you may configure your Typesense settings. Typesense is an open
| source search engine using minimal configuration. Below, you will
| state the host, key, and schema configuration for the instance.
|
*/
'typesense' => [
'client-settings' => [
'api_key' => env('TYPESENSE_API_KEY', 'xyz'),
'nodes' => [
[
'host' => env('TYPESENSE_HOST', 'localhost'),
'port' => env('TYPESENSE_PORT', '8108'),
'path' => env('TYPESENSE_PATH', ''),
'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
],
],
'nearest_node' => [
'host' => env('TYPESENSE_HOST', 'localhost'),
'port' => env('TYPESENSE_PORT', '8108'),
'path' => env('TYPESENSE_PATH', ''),
'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
],
'connection_timeout_seconds' => env('TYPESENSE_CONNECTION_TIMEOUT_SECONDS', 2),
'healthcheck_interval_seconds' => env('TYPESENSE_HEALTHCHECK_INTERVAL_SECONDS', 30),
'num_retries' => env('TYPESENSE_NUM_RETRIES', 3),
'retry_interval_seconds' => env('TYPESENSE_RETRY_INTERVAL_SECONDS', 1),
],
// 'max_total_results' => env('TYPESENSE_MAX_TOTAL_RESULTS', 1000),
'model-settings' => [
// User::class => [
// 'collection-schema' => [
// 'fields' => [
// [
// 'name' => 'id',
// 'type' => 'string',
// ],
// [
// 'name' => 'name',
// 'type' => 'string',
// ],
// [
// 'name' => 'created_at',
// 'type' => 'int64',
// ],
// ],
// 'default_sorting_field' => 'created_at',
// ],
// 'search-parameters' => [
// 'query_by' => 'name'
// ],
// ],
],
'import_action' => env('TYPESENSE_IMPORT_ACTION', 'upsert'),
],
];
-38
View File
@@ -1,38 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Third Party Services
|--------------------------------------------------------------------------
|
| This file is for storing the credentials for third party services such
| as Mailgun, Postmark, AWS and more. This file provides the de facto
| location for this type of information, allowing packages to have
| a conventional file to locate the various service credentials.
|
*/
'postmark' => [
'key' => env('POSTMARK_API_KEY'),
],
'resend' => [
'key' => env('RESEND_API_KEY'),
],
'ses' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
'slack' => [
'notifications' => [
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
],
],
];
-233
View File
@@ -1,233 +0,0 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Session Driver
|--------------------------------------------------------------------------
|
| This option determines the default session driver that is utilized for
| incoming requests. Laravel supports a variety of storage options to
| persist session data. Database storage is a great default choice.
|
| Supported: "file", "cookie", "database", "memcached",
| "redis", "dynamodb", "array"
|
*/
'driver' => env('SESSION_DRIVER', 'database'),
/*
|--------------------------------------------------------------------------
| Session Lifetime
|--------------------------------------------------------------------------
|
| Here you may specify the number of minutes that you wish the session
| to be allowed to remain idle before it expires. If you want them
| to expire immediately when the browser is closed then you may
| indicate that via the expire_on_close configuration option.
|
*/
'lifetime' => (int) env('SESSION_LIFETIME', 120),
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
/*
|--------------------------------------------------------------------------
| Session Encryption
|--------------------------------------------------------------------------
|
| This option allows you to easily specify that all of your session data
| should be encrypted before it's stored. All encryption is performed
| automatically by Laravel and you may use the session like normal.
|
*/
'encrypt' => env('SESSION_ENCRYPT', false),
/*
|--------------------------------------------------------------------------
| Session File Location
|--------------------------------------------------------------------------
|
| When utilizing the "file" session driver, the session files are placed
| on disk. The default storage location is defined here; however, you
| are free to provide another location where they should be stored.
|
*/
'files' => storage_path('framework/sessions'),
/*
|--------------------------------------------------------------------------
| Session Database Connection
|--------------------------------------------------------------------------
|
| When using the "database" or "redis" session drivers, you may specify a
| connection that should be used to manage these sessions. This should
| correspond to a connection in your database configuration options.
|
*/
'connection' => env('SESSION_CONNECTION'),
/*
|--------------------------------------------------------------------------
| Session Database Table
|--------------------------------------------------------------------------
|
| When using the "database" session driver, you may specify the table to
| be used to store sessions. Of course, a sensible default is defined
| for you; however, you're welcome to change this to another table.
|
*/
'table' => env('SESSION_TABLE', 'sessions'),
/*
|--------------------------------------------------------------------------
| Session Cache Store
|--------------------------------------------------------------------------
|
| When using one of the framework's cache driven session backends, you may
| define the cache store which should be used to store the session data
| between requests. This must match one of your defined cache stores.
|
| Affects: "dynamodb", "memcached", "redis"
|
*/
'store' => env('SESSION_STORE'),
/*
|--------------------------------------------------------------------------
| Session Sweeping Lottery
|--------------------------------------------------------------------------
|
| Some session drivers must manually sweep their storage location to get
| rid of old sessions from storage. Here are the chances that it will
| happen on a given request. By default, the odds are 2 out of 100.
|
*/
'lottery' => [2, 100],
/*
|--------------------------------------------------------------------------
| Session Cookie Name
|--------------------------------------------------------------------------
|
| Here you may change the name of the session cookie that is created by
| the framework. Typically, you should not need to change this value
| since doing so does not grant a meaningful security improvement.
|
*/
'cookie' => env(
'SESSION_COOKIE',
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
),
/*
|--------------------------------------------------------------------------
| Session Cookie Path
|--------------------------------------------------------------------------
|
| The session cookie path determines the path for which the cookie will
| be regarded as available. Typically, this will be the root path of
| your application, but you're free to change this when necessary.
|
*/
'path' => env('SESSION_PATH', '/'),
/*
|--------------------------------------------------------------------------
| Session Cookie Domain
|--------------------------------------------------------------------------
|
| This value determines the domain and subdomains the session cookie is
| available to. By default, the cookie will be available to the root
| domain without subdomains. Typically, this shouldn't be changed.
|
*/
'domain' => env('SESSION_DOMAIN'),
/*
|--------------------------------------------------------------------------
| HTTPS Only Cookies
|--------------------------------------------------------------------------
|
| By setting this option to true, session cookies will only be sent back
| to the server if the browser has a HTTPS connection. This will keep
| the cookie from being sent to you when it can't be done securely.
|
*/
'secure' => env('SESSION_SECURE_COOKIE'),
/*
|--------------------------------------------------------------------------
| HTTP Access Only
|--------------------------------------------------------------------------
|
| Setting this value to true will prevent JavaScript from accessing the
| value of the cookie and the cookie will only be accessible through
| the HTTP protocol. It's unlikely you should disable this option.
|
*/
'http_only' => env('SESSION_HTTP_ONLY', true),
/*
|--------------------------------------------------------------------------
| Same-Site Cookies
|--------------------------------------------------------------------------
|
| This option determines how your cookies behave when cross-site requests
| take place, and can be used to mitigate CSRF attacks. By default, we
| will set this value to "lax" to permit secure cross-site requests.
|
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
| Supported: "lax", "strict", "none", null
|
*/
'same_site' => env('SESSION_SAME_SITE', 'lax'),
/*
|--------------------------------------------------------------------------
| Partitioned Cookies
|--------------------------------------------------------------------------
|
| Setting this value to true will tie the cookie to the top-level site for
| a cross-site context. Partitioned cookies are accepted by the browser
| when flagged "secure" and the Same-Site attribute is set to "none".
|
*/
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
/*
|--------------------------------------------------------------------------
| Session Serialization
|--------------------------------------------------------------------------
|
| This value controls the serialization strategy for session data, which
| is JSON by default. Setting this to "php" allows the storage of PHP
| objects in the session but can make an application vulnerable to
| "gadget chain" serialization attacks if the APP_KEY is leaked.
|
| Supported: "json", "php"
|
*/
'serialization' => 'json',
];
-1
View File
@@ -1 +0,0 @@
*.sqlite*
-45
View File
@@ -1,45 +0,0 @@
<?php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends Factory<User>
*/
class UserFactory extends Factory
{
/**
* The current password being used by the factory.
*/
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}
@@ -1,49 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};
@@ -1,35 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->primary();
$table->mediumText('value');
$table->bigInteger('expiration')->index();
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->bigInteger('expiration')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};
@@ -1,59 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedSmallInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('connection');
$table->string('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
$table->index(['connection', 'queue', 'failed_at']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('failed_jobs');
}
};
@@ -1,27 +0,0 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateActivityLogTable extends Migration
{
public function up()
{
Schema::connection(config('activitylog.database_connection'))->create(config('activitylog.table_name'), function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('log_name')->nullable();
$table->text('description');
$table->nullableMorphs('subject', 'subject');
$table->nullableMorphs('causer', 'causer');
$table->json('properties')->nullable();
$table->timestamps();
$table->index('log_name');
});
}
public function down()
{
Schema::connection(config('activitylog.database_connection'))->dropIfExists(config('activitylog.table_name'));
}
}
@@ -1,137 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$teams = config('permission.teams');
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
throw_if(empty($tableNames), 'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), 'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
/**
* See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered.
*/
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
$table->id(); // permission id
$table->string('name');
$table->string('guard_name');
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
/**
* See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered.
*/
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
$table->id(); // role id
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
}
$table->string('name');
$table->string('guard_name');
$table->timestamps();
if ($teams || config('permission.testing')) {
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
} else {
$table->unique(['name', 'guard_name']);
}
});
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
$table->unsignedBigInteger($pivotPermission);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->cascadeOnDelete();
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
} else {
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
}
});
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
$table->unsignedBigInteger($pivotRole);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->cascadeOnDelete();
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
} else {
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
}
});
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
$table->unsignedBigInteger($pivotPermission);
$table->unsignedBigInteger($pivotRole);
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->cascadeOnDelete();
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->cascadeOnDelete();
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
});
app('cache')
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
->forget(config('permission.cache.key'));
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$tableNames = config('permission.table_names');
throw_if(empty($tableNames), 'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
Schema::dropIfExists($tableNames['role_has_permissions']);
Schema::dropIfExists($tableNames['model_has_roles']);
Schema::dropIfExists($tableNames['model_has_permissions']);
Schema::dropIfExists($tableNames['roles']);
Schema::dropIfExists($tableNames['permissions']);
}
};
@@ -1,33 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->text('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable()->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('personal_access_tokens');
}
};
@@ -1,22 +0,0 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddEventColumnToActivityLogTable extends Migration
{
public function up()
{
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
$table->string('event')->nullable()->after('subject_type');
});
}
public function down()
{
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
$table->dropColumn('event');
});
}
}
@@ -1,22 +0,0 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddBatchUuidColumnToActivityLogTable extends Migration
{
public function up()
{
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
$table->uuid('batch_uuid')->nullable()->after('properties');
});
}
public function down()
{
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
$table->dropColumn('batch_uuid');
});
}
}
@@ -1,28 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('organizations', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->string('org_type')->nullable(); // client, internal, vendor
$table->string('status')->default('active'); // active, inactive
$table->text('description')->nullable();
$table->json('allowed_ip_ranges')->nullable(); // CIDR list for IP access control
$table->timestamps();
$table->softDeletes();
});
}
public function down(): void
{
Schema::dropIfExists('organizations');
}
};
@@ -1,31 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('locations', function (Blueprint $table) {
$table->id();
$table->foreignId('organization_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('address_line_1')->nullable();
$table->string('address_line_2')->nullable();
$table->string('city')->nullable();
$table->string('region')->nullable();
$table->string('postal_code')->nullable();
$table->string('country')->nullable();
$table->string('phone')->nullable();
$table->boolean('is_primary')->default(false);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('locations');
}
};
@@ -1,23 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('configuration_types', function (Blueprint $table) {
$table->id();
$table->string('name'); // Workstation, Server, Network Device, Telephone...
$table->string('icon')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('configuration_types');
}
};
@@ -1,23 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('configuration_statuses', function (Blueprint $table) {
$table->id();
$table->string('name'); // Active, Inactive, Retired...
$table->string('color')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('configuration_statuses');
}
};
@@ -1,42 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('configurations', function (Blueprint $table) {
$table->id();
$table->foreignId('organization_id')->constrained()->cascadeOnDelete();
$table->foreignId('configuration_type_id')->constrained()->restrictOnDelete();
$table->foreignId('configuration_status_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('location_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('contact_id')->nullable()->constrained()->nullOnDelete();
$table->string('name');
$table->string('hostname')->nullable();
$table->string('primary_ip')->nullable();
$table->string('mac_address')->nullable();
$table->string('serial_number')->nullable();
$table->string('asset_tag')->nullable();
$table->string('manufacturer')->nullable();
$table->string('model')->nullable();
$table->string('operating_system')->nullable();
$table->date('installed_at')->nullable();
$table->date('warranty_expires_at')->nullable();
$table->text('notes')->nullable();
$table->boolean('is_archived')->default(false);
$table->timestamps();
$table->softDeletes();
$table->index(['organization_id', 'name']);
});
}
public function down(): void
{
Schema::dropIfExists('configurations');
}
};
@@ -1,32 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('contacts', function (Blueprint $table) {
$table->id();
$table->foreignId('organization_id')->constrained()->cascadeOnDelete();
$table->foreignId('location_id')->nullable()->constrained()->nullOnDelete();
$table->string('first_name');
$table->string('last_name');
$table->string('title')->nullable();
$table->string('email')->nullable();
$table->string('phone')->nullable();
$table->string('mobile')->nullable();
$table->boolean('is_important')->default(false);
$table->text('notes')->nullable();
$table->timestamps();
$table->softDeletes();
});
}
public function down(): void
{
Schema::dropIfExists('contacts');
}
};
@@ -1,37 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('passwords', function (Blueprint $table) {
$table->id();
$table->foreignId('organization_id')->constrained()->cascadeOnDelete();
// optional link to the asset this credential belongs to (polymorphic: configuration, flexible_asset...)
$table->nullableMorphs('resource');
$table->string('name');
$table->string('username')->nullable();
$table->text('password_ciphertext'); // AES-256-GCM envelope-encrypted payload
$table->string('url')->nullable();
$table->text('notes')->nullable();
$table->string('otp_secret')->nullable(); // for OTP generator feature, also encrypted at rest via cast
$table->boolean('is_favorite')->default(false);
$table->timestamp('password_updated_at')->nullable();
$table->timestamp('rotation_due_at')->nullable(); // for scheduled rotation
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->softDeletes();
$table->index(['organization_id', 'name']);
});
}
public function down(): void
{
Schema::dropIfExists('passwords');
}
};
@@ -1,27 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
// Append-only. No updated_at, no soft deletes, no update/delete performed by app code.
Schema::create('password_audit_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('password_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('action'); // viewed, revealed, copied, created, updated, deleted, rotated
$table->string('ip_address', 45)->nullable();
$table->string('user_agent')->nullable();
$table->timestamp('created_at')->useCurrent();
});
}
public function down(): void
{
Schema::dropIfExists('password_audit_logs');
}
};
@@ -1,25 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('flexible_asset_types', function (Blueprint $table) {
$table->id();
$table->string('name'); // e.g. "SSL Certificate", "Wireless Network", "Warranty"
$table->string('icon')->nullable();
$table->text('description')->nullable();
$table->boolean('enabled_for_all_organizations')->default(true);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('flexible_asset_types');
}
};
@@ -1,31 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('flexible_asset_fields', function (Blueprint $table) {
$table->id();
$table->foreignId('flexible_asset_type_id')->constrained()->cascadeOnDelete();
$table->string('name'); // machine key, e.g. "ip_address"
$table->string('label'); // display label, e.g. "IP Address"
$table->string('field_type'); // text, textarea, password, checkbox, date, tag, select, related_asset
$table->json('options')->nullable(); // select choices, related_asset target type, etc.
$table->boolean('is_required')->default(false);
$table->boolean('show_in_list')->default(false);
$table->unsignedInteger('sort_order')->default(0);
$table->timestamps();
$table->unique(['flexible_asset_type_id', 'name']);
});
}
public function down(): void
{
Schema::dropIfExists('flexible_asset_fields');
}
};
@@ -1,30 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('flexible_assets', function (Blueprint $table) {
$table->id();
$table->foreignId('flexible_asset_type_id')->constrained()->restrictOnDelete();
$table->foreignId('organization_id')->constrained()->cascadeOnDelete();
$table->string('name'); // primary display value, denormalized from data for listing/search
$table->json('data'); // { field_name: value, ... } validated against flexible_asset_fields
$table->boolean('is_archived')->default(false);
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->softDeletes();
$table->index(['organization_id', 'flexible_asset_type_id']);
});
}
public function down(): void
{
Schema::dropIfExists('flexible_assets');
}
};
@@ -1,28 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
// Directional edge; UI queries both directions to show a symmetric "Related Items" panel.
Schema::create('related_items', function (Blueprint $table) {
$table->id();
$table->morphs('item'); // owning side
$table->morphs('related_item'); // linked side
$table->string('notes')->nullable();
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->unique(['item_type', 'item_id', 'related_item_type', 'related_item_id'], 'related_items_unique');
});
}
public function down(): void
{
Schema::dropIfExists('related_items');
}
};
@@ -1,43 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('documents', function (Blueprint $table) {
$table->id();
$table->foreignId('organization_id')->constrained()->cascadeOnDelete();
$table->foreignId('folder_id')->nullable(); // self-referencing folder tree, FK added after table exists
$table->string('name');
$table->longText('content')->nullable(); // rich text body (Tiptap JSON/HTML)
$table->string('file_path')->nullable(); // for uploaded (non rich-text) documents
$table->unsignedInteger('current_version')->default(1);
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->softDeletes();
});
Schema::table('documents', function (Blueprint $table) {
$table->foreign('folder_id')->references('id')->on('documents')->nullOnDelete();
});
Schema::create('document_versions', function (Blueprint $table) {
$table->id();
$table->foreignId('document_id')->constrained()->cascadeOnDelete();
$table->unsignedInteger('version');
$table->longText('content')->nullable();
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('created_at')->useCurrent();
});
}
public function down(): void
{
Schema::dropIfExists('document_versions');
Schema::dropIfExists('documents');
}
};
@@ -1,27 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('domains', function (Blueprint $table) {
$table->id();
$table->foreignId('organization_id')->constrained()->cascadeOnDelete();
$table->string('name')->unique(); // domain name
$table->string('registrar')->nullable();
$table->date('expires_at')->nullable();
$table->boolean('auto_renew')->default(false);
$table->timestamp('last_checked_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('domains');
}
};
@@ -1,28 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('ssl_certificates', function (Blueprint $table) {
$table->id();
$table->foreignId('organization_id')->constrained()->cascadeOnDelete();
$table->foreignId('domain_id')->nullable()->constrained()->nullOnDelete();
$table->string('name'); // hostname/CN
$table->string('issuer')->nullable();
$table->date('issued_at')->nullable();
$table->date('expires_at')->nullable();
$table->timestamp('last_checked_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('ssl_certificates');
}
};
@@ -1,37 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
// MSP staff account; null = client-portal user scoped only via organization_user pivot
$table->boolean('is_msp_staff')->default(true)->after('password');
// two_factor_* columns owned by Fortify's own migration (add_two_factor_columns_to_users_table)
$table->timestamp('last_login_at')->nullable()->after('is_msp_staff');
$table->string('last_login_ip', 45)->nullable()->after('last_login_at');
});
Schema::create('organization_user', function (Blueprint $table) {
$table->id();
$table->foreignId('organization_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->timestamps();
$table->unique(['organization_id', 'user_id']);
});
}
public function down(): void
{
Schema::dropIfExists('organization_user');
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['is_msp_staff', 'last_login_at', 'last_login_ip']);
});
}
};
@@ -1,42 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->text('two_factor_secret')
->after('password')
->nullable();
$table->text('two_factor_recovery_codes')
->after('two_factor_secret')
->nullable();
$table->timestamp('two_factor_confirmed_at')
->after('two_factor_recovery_codes')
->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn([
'two_factor_secret',
'two_factor_recovery_codes',
'two_factor_confirmed_at',
]);
});
}
};
@@ -1,35 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Laravel\Passkeys\Passkeys;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('passkeys', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(Passkeys::userModel(), 'user_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('credential_id')->unique();
$table->json('credential');
$table->timestamp('last_used_at')->nullable();
$table->timestamps();
$table->index('user_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('passkeys');
}
};
@@ -1,24 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('documents', function (Blueprint $table) {
$table->string('type')->default('text')->after('name'); // text|office
$table->string('office_extension')->nullable()->after('file_path'); // docx|xlsx|pptx
$table->string('office_key')->nullable()->after('office_extension'); // OnlyOffice cache-busting version key
});
}
public function down(): void
{
Schema::table('documents', function (Blueprint $table) {
$table->dropColumn(['type', 'office_extension', 'office_key']);
});
}
};

Some files were not shown because too many files have changed in this diff Show More