Compare commits
5 Commits
main
...
feature/sa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f25f4acaaf | ||
|
|
41caefece3 | ||
|
|
abca346b3e | ||
|
|
6a3de5847d | ||
|
|
63e448ef22 |
40
app/Console/Commands/MakeSuperAdmin.php
Normal file
40
app/Console/Commands/MakeSuperAdmin.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Role;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class MakeSuperAdmin extends Command
|
||||
{
|
||||
protected $signature = 'make:superadmin {email}';
|
||||
protected $description = 'Promote a user to SuperAdmin';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$email = $this->argument('email');
|
||||
$user = User::withoutGlobalScope('structure')->where('email', $email)->first();
|
||||
|
||||
if (!$user) {
|
||||
$this->error("Utilisateur non trouvé.");
|
||||
return;
|
||||
}
|
||||
|
||||
// S'assurer que le rôle SuperAdmin existe (globalement)
|
||||
$role = Role::withoutGlobalScope('structure')->firstOrCreate(
|
||||
['name' => 'SuperAdmin', 'guard_name' => 'web'],
|
||||
['structure_id' => null] // Rôle global
|
||||
);
|
||||
|
||||
// Assigner le rôle sur le contexte de l'utilisateur (ou structure 1 par défaut pour le CABM)
|
||||
setPermissionsTeamId($user->structure_id ?? 1);
|
||||
|
||||
if (!$user->hasRole('SuperAdmin')) {
|
||||
$user->assignRole($role);
|
||||
}
|
||||
|
||||
$this->info("Félicitations ! L'utilisateur {$email} a été promu SuperAdmin.");
|
||||
}
|
||||
}
|
||||
@@ -31,17 +31,41 @@ class RegisteredUserController extends Controller
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'structure_name' => 'required|string|max:255|unique:structures,name',
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|lowercase|email|max:255|unique:'.User::class,
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
// 1. Créer la structure (Tenant)
|
||||
// Le slug est généré avec un uniqid() pour éviter les conflits si deux noms produisent le même slug.
|
||||
$structure = \App\Models\Structure::create([
|
||||
'name' => $request->structure_name,
|
||||
'slug' => \Illuminate\Support\Str::slug($request->structure_name) . '-' . substr(uniqid(), -5),
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
// 2. Définir le contexte Tenant pour que Spatie attache les rôles à cette structure-ci
|
||||
config(['tenant.structure_id' => $structure->id]);
|
||||
setPermissionsTeamId($structure->id);
|
||||
|
||||
// 3. Création des rôles par défaut pour le nouveau locataire
|
||||
$adminRole = \App\Models\Role::firstOrCreate(['name' => 'Admin']);
|
||||
\App\Models\Role::firstOrCreate(['name' => 'Agent']);
|
||||
\App\Models\Role::firstOrCreate(['name' => 'Manager']);
|
||||
\App\Models\Role::firstOrCreate(['name' => 'RH']);
|
||||
|
||||
// 4. Créer le premier compte Administrateur
|
||||
$user = User::withoutGlobalScope('structure')->create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
'structure_id' => $structure->id,
|
||||
]);
|
||||
|
||||
// Affectation du rôle
|
||||
$user->assignRole($adminRole);
|
||||
|
||||
event(new Registered($user));
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
@@ -20,7 +20,7 @@ class DashboardController extends Controller
|
||||
return $this->adminDashboard();
|
||||
}
|
||||
|
||||
if ($user->hasRole('RH')) {
|
||||
if ($user->can('validate rh')) {
|
||||
return $this->rhDashboard();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use App\Models\Role;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
class RoleController extends Controller
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace App\Http\Controllers;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use App\Models\Role;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
class ServiceController extends Controller
|
||||
|
||||
121
app/Http/Controllers/SuperAdminController.php
Normal file
121
app/Http/Controllers/SuperAdminController.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use App\Models\Structure;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
|
||||
class SuperAdminController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
// On s'assure que seul un SuperAdmin peut accéder ici
|
||||
if (!auth()->user()->hasRole('SuperAdmin')) {
|
||||
abort(403, 'Accès refusé. Vous devez être SuperAdmin.');
|
||||
}
|
||||
|
||||
return Inertia::render('SuperAdmin/Index', [
|
||||
'structures' => fn() => Structure::withCount(['users' => function ($query) {
|
||||
$query->withoutGlobalScope('structure');
|
||||
}])->get(),
|
||||
'current_structure_id' => session('target_structure_id'),
|
||||
'db_latency' => fn() => $this->calculateDbLatency(),
|
||||
'db_type' => DB::connection()->getDriverName()
|
||||
]);
|
||||
}
|
||||
|
||||
private function calculateDbLatency()
|
||||
{
|
||||
$start = microtime(true);
|
||||
DB::select('SELECT 1');
|
||||
return round((microtime(true) - $start) * 1000, 2);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
if (!auth()->user()->hasRole('SuperAdmin')) { abort(403); }
|
||||
|
||||
return Inertia::render('SuperAdmin/Create');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
if (!auth()->user()->hasRole('SuperAdmin')) { abort(403); }
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'slug' => 'required|string|max:255|unique:structures',
|
||||
'domain' => 'nullable|string|max:255|unique:structures',
|
||||
'is_active' => 'boolean'
|
||||
]);
|
||||
|
||||
Structure::create($validated);
|
||||
|
||||
return redirect()->route('superadmin.index')->with('success', 'Structure créée avec succès.');
|
||||
}
|
||||
|
||||
public function edit(Structure $structure)
|
||||
{
|
||||
if (!auth()->user()->hasRole('SuperAdmin')) { abort(403); }
|
||||
|
||||
return Inertia::render('SuperAdmin/Edit', [
|
||||
'structure' => $structure
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, Structure $structure)
|
||||
{
|
||||
if (!auth()->user()->hasRole('SuperAdmin')) { abort(403); }
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'slug' => 'required|string|max:255|unique:structures,slug,' . $structure->id,
|
||||
'domain' => 'nullable|string|max:255|unique:structures,domain,' . $structure->id,
|
||||
'is_active' => 'boolean'
|
||||
]);
|
||||
|
||||
$structure->update($validated);
|
||||
|
||||
return redirect()->route('superadmin.index')->with('success', 'Structure mise à jour.');
|
||||
}
|
||||
|
||||
public function destroy(Structure $structure)
|
||||
{
|
||||
if (!auth()->user()->hasRole('SuperAdmin')) { abort(403); }
|
||||
|
||||
if (Structure::count() <= 1) {
|
||||
return redirect()->back()->with('error', 'Impossible de supprimer la dernière structure.');
|
||||
}
|
||||
|
||||
$structure->delete();
|
||||
|
||||
return redirect()->route('superadmin.index')->with('success', 'Structure supprimée avec succès.');
|
||||
}
|
||||
|
||||
public function switchStructure(Request $request, Structure $structure)
|
||||
{
|
||||
if (!auth()->user()->hasRole('SuperAdmin')) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
// On enregistre dans la session qu'on veut "impersonner" cette structure
|
||||
$request->session()->put('target_structure_id', $structure->id);
|
||||
|
||||
return redirect()->route('dashboard')->with('success', "Vous naviguez maintenant sur la structure : {$structure->name}.");
|
||||
}
|
||||
|
||||
public function resetStructure(Request $request)
|
||||
{
|
||||
if (!auth()->user()->hasRole('SuperAdmin')) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
// On retire l'impersonnation, on redevient un SuperAdmin "Global"
|
||||
$request->session()->forget('target_structure_id');
|
||||
|
||||
return redirect()->route('superadmin.index')->with('success', "Périmètre global restauré.");
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class UserController extends Controller implements \Illuminate\Routing\Controlle
|
||||
public function create()
|
||||
{
|
||||
return Inertia::render('User/Edit', [
|
||||
'roles' => \Spatie\Permission\Models\Role::all(),
|
||||
'roles' => \App\Models\Role::all(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,10 @@ class UserController extends Controller implements \Illuminate\Routing\Controlle
|
||||
]);
|
||||
|
||||
if (isset($validated['roles'])) {
|
||||
setPermissionsTeamId($user->structure_id);
|
||||
$user->syncRoles($validated['roles']);
|
||||
// Restaurer le contexte actuel après
|
||||
setPermissionsTeamId(config('tenant.structure_id'));
|
||||
}
|
||||
|
||||
return redirect()->route('users.index')->with('success', 'Utilisateur créé avec succès.');
|
||||
@@ -60,7 +63,7 @@ class UserController extends Controller implements \Illuminate\Routing\Controlle
|
||||
{
|
||||
return Inertia::render('User/Edit', [
|
||||
'user' => $user->load('roles'),
|
||||
'roles' => \Spatie\Permission\Models\Role::all(),
|
||||
'roles' => \App\Models\Role::all(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -85,9 +88,10 @@ class UserController extends Controller implements \Illuminate\Routing\Controlle
|
||||
}
|
||||
|
||||
if (isset($validated['roles'])) {
|
||||
// Prevent removing own admin role if it's the only one left?
|
||||
// For simplicity, just allow sync.
|
||||
setPermissionsTeamId($user->structure_id);
|
||||
$user->syncRoles($validated['roles']);
|
||||
// Restaurer le contexte actuel après
|
||||
setPermissionsTeamId(config('tenant.structure_id'));
|
||||
}
|
||||
|
||||
return redirect()->route('users.index')->with('success', 'Utilisateur mis à jour.');
|
||||
|
||||
@@ -32,7 +32,22 @@ class HandleInertiaRequests extends Middleware
|
||||
return [
|
||||
...parent::share($request),
|
||||
'auth' => [
|
||||
'user' => $request->user() ? $request->user()->load('roles') : null,
|
||||
'user' => $request->user()
|
||||
? $request->user()->load([
|
||||
'roles' => function($q) { $q->withoutGlobalScope('structure'); },
|
||||
'structure'
|
||||
])
|
||||
: null,
|
||||
],
|
||||
'tenant' => [
|
||||
'current' => config('tenant.structure_id')
|
||||
? \App\Models\Structure::find(config('tenant.structure_id'))
|
||||
: null,
|
||||
'is_impersonating' => $request->session()->has('target_structure_id'),
|
||||
],
|
||||
'flash' => [
|
||||
'success' => $request->session()->get('success'),
|
||||
'error' => $request->session()->get('error'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
50
app/Http/Middleware/TenantContext.php
Normal file
50
app/Http/Middleware/TenantContext.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class TenantContext
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (Auth::check()) {
|
||||
$user = Auth::user();
|
||||
|
||||
// 1. Initialiser le contexte Spatie sur la propre structure de l'utilisateur
|
||||
// CELA NE DOIT JAMAIS CHANGER !
|
||||
// C'est ce qui permet à Laravel de toujours charger les rôles originels (Admin, SuperAdmin) du compte,
|
||||
// même s'il est en train d'ausculter les données d'un autre client.
|
||||
setPermissionsTeamId($user->structure_id);
|
||||
|
||||
// S'il s'agit d'un SuperAdmin, il peut avoir choisi une structure spécifique en session
|
||||
if ($user->hasRole('SuperAdmin')) {
|
||||
if ($request->session()->has('target_structure_id')) {
|
||||
$targetId = $request->session()->get('target_structure_id');
|
||||
// On modifie UNIQUEMENT la configuration globale pour filtrer les données (BelongsToStructure scope)
|
||||
config(['tenant.structure_id' => $targetId]);
|
||||
} else {
|
||||
// Par défaut, s'il n'a pas ciblé de structure, vue globale (tous les locataires)
|
||||
config(['tenant.structure_id' => null]);
|
||||
// Et par défaut on remet sa session sur son origine si elle était vide
|
||||
$request->session()->put('target_structure_id', null);
|
||||
}
|
||||
} else {
|
||||
// Utilisateur SaaS standard : on fixe la config globale à SA structure
|
||||
config(['tenant.structure_id' => $user->structure_id]);
|
||||
// On met de force cette valeur dans sa session
|
||||
$request->session()->put('target_structure_id', $user->structure_id);
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,11 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Traits\BelongsToStructure;
|
||||
|
||||
class Agent extends Model
|
||||
{
|
||||
use BelongsToStructure;
|
||||
protected $fillable = [
|
||||
'first_name',
|
||||
'last_name',
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Traits\BelongsToStructure;
|
||||
|
||||
class Attachment extends Model
|
||||
{
|
||||
use BelongsToStructure;
|
||||
protected $fillable = [
|
||||
'service_task_id',
|
||||
'filename',
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Traits\BelongsToStructure;
|
||||
|
||||
class Comment extends Model
|
||||
{
|
||||
use BelongsToStructure;
|
||||
protected $fillable = ['user_id', 'content', 'commentable_id', 'commentable_type'];
|
||||
|
||||
public function user()
|
||||
|
||||
@@ -5,10 +5,11 @@ namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
use App\Traits\BelongsToStructure;
|
||||
|
||||
class IntegrationRequest extends Model
|
||||
{
|
||||
use LogsActivity;
|
||||
use LogsActivity, BelongsToStructure;
|
||||
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Traits\BelongsToStructure;
|
||||
|
||||
class IntegrationTemplate extends Model
|
||||
{
|
||||
use BelongsToStructure;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
|
||||
13
app/Models/Role.php
Normal file
13
app/Models/Role.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Spatie\Permission\Models\Role as SpatieRole;
|
||||
use App\Traits\BelongsToStructure;
|
||||
|
||||
class Role extends SpatieRole
|
||||
{
|
||||
use BelongsToStructure;
|
||||
|
||||
protected $fillable = ['name', 'guard_name', 'structure_id'];
|
||||
}
|
||||
@@ -3,9 +3,12 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Traits\BelongsToStructure;
|
||||
|
||||
class Service extends Model
|
||||
{
|
||||
use BelongsToStructure;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'code',
|
||||
|
||||
@@ -5,10 +5,11 @@ namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
use App\Traits\BelongsToStructure;
|
||||
|
||||
class ServiceTask extends Model
|
||||
{
|
||||
use LogsActivity;
|
||||
use LogsActivity, BelongsToStructure;
|
||||
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
|
||||
20
app/Models/Structure.php
Normal file
20
app/Models/Structure.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Structure extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'slug',
|
||||
'domain',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
public function users()
|
||||
{
|
||||
return $this->hasMany(User::class);
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,11 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Traits\BelongsToStructure;
|
||||
|
||||
class TaskItem extends Model
|
||||
{
|
||||
use BelongsToStructure;
|
||||
protected $fillable = [
|
||||
'service_task_id',
|
||||
'label',
|
||||
|
||||
@@ -8,11 +8,14 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
use App\Traits\BelongsToStructure;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable, HasRoles;
|
||||
use HasFactory, Notifiable, BelongsToStructure;
|
||||
use HasRoles {
|
||||
hasRole as traitHasRole;
|
||||
}
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
@@ -23,6 +26,7 @@ class User extends Authenticatable
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'structure_id',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -47,4 +51,29 @@ class User extends Authenticatable
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Override de Spatie HasRoles pour qu'un SuperAdmin valide toutes les vérifications de rôle
|
||||
* Cela permet notamment de parcourir les locataires (Tenant) sans être bloqué par les "hasRole('Admin')"
|
||||
*/
|
||||
public function hasRole($roles, string $guard = null): bool
|
||||
{
|
||||
// Si on ne demande pas explicitement le rôle SuperAdmin, on vérifie si l'utilisateur l'a globalement.
|
||||
// On passe par DB::table pour éviter que le GlobalScope 'structure' ne filtre nos propres rôles
|
||||
// lorsqu'on est en train de simuler une autre structure.
|
||||
if ($roles !== 'SuperAdmin') {
|
||||
$isSuperAdmin = \Illuminate\Support\Facades\DB::table('model_has_roles')
|
||||
->join('roles', 'roles.id', '=', 'model_has_roles.role_id')
|
||||
->where('model_has_roles.model_id', $this->id)
|
||||
->where('model_has_roles.model_type', self::class)
|
||||
->where('roles.name', 'SuperAdmin')
|
||||
->exists();
|
||||
|
||||
if ($isSuperAdmin) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->traitHasRole($roles, $guard);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ class IntegrationRequestPolicy
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->hasAnyRole(['Admin', 'RH', 'DSI', 'Batiment', 'Parc Auto']);
|
||||
return $user->hasRole('Admin') || $user->can('view dashboard');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,7 +21,7 @@ class IntegrationRequestPolicy
|
||||
*/
|
||||
public function view(User $user, IntegrationRequest $integrationRequest): bool
|
||||
{
|
||||
if ($user->hasRole('Admin') || $user->hasRole('RH')) {
|
||||
if ($user->hasRole('Admin') || $user->can('validate rh')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class IntegrationRequestPolicy
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user->hasAnyRole(['Admin', 'RH', 'Prescripteur', 'DSI', 'Batiment', 'Parc Auto']);
|
||||
return $user->hasRole('Admin') || $user->can('create integration');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,7 +44,7 @@ class IntegrationRequestPolicy
|
||||
*/
|
||||
public function update(User $user, IntegrationRequest $integrationRequest): bool
|
||||
{
|
||||
return $user->hasAnyRole(['Admin', 'RH']);
|
||||
return $user->hasRole('Admin') || $user->can('validate rh');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -48,7 +48,7 @@ class IntegrationService
|
||||
]);
|
||||
|
||||
// Notify RH users about the new request
|
||||
$rhUsers = \App\Models\User::role('RH')->get();
|
||||
$rhUsers = \App\Models\User::permission(['validate rh', 'create integration'])->get();
|
||||
\Illuminate\Support\Facades\Notification::send($rhUsers, new \App\Notifications\NewIntegrationRequestNotification($request));
|
||||
|
||||
return $request;
|
||||
@@ -110,7 +110,7 @@ class IntegrationService
|
||||
$request->agent->update(['integration_status' => IntegrationStatus::Completed]);
|
||||
|
||||
// Notify RH (Standard notification)
|
||||
$rhUsers = \App\Models\User::role('RH')->get();
|
||||
$rhUsers = \App\Models\User::permission('validate rh')->get();
|
||||
\Illuminate\Support\Facades\Notification::send($rhUsers, new \App\Notifications\ProcessCompletedNotification($request));
|
||||
|
||||
// Generate PDF for Prescriber and DSI
|
||||
|
||||
@@ -56,7 +56,7 @@ class ServiceTaskManager
|
||||
]);
|
||||
|
||||
// Notify RH that a service has completed their task
|
||||
$rhUsers = \App\Models\User::role('RH')->get();
|
||||
$rhUsers = \App\Models\User::permission('validate rh')->get();
|
||||
\Illuminate\Support\Facades\Notification::send($rhUsers, new \App\Notifications\ServiceTaskValidatedNotification($task));
|
||||
|
||||
// Trigger check on the parent integration request
|
||||
|
||||
45
app/Traits/BelongsToStructure.php
Normal file
45
app/Traits/BelongsToStructure.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use App\Models\Structure;
|
||||
use App\Models\Scopes\StructureScope;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
trait BelongsToStructure
|
||||
{
|
||||
/**
|
||||
* Boot the trait to apply the GlobalScope.
|
||||
*/
|
||||
protected static function bootBelongsToStructure()
|
||||
{
|
||||
static::addGlobalScope('structure', function (Builder $builder) {
|
||||
// On utilise la configuration injectée par le middleware plutôt que d'appeler auth()->user()
|
||||
// Cela évite la boucle infinie d'authentification lorsque le scope s'applique à la table `users`.
|
||||
$structureId = config('tenant.structure_id');
|
||||
if ($structureId !== null) {
|
||||
// Dans le cas spécifique de SQLite en mode de test, il faut parfois préciser la table.
|
||||
// Par sécurité on gère la jointure si besoin, mas ici on reste simple.
|
||||
$builder->where($builder->getModel()->getTable() . '.structure_id', $structureId);
|
||||
}
|
||||
});
|
||||
|
||||
static::creating(function ($model) {
|
||||
// Assigner automatiquement la structure_id sur les nouveaux enregistrements
|
||||
if (!$model->structure_id) {
|
||||
$structureId = config('tenant.structure_id');
|
||||
if ($structureId) {
|
||||
$model->structure_id = $structureId;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the relationship to Structure.
|
||||
*/
|
||||
public function structure()
|
||||
{
|
||||
return $this->belongsTo(Structure::class);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware) {
|
||||
$middleware->web(append: [
|
||||
\App\Http\Middleware\TenantContext::class,
|
||||
\App\Http\Middleware\HandleInertiaRequests::class,
|
||||
\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
|
||||
]);
|
||||
|
||||
@@ -24,7 +24,7 @@ return [
|
||||
* `Spatie\Permission\Contracts\Role` contract.
|
||||
*/
|
||||
|
||||
'role' => Spatie\Permission\Models\Role::class,
|
||||
'role' => \App\Models\Role::class,
|
||||
|
||||
],
|
||||
|
||||
@@ -93,7 +93,7 @@ return [
|
||||
* foreign key is other than `team_id`.
|
||||
*/
|
||||
|
||||
'team_foreign_key' => 'team_id',
|
||||
'team_foreign_key' => 'structure_id',
|
||||
],
|
||||
|
||||
/*
|
||||
@@ -131,7 +131,7 @@ return [
|
||||
* (view the latest version of this package's migration file)
|
||||
*/
|
||||
|
||||
'teams' => false,
|
||||
'teams' => true,
|
||||
|
||||
/*
|
||||
* The class to use to resolve the permissions team id
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?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('structures', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('slug')->unique();
|
||||
$table->string('domain')->nullable()->unique();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('structures');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
<?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
|
||||
{
|
||||
$tables = [
|
||||
'users',
|
||||
'integration_templates',
|
||||
'services',
|
||||
'integration_requests',
|
||||
'comments',
|
||||
'service_tasks'
|
||||
];
|
||||
|
||||
foreach ($tables as $tableName) {
|
||||
if (Schema::hasTable($tableName)) {
|
||||
Schema::table($tableName, function (Blueprint $table) {
|
||||
// Si la colonne n'existe pas déjà (sécurité)
|
||||
if (!Schema::hasColumn($table->getTable(), 'structure_id')) {
|
||||
$table->foreignId('structure_id')->nullable()->constrained()->onDelete('cascade');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
$tables = [
|
||||
'users',
|
||||
'integration_templates',
|
||||
'services',
|
||||
'integration_requests',
|
||||
'comments',
|
||||
'service_tasks'
|
||||
];
|
||||
|
||||
foreach ($tables as $tableName) {
|
||||
if (Schema::hasTable($tableName)) {
|
||||
Schema::table($tableName, function (Blueprint $table) {
|
||||
if (Schema::hasColumn($table->getTable(), 'structure_id')) {
|
||||
$table->dropForeign([$table->getTable() . '_structure_id_foreign']);
|
||||
$table->dropColumn('structure_id');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -13,18 +13,49 @@ class DatabaseSeeder extends Seeder
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// 1. Initialiser la structure par défaut (CABM)
|
||||
$cabm = \App\Models\Structure::firstOrCreate([
|
||||
'slug' => 'cabm'
|
||||
], [
|
||||
'name' => 'CABM',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
// 2. Définir le contexte global pour les seeders suivants
|
||||
config(['tenant.structure_id' => $cabm->id]);
|
||||
app()[\Spatie\Permission\PermissionRegistrar::class]->setPermissionsTeamId($cabm->id);
|
||||
|
||||
// 3. Appeler les seeders de base (ils utiliseront le contexte CABM)
|
||||
$this->call([
|
||||
ServiceSeeder::class,
|
||||
RolesAndPermissionsSeeder::class,
|
||||
IntegrationTemplateSeeder::class,
|
||||
]);
|
||||
|
||||
$admin = User::factory()->create([
|
||||
// 4. Créer le compte Admin lié à cette structure
|
||||
$admin = User::withoutGlobalScope('structure')->updateOrCreate(
|
||||
['email' => 'admin@admin.com'],
|
||||
[
|
||||
'name' => 'Admin User',
|
||||
'email' => 'admin@admin.com',
|
||||
'password' => bcrypt('password'),
|
||||
]);
|
||||
'structure_id' => $cabm->id,
|
||||
]
|
||||
);
|
||||
|
||||
// 5. Lui assigner le rôle Admin (dans le contexte de CABM)
|
||||
if (!$admin->hasRole('Admin')) {
|
||||
$admin->assignRole('Admin');
|
||||
}
|
||||
|
||||
// 6. Créer le rôle SuperAdmin lié à CABM pour le seeder
|
||||
$superAdminRole = \App\Models\Role::updateOrCreate(
|
||||
['name' => 'SuperAdmin'],
|
||||
['guard_name' => 'web', 'structure_id' => $cabm->id]
|
||||
);
|
||||
|
||||
// L'admin de base sera aussi SuperAdmin
|
||||
if (!$admin->hasRole('SuperAdmin')) {
|
||||
$admin->assignRole('SuperAdmin');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use App\Models\Role;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
class RolesAndPermissionsSeeder extends Seeder
|
||||
@@ -13,6 +13,11 @@ class RolesAndPermissionsSeeder extends Seeder
|
||||
// Reset cached roles and permissions
|
||||
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
|
||||
|
||||
// Rétablir le context multi-tenant s'il est défini en config
|
||||
if (config('tenant.structure_id')) {
|
||||
app()[\Spatie\Permission\PermissionRegistrar::class]->setPermissionsTeamId(config('tenant.structure_id'));
|
||||
}
|
||||
|
||||
// Create permissions
|
||||
$permissions = [
|
||||
'create integration',
|
||||
|
||||
62
database/seeders/SaaSTenantSeeder.php
Normal file
62
database/seeders/SaaSTenantSeeder.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Structure;
|
||||
use App\Models\User;
|
||||
use App\Models\Role;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class SaaSTenantSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// 1. Créer la structure principale (votre agence CABM par défaut)
|
||||
$cabm = Structure::firstOrCreate([
|
||||
'slug' => 'cabm'
|
||||
], [
|
||||
'name' => 'CABM',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
// 2. Mettre à jour tous les agents orphelins vers cette structure
|
||||
DB::table('agents')->whereNull('structure_id')->update(['structure_id' => $cabm->id]);
|
||||
|
||||
// 3. Mettre à jour tous les services, templates, users, etc. vers cette structure
|
||||
$tables = ['services', 'integration_templates', 'integration_requests', 'users', 'service_tasks', 'comments'];
|
||||
foreach ($tables as $table) {
|
||||
if (Schema::hasColumn($table, 'structure_id')) {
|
||||
DB::table($table)->whereNull('structure_id')->update(['structure_id' => $cabm->id]);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. S'assurer que le rôle SuperAdmin existe globalement
|
||||
$superAdminRole = Role::withoutGlobalScope('structure')->updateOrCreate(
|
||||
['name' => 'SuperAdmin'],
|
||||
['guard_name' => 'web', 'structure_id' => null]
|
||||
);
|
||||
|
||||
// 5. Créer l'utilisateur Admin par défaut s'il n'existe pas
|
||||
$admin = User::withoutGlobalScope('structure')->firstOrCreate(
|
||||
['email' => 'admin@admin.com'],
|
||||
[
|
||||
'name' => 'Super Admin',
|
||||
'password' => Hash::make('password'),
|
||||
'structure_id' => $cabm->id
|
||||
]
|
||||
);
|
||||
|
||||
// Lui assigner le rôle SuperAdmin
|
||||
if (!$admin->hasRole('SuperAdmin')) {
|
||||
$admin->assignRole($superAdminRole);
|
||||
}
|
||||
|
||||
$this->command->info('Migration vers le mode SaaS terminée. Structure par défaut : CABM.');
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ const props = defineProps({
|
||||
|
||||
const user = computed(() => usePage().props.auth.user);
|
||||
const canManage = computed(() => {
|
||||
return user.value.roles.some(r => r.name === props.task.service.name || r.name === 'Admin');
|
||||
return user.value.roles.some(r => r.name === props.task.service.name || r.name === 'Admin' || r.name === 'SuperAdmin');
|
||||
});
|
||||
|
||||
const progress = computed(() => {
|
||||
|
||||
@@ -13,6 +13,21 @@ const showingNavigationDropdown = ref(false);
|
||||
<template>
|
||||
<div>
|
||||
<div class="min-h-screen bg-gray-100 dark:bg-gray-900">
|
||||
<!-- Impersonation Banner -->
|
||||
<div v-if="$page.props.tenant.is_impersonating" class="bg-amber-500 text-white py-2 px-4 shadow-md">
|
||||
<div class="max-w-7xl mx-auto flex justify-between items-center text-sm font-bold">
|
||||
<div class="flex items-center space-x-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<span>MODE SIMULATION ACTIF : Vous modifiez actuellement le locataire "{{ $page.props.tenant?.current?.name || 'VUE GLOBALE' }}"</span>
|
||||
</div>
|
||||
<Link :href="route('superadmin.reset')" method="post" as="button" class="bg-white text-amber-600 px-3 py-1 rounded-md hover:bg-amber-50 transition-colors">
|
||||
Arrêter la simulation
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav
|
||||
class="border-b border-gray-100 bg-white dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
@@ -20,13 +35,16 @@ const showingNavigationDropdown = ref(false);
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex h-16 justify-between">
|
||||
<div class="flex">
|
||||
<!-- Logo -->
|
||||
<div class="flex shrink-0 items-center">
|
||||
<!-- Logo and Structure Name -->
|
||||
<div class="flex shrink-0 items-center space-x-3">
|
||||
<Link :href="route('dashboard')">
|
||||
<ApplicationLogo
|
||||
class="block h-9 w-auto"
|
||||
/>
|
||||
</Link>
|
||||
<span class="text-lg font-bold text-gray-800 dark:text-gray-200">
|
||||
{{ $page.props.tenant?.current?.name || $page.props.auth?.user?.structure?.name || '' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
@@ -40,40 +58,48 @@ const showingNavigationDropdown = ref(false);
|
||||
Tableau de Bord
|
||||
</NavLink>
|
||||
<NavLink
|
||||
v-if="$page.props.auth.user.roles.some(r => r.name === 'Admin')"
|
||||
v-if="$page.props.auth.user.roles.some(r => r.name === 'Admin' || r.name === 'SuperAdmin')"
|
||||
:href="route('users.index')"
|
||||
:active="route().current('users.*')"
|
||||
>
|
||||
Utilisateurs
|
||||
</NavLink>
|
||||
<NavLink
|
||||
v-if="$page.props.auth.user.roles.some(r => r.name === 'Admin')"
|
||||
v-if="$page.props.auth.user.roles.some(r => r.name === 'Admin' || r.name === 'SuperAdmin')"
|
||||
:href="route('roles.index')"
|
||||
:active="route().current('roles.*')"
|
||||
>
|
||||
Rôles
|
||||
</NavLink>
|
||||
<NavLink
|
||||
v-if="$page.props.auth.user.roles.some(r => r.name === 'Admin')"
|
||||
v-if="$page.props.auth.user.roles.some(r => r.name === 'Admin' || r.name === 'SuperAdmin')"
|
||||
:href="route('permissions.index')"
|
||||
:active="route().current('permissions.*')"
|
||||
>
|
||||
Permissions
|
||||
</NavLink>
|
||||
<NavLink
|
||||
v-if="$page.props.auth.user.roles.some(r => r.name === 'Admin')"
|
||||
v-if="$page.props.auth.user.roles.some(r => r.name === 'Admin' || r.name === 'SuperAdmin')"
|
||||
:href="route('services.index')"
|
||||
:active="route().current('services.*')"
|
||||
>
|
||||
Services
|
||||
</NavLink>
|
||||
<NavLink
|
||||
v-if="$page.props.auth.user.roles.some(r => r.name === 'Admin')"
|
||||
v-if="$page.props.auth.user.roles.some(r => r.name === 'Admin' || r.name === 'SuperAdmin')"
|
||||
:href="route('templates.index')"
|
||||
:active="route().current('templates.*')"
|
||||
>
|
||||
Modèles
|
||||
</NavLink>
|
||||
<NavLink
|
||||
v-if="$page.props.auth.user.roles.some(r => r.name === 'SuperAdmin')"
|
||||
:href="route('superadmin.index')"
|
||||
:active="route().current('superadmin.*')"
|
||||
class="text-indigo-600 dark:text-indigo-400 font-bold"
|
||||
>
|
||||
🛠️ SaaS Admin
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -182,40 +208,48 @@ const showingNavigationDropdown = ref(false);
|
||||
Tableau de Bord
|
||||
</ResponsiveNavLink>
|
||||
<ResponsiveNavLink
|
||||
v-if="$page.props.auth.user.roles.some(r => r.name === 'Admin')"
|
||||
v-if="$page.props.auth.user.roles.some(r => r.name === 'Admin' || r.name === 'SuperAdmin')"
|
||||
:href="route('users.index')"
|
||||
:active="route().current('users.*')"
|
||||
>
|
||||
Utilisateurs
|
||||
</ResponsiveNavLink>
|
||||
<ResponsiveNavLink
|
||||
v-if="$page.props.auth.user.roles.some(r => r.name === 'Admin')"
|
||||
v-if="$page.props.auth.user.roles.some(r => r.name === 'Admin' || r.name === 'SuperAdmin')"
|
||||
:href="route('roles.index')"
|
||||
:active="route().current('roles.*')"
|
||||
>
|
||||
Rôles
|
||||
</ResponsiveNavLink>
|
||||
<ResponsiveNavLink
|
||||
v-if="$page.props.auth.user.roles.some(r => r.name === 'Admin')"
|
||||
v-if="$page.props.auth.user.roles.some(r => r.name === 'Admin' || r.name === 'SuperAdmin')"
|
||||
:href="route('permissions.index')"
|
||||
:active="route().current('permissions.*')"
|
||||
>
|
||||
Permissions
|
||||
</ResponsiveNavLink>
|
||||
<ResponsiveNavLink
|
||||
v-if="$page.props.auth.user.roles.some(r => r.name === 'Admin')"
|
||||
v-if="$page.props.auth.user.roles.some(r => r.name === 'Admin' || r.name === 'SuperAdmin')"
|
||||
:href="route('services.index')"
|
||||
:active="route().current('services.*')"
|
||||
>
|
||||
Services
|
||||
</ResponsiveNavLink>
|
||||
<ResponsiveNavLink
|
||||
v-if="$page.props.auth.user.roles.some(r => r.name === 'Admin')"
|
||||
v-if="$page.props.auth.user.roles.some(r => r.name === 'Admin' || r.name === 'SuperAdmin')"
|
||||
:href="route('templates.index')"
|
||||
:active="route().current('templates.*')"
|
||||
>
|
||||
Modèles
|
||||
</ResponsiveNavLink>
|
||||
<ResponsiveNavLink
|
||||
v-if="$page.props.auth.user.roles.some(r => r.name === 'SuperAdmin')"
|
||||
:href="route('superadmin.index')"
|
||||
:active="route().current('superadmin.*')"
|
||||
class="text-indigo-600 dark:text-indigo-400 font-bold"
|
||||
>
|
||||
🛠️ SaaS Admin
|
||||
</ResponsiveNavLink>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Settings Options -->
|
||||
@@ -261,6 +295,18 @@ const showingNavigationDropdown = ref(false);
|
||||
|
||||
<!-- Page Content -->
|
||||
<main>
|
||||
<!-- Flash Messages -->
|
||||
<div v-if="$page.props.flash.success" class="max-w-7xl mx-auto mt-4 px-4 sm:px-6 lg:px-8">
|
||||
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative" role="alert">
|
||||
<span class="block sm:inline">{{ $page.props.flash.success }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$page.props.flash.error" class="max-w-7xl mx-auto mt-4 px-4 sm:px-6 lg:px-8">
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
|
||||
<span class="block sm:inline">{{ $page.props.flash.error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import TextInput from '@/Components/TextInput.vue';
|
||||
import { Head, Link, useForm } from '@inertiajs/vue3';
|
||||
|
||||
const form = useForm({
|
||||
structure_name: '',
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
@@ -26,7 +27,23 @@ const submit = () => {
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div>
|
||||
<InputLabel for="name" value="Nom" />
|
||||
<InputLabel for="structure_name" value="Nom de votre Organisation (SaaS)" />
|
||||
|
||||
<TextInput
|
||||
id="structure_name"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.structure_name"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="organization"
|
||||
/>
|
||||
|
||||
<InputError class="mt-2" :message="form.errors.structure_name" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<InputLabel for="name" value="Votre Nom complet" />
|
||||
|
||||
<TextInput
|
||||
id="name"
|
||||
@@ -34,7 +51,6 @@ const submit = () => {
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.name"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="name"
|
||||
/>
|
||||
|
||||
|
||||
86
resources/js/Pages/SuperAdmin/Create.vue
Normal file
86
resources/js/Pages/SuperAdmin/Create.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
|
||||
const form = useForm({
|
||||
name: '',
|
||||
slug: '',
|
||||
domain: '',
|
||||
is_active: true,
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('superadmin.store'));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Créer une Structure" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Créer une nouvelle Structure (Tenant)
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6">
|
||||
<form @submit.prevent="submit" class="max-w-xl space-y-6">
|
||||
<div>
|
||||
<InputLabel for="name" value="Nom de l'organisation" />
|
||||
<TextInput
|
||||
id="name"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.name"
|
||||
required
|
||||
autofocus
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.name" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="slug" value="Slug (Identifiant URL)" />
|
||||
<TextInput
|
||||
id="slug"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.slug"
|
||||
required
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.slug" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="domain" value="Domaine personnalisé (Optionnel)" />
|
||||
<TextInput
|
||||
id="domain"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.domain"
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.domain" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="checkbox" id="is_active" v-model="form.is_active" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500">
|
||||
<InputLabel for="is_active" value="Structure active" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<PrimaryButton :disabled="form.processing">Créer</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
89
resources/js/Pages/SuperAdmin/Edit.vue
Normal file
89
resources/js/Pages/SuperAdmin/Edit.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
|
||||
const props = defineProps({
|
||||
structure: Object
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
name: props.structure.name,
|
||||
slug: props.structure.slug,
|
||||
domain: props.structure.domain,
|
||||
is_active: props.structure.is_active === 1 || props.structure.is_active === true,
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.put(route('superadmin.update', props.structure.id));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Modifier la Structure" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Modifier : {{ structure.name }}
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6">
|
||||
<form @submit.prevent="submit" class="max-w-xl space-y-6">
|
||||
<div>
|
||||
<InputLabel for="name" value="Nom de l'organisation" />
|
||||
<TextInput
|
||||
id="name"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.name"
|
||||
required
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.name" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="slug" value="Slug (Identifiant URL)" />
|
||||
<TextInput
|
||||
id="slug"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.slug"
|
||||
required
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.slug" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="domain" value="Domaine personnalisé (Optionnel)" />
|
||||
<TextInput
|
||||
id="domain"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.domain"
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.domain" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="checkbox" id="is_active" v-model="form.is_active" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500">
|
||||
<InputLabel for="is_active" value="Structure active" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<PrimaryButton :disabled="form.processing">Mettre à jour</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
136
resources/js/Pages/SuperAdmin/Index.vue
Normal file
136
resources/js/Pages/SuperAdmin/Index.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, Link, router } from '@inertiajs/vue3';
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
structures: Array,
|
||||
current_structure_id: Number,
|
||||
db_latency: Number,
|
||||
db_type: String
|
||||
});
|
||||
|
||||
const switchTo = (id) => {
|
||||
router.post(route('superadmin.switch', id));
|
||||
};
|
||||
|
||||
const resetSession = () => {
|
||||
router.post(route('superadmin.reset'));
|
||||
};
|
||||
|
||||
const deleteStructure = (id) => {
|
||||
if (confirm('Êtes-vous sûr de vouloir supprimer cette structure (Tenant) et toutes ses données associées ?')) {
|
||||
router.delete(route('superadmin.destroy', id));
|
||||
}
|
||||
};
|
||||
|
||||
let refreshInterval = null;
|
||||
|
||||
onMounted(() => {
|
||||
refreshInterval = setInterval(() => {
|
||||
router.reload({
|
||||
only: ['db_latency'],
|
||||
preserveScroll: true,
|
||||
preserveState: true
|
||||
});
|
||||
}, 30000); // 30 seconds
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Super Administration SaaS" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center space-x-4">
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Super Administration SaaS
|
||||
</h2>
|
||||
<span v-if="db_type" class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400 border border-blue-200 dark:border-blue-800">
|
||||
Type : {{ db_type.toUpperCase() }}
|
||||
</span>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400 border border-green-200 dark:border-green-800">
|
||||
<svg class="mr-1.5 h-2 w-2 text-green-400" fill="currentColor" viewBox="0 0 8 8">
|
||||
<circle cx="4" cy="4" r="3" />
|
||||
</svg>
|
||||
Latence BDD : {{ db_latency }}ms
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex space-x-4">
|
||||
<Link :href="route('superadmin.create')" class="px-4 py-2 bg-indigo-600 text-white rounded-md text-sm hover:bg-indigo-700">
|
||||
+ Créer une Structure
|
||||
</Link>
|
||||
<button v-if="current_structure_id" @click="resetSession" class="px-4 py-2 bg-red-600 text-white rounded-md text-sm hover:bg-red-700">
|
||||
Arrêter la simulation (Vue Globale)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 text-gray-900 dark:text-gray-100">
|
||||
<h3 class="text-lg font-medium mb-4">Gestion des Structures (Tenants)</h3>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500">Nom</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500">Slug (URL)</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500">Utilisateurs</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500">Statut</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="structure in structures" :key="structure.id" class="border-b dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-6 py-4 font-medium">{{ structure.name }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">{{ structure.slug }}</td>
|
||||
<td class="px-6 py-4">{{ structure.users_count }}</td>
|
||||
<td class="px-6 py-4">
|
||||
<span v-if="structure.is_active" class="px-2 py-1 text-xs font-semibold text-green-800 bg-green-100 rounded-full">Actif</span>
|
||||
<span v-else class="px-2 py-1 text-xs font-semibold text-red-800 bg-red-100 rounded-full">Inactif</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center space-x-3">
|
||||
<button
|
||||
v-if="current_structure_id !== structure.id"
|
||||
@click="switchTo(structure.id)"
|
||||
title="Simuler (Impersonate)"
|
||||
class="text-blue-600 hover:text-blue-900 text-sm font-medium">
|
||||
Gérer ce SaaS
|
||||
</button>
|
||||
<span v-else class="text-green-600 text-sm font-bold w-[100px]">✓ En cours</span>
|
||||
|
||||
<span class="text-gray-300">|</span>
|
||||
|
||||
<Link :href="route('superadmin.edit', structure.id)" class="text-indigo-600 hover:text-indigo-900">
|
||||
Modifier
|
||||
</Link>
|
||||
|
||||
<button @click="deleteStructure(structure.id)" class="text-red-600 hover:text-red-900">
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
@@ -43,6 +43,16 @@ Route::middleware('auth')->group(function () {
|
||||
Route::resource('roles', \App\Http\Controllers\RoleController::class);
|
||||
Route::resource('permissions', \App\Http\Controllers\PermissionController::class);
|
||||
Route::resource('services', \App\Http\Controllers\ServiceController::class);
|
||||
|
||||
// Routes Super Admin
|
||||
Route::get('superadmin', [\App\Http\Controllers\SuperAdminController::class, 'index'])->name('superadmin.index');
|
||||
Route::get('superadmin/create', [\App\Http\Controllers\SuperAdminController::class, 'create'])->name('superadmin.create');
|
||||
Route::post('superadmin/store', [\App\Http\Controllers\SuperAdminController::class, 'store'])->name('superadmin.store');
|
||||
Route::get('superadmin/{structure}/edit', [\App\Http\Controllers\SuperAdminController::class, 'edit'])->name('superadmin.edit');
|
||||
Route::put('superadmin/{structure}', [\App\Http\Controllers\SuperAdminController::class, 'update'])->name('superadmin.update');
|
||||
Route::delete('superadmin/{structure}', [\App\Http\Controllers\SuperAdminController::class, 'destroy'])->name('superadmin.destroy');
|
||||
Route::post('superadmin/switch/{structure}', [\App\Http\Controllers\SuperAdminController::class, 'switchStructure'])->name('superadmin.switch');
|
||||
Route::post('superadmin/reset', [\App\Http\Controllers\SuperAdminController::class, 'resetStructure'])->name('superadmin.reset');
|
||||
});
|
||||
|
||||
require __DIR__.'/auth.php';
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
||||
$kernel->bootstrap();
|
||||
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
|
||||
echo "Configuration Mail:\n";
|
||||
echo "Mailer: " . config('mail.default') . "\n";
|
||||
echo "Host: " . config('mail.mailers.smtp.host') . "\n";
|
||||
echo "Port: " . config('mail.mailers.smtp.port') . "\n";
|
||||
|
||||
try {
|
||||
Mail::raw('Ceci est un test pour vérifier la connexion à Mailpit.', function ($message) {
|
||||
$message->to('test@cabm.local')
|
||||
->subject('Test Mailpit - ' . now());
|
||||
});
|
||||
echo "✅ Email envoyé avec succès ! Vérifiez votre interface Mailpit (http://localhost:8025).";
|
||||
} catch (\Exception $e) {
|
||||
echo "❌ Erreur lors de l'envoi : " . $e->getMessage();
|
||||
}
|
||||
Reference in New Issue
Block a user