feat: multi-tenant SaaS implementation - admin interface, tenant isolation, and UI updates

This commit is contained in:
jeremy bayse
2026-03-28 18:38:22 +01:00
parent 7d94be7a8c
commit f53d5770df
20 changed files with 757 additions and 34 deletions

View File

@@ -15,10 +15,14 @@ class CandidateController extends Controller
{
public function index()
{
$candidates = Candidate::with(['user', 'documents', 'attempts'])->latest()->get();
$candidates = Candidate::with(['user', 'documents', 'attempts', 'tenant', 'jobPosition'])->latest()->get();
$jobPositions = \App\Models\JobPosition::orderBy('title')->get();
$tenants = \App\Models\Tenant::orderBy('name')->get();
return \Inertia\Inertia::render('Admin/Candidates/Index', [
'candidates' => $candidates
'candidates' => $candidates,
'jobPositions' => $jobPositions,
'tenants' => $tenants
]);
}
@@ -42,8 +46,10 @@ class CandidateController extends Controller
'email' => 'required|string|email|max:255|unique:users',
'phone' => 'nullable|string|max:20',
'linkedin_url' => 'nullable|url|max:255',
'cv' => 'nullable|file|mimes:pdf|max:5120',
'cover_letter' => 'nullable|file|mimes:pdf|max:5120',
'cv' => 'nullable|mimes:pdf|max:5120',
'cover_letter' => 'nullable|mimes:pdf|max:5120',
'tenant_id' => 'nullable|exists:tenants,id',
'job_position_id' => 'nullable|exists:job_positions,id',
]);
$password = Str::random(10);
@@ -51,15 +57,17 @@ class CandidateController extends Controller
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($password),
'password' => Hash::make(Str::random(12)),
'role' => 'candidate',
'tenant_id' => auth()->user()->isSuperAdmin() ? $request->tenant_id : auth()->user()->tenant_id,
]);
$candidate = Candidate::create([
'user_id' => $user->id,
$candidate = $user->candidate()->create([
'phone' => $request->phone,
'linkedin_url' => $request->linkedin_url,
'status' => 'en_attente',
'tenant_id' => auth()->user()->isSuperAdmin() ? $request->tenant_id : auth()->user()->tenant_id,
'job_position_id' => $request->job_position_id,
]);
$this->storeDocument($candidate, $request->file('cv'), 'cv');

View File

@@ -13,7 +13,8 @@ class JobPositionController extends Controller
$this->authorizeAdmin();
return Inertia::render('Admin/JobPositions/Index', [
'jobPositions' => JobPosition::all()
'jobPositions' => JobPosition::with('tenant')->get(),
'tenants' => \App\Models\Tenant::orderBy('name')->get()
]);
}
@@ -26,6 +27,7 @@ class JobPositionController extends Controller
'description' => 'required|string',
'requirements' => 'nullable|array',
'ai_prompt' => 'nullable|string',
'tenant_id' => 'nullable|exists:tenants,id',
]);
JobPosition::create([
@@ -33,6 +35,7 @@ class JobPositionController extends Controller
'description' => $request->description,
'requirements' => $request->requirements,
'ai_prompt' => $request->ai_prompt,
'tenant_id' => auth()->user()->isSuperAdmin() ? $request->tenant_id : auth()->user()->tenant_id,
]);
return back()->with('success', 'Fiche de poste créée avec succès.');
@@ -47,6 +50,7 @@ class JobPositionController extends Controller
'description' => 'required|string',
'requirements' => 'nullable|array',
'ai_prompt' => 'nullable|string',
'tenant_id' => 'nullable|exists:tenants,id',
]);
$jobPosition->update([
@@ -54,6 +58,7 @@ class JobPositionController extends Controller
'description' => $request->description,
'requirements' => $request->requirements,
'ai_prompt' => $request->ai_prompt,
'tenant_id' => auth()->user()->isSuperAdmin() ? $request->tenant_id : auth()->user()->tenant_id,
]);
return back()->with('success', 'Fiche de poste mise à jour.');

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers;
use App\Models\Tenant;
use Illuminate\Http\Request;
use Inertia\Inertia;
class TenantController extends Controller
{
public function index()
{
if (!auth()->user()->isSuperAdmin()) {
abort(403, 'Unauthorized action.');
}
$tenants = Tenant::orderBy('name')->get();
return Inertia::render('Admin/Tenants/Index', [
'tenants' => $tenants
]);
}
public function store(Request $request)
{
if (!auth()->user()->isSuperAdmin()) {
abort(403, 'Unauthorized action.');
}
$request->validate([
'name' => 'required|string|max:255|unique:tenants,name',
]);
Tenant::create($request->only('name'));
return back()->with('success', 'Structure créée avec succès.');
}
public function update(Request $request, Tenant $tenant)
{
if (!auth()->user()->isSuperAdmin()) {
abort(403, 'Unauthorized action.');
}
$request->validate([
'name' => 'required|string|max:255|unique:tenants,name,' . $tenant->id,
]);
$tenant->update($request->only('name'));
return back()->with('success', 'Structure mise à jour.');
}
public function destroy(Tenant $tenant)
{
if (!auth()->user()->isSuperAdmin()) {
abort(403, 'Unauthorized action.');
}
$tenant->delete();
return back()->with('success', 'Structure supprimée.');
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use App\Models\Tenant;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Inertia\Inertia;
class UserController extends Controller
{
public function index()
{
if (!auth()->user()->isSuperAdmin()) {
abort(403, 'Unauthorized action.');
}
$users = User::whereIn('role', ['admin', 'super_admin'])
->with('tenant')
->orderBy('name')
->get();
$tenants = Tenant::orderBy('name')->get();
return Inertia::render('Admin/Users/Index', [
'users' => $users,
'tenants' => $tenants
]);
}
public function store(Request $request)
{
if (!auth()->user()->isSuperAdmin()) {
abort(403, 'Unauthorized action.');
}
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'role' => ['required', Rule::in(['admin', 'super_admin'])],
'tenant_id' => 'nullable|exists:tenants,id',
]);
$password = Str::random(10);
User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($password),
'role' => $request->role,
'tenant_id' => $request->role === 'super_admin' ? null : $request->tenant_id,
]);
return back()->with('success', 'Administrateur créé avec succès. Mot de passe généré : ' . $password);
}
public function update(Request $request, User $user)
{
if (!auth()->user()->isSuperAdmin()) {
abort(403, 'Unauthorized action.');
}
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users,email,' . $user->id,
'role' => ['required', Rule::in(['admin', 'super_admin'])],
'tenant_id' => 'nullable|exists:tenants,id',
]);
$user->update([
'name' => $request->name,
'email' => $request->email,
'role' => $request->role,
'tenant_id' => $request->role === 'super_admin' ? null : $request->tenant_id,
]);
return back()->with('success', 'Administrateur mis à jour.');
}
public function destroy(User $user)
{
if (!auth()->user()->isSuperAdmin()) {
abort(403, 'Unauthorized action.');
}
if ($user->id === auth()->id()) {
return back()->with('error', 'Vous ne pouvez pas supprimer votre propre compte.');
}
$user->delete();
return back()->with('success', 'Administrateur supprimé.');
}
}