From 6a73a2f00167fafea604074aa5755eecdb87880a Mon Sep 17 00:00:00 2001 From: yann64 Date: Sun, 7 Jun 2026 03:39:06 +0200 Subject: [PATCH] Gestion utilisateurs, limites recherche, filtres lieux/sources, fix logo prod MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Admin : CRUD complet utilisateurs (créer, modifier nom/email/mdp/rôle, supprimer) avec garde-fous (dernier admin, compte propre) - Recherche : limite configurable par l'admin (défaut 200), bannière d'avertissement quand la limite est atteinte, plus de pagination (résultats en bloc) - Lieux : liste non chargée sans filtre actif (performance sur grands volumes) - Sources : idem pour admin/responsables ; membres voient toujours leurs sources - Logo 404 prod : +FollowSymLinks dans .htaccess, storage:link dans l'assistant d'installation, bouton "Recréer le lien" dans Administration → Paramètres Co-Authored-By: Claude Sonnet 4.6 --- .../Controllers/Admin/SettingController.php | 16 +- app/Http/Controllers/Admin/UserController.php | 83 +++++++- app/Http/Controllers/LieuController.php | 5 +- app/Http/Controllers/RechercheController.php | 18 +- app/Http/Controllers/SetupController.php | 11 + app/Http/Controllers/SourceController.php | 11 +- app/Services/SiteSettingsService.php | 7 + public/.htaccess | 2 +- .../views/admin/parametres/index.blade.php | 34 +++ .../views/admin/utilisateurs/create.blade.php | 101 +++++++++ .../views/admin/utilisateurs/edit.blade.php | 194 ++++++++++++------ .../views/admin/utilisateurs/index.blade.php | 19 +- resources/views/lieux/index.blade.php | 15 +- resources/views/recherche/index.blade.php | 35 ++-- resources/views/sources/index.blade.php | 9 + routes/admin.php | 3 +- 16 files changed, 464 insertions(+), 99 deletions(-) create mode 100644 resources/views/admin/utilisateurs/create.blade.php diff --git a/app/Http/Controllers/Admin/SettingController.php b/app/Http/Controllers/Admin/SettingController.php index 93ce0ce..d481845 100644 --- a/app/Http/Controllers/Admin/SettingController.php +++ b/app/Http/Controllers/Admin/SettingController.php @@ -162,13 +162,27 @@ class SettingController extends Controller public function updateSettings(Request $request): RedirectResponse { $data = $request->validate([ - 'site_name' => ['nullable', 'string', 'max:100'], + 'site_name' => ['nullable', 'string', 'max:100'], + 'search_max_results' => ['nullable', 'integer', 'min:10', 'max:5000'], ]); $siteName = trim($data['site_name'] ?? ''); SiteSettingsService::set('site_name', $siteName ?: null); SiteSettingsService::set('registration_enabled', $request->boolean('registration_enabled')); + if (isset($data['search_max_results'])) { + SiteSettingsService::set('search_max_results', (int) $data['search_max_results']); + } return back()->with('success', 'Paramètres enregistrés.'); } + + public function storageLink(): RedirectResponse + { + try { + \Illuminate\Support\Facades\Artisan::call('storage:link'); + return back()->with('success', 'Lien de stockage public créé (public/storage → storage/app/public).'); + } catch (\Exception $e) { + return back()->with('error', 'Impossible de créer le lien : ' . $e->getMessage()); + } + } } diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 2004183..6315415 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -208,6 +208,33 @@ class UserController extends Controller return view('admin.utilisateurs.import', compact('results', 'created', 'errors')); } + public function create(): View + { + return view('admin.utilisateurs.create'); + } + + public function store(Request $request): RedirectResponse + { + $data = $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'email', 'max:255', 'unique:users,email'], + 'password' => ['required', 'string', 'min:8', 'confirmed'], + 'role' => ['required', new Enum(UserRole::class)], + ]); + + $user = User::create([ + 'name' => $data['name'], + 'email' => $data['email'], + 'password' => Hash::make($data['password']), + 'role' => $data['role'], + 'is_active' => true, + 'email_verified_at' => now(), + ]); + + return redirect()->route('admin.utilisateurs.edit', $user) + ->with('success', 'Utilisateur créé.'); + } + public function edit(User $user): View { $user->load('sections', 'sourcesAssignees'); @@ -217,24 +244,64 @@ class UserController extends Controller public function update(Request $request, User $user): RedirectResponse { - $data = $request->validate([ - 'role' => ['required', new Enum(UserRole::class)], - ]); + $isSelf = $user->id === auth()->id(); - if ($user->id === auth()->id()) { - return back()->with('error', 'Vous ne pouvez pas modifier votre propre rôle.'); + $rules = [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'email', 'max:255', 'unique:users,email,' . $user->id], + 'role' => ['required', new Enum(UserRole::class)], + ]; + + if ($request->filled('password')) { + $rules['password'] = ['string', 'min:8', 'confirmed']; + $rules['password_confirmation'] = ['required']; } - if ($user->role === UserRole::Admin && $data['role'] !== UserRole::Admin->value) { + $data = $request->validate($rules); + + // Protection : retrait du dernier admin ou de son propre rôle + if (! $isSelf && $user->role === UserRole::Admin && $data['role'] !== UserRole::Admin->value) { $adminCount = User::where('role', UserRole::Admin->value)->count(); if ($adminCount <= 1) { return back()->with('error', 'Impossible de retirer le rôle admin au dernier administrateur.'); } } - $user->update(['role' => $data['role']]); + $update = [ + 'name' => $data['name'], + 'email' => $data['email'], + ]; - return back()->with('success', 'Rôle mis à jour.'); + if (! $isSelf) { + $update['role'] = $data['role']; + } + + if ($request->filled('password')) { + $update['password'] = Hash::make($data['password']); + } + + $user->update($update); + + return back()->with('success', 'Utilisateur mis à jour.'); + } + + public function destroy(User $user): RedirectResponse + { + if ($user->id === auth()->id()) { + return back()->with('error', 'Vous ne pouvez pas supprimer votre propre compte.'); + } + + if ($user->role === UserRole::Admin) { + $adminCount = User::where('role', UserRole::Admin->value)->count(); + if ($adminCount <= 1) { + return back()->with('error', 'Impossible de supprimer le dernier administrateur.'); + } + } + + $user->delete(); + + return redirect()->route('admin.utilisateurs.index') + ->with('success', 'Utilisateur supprimé.'); } public function toggleActive(User $user): RedirectResponse diff --git a/app/Http/Controllers/LieuController.php b/app/Http/Controllers/LieuController.php index e087e73..b63b08d 100644 --- a/app/Http/Controllers/LieuController.php +++ b/app/Http/Controllers/LieuController.php @@ -66,9 +66,10 @@ class LieuController extends Controller $lieuSelectionne = $request->filled('lieu_id') ? Lieu::find($request->integer('lieu_id'), ['id', 'nom', 'nom_long']) : null; - $lieux = $query->paginate(50)->withQueryString(); + $hasFilters = $request->anyFilled(['lieu_type_id', 'q', 'lieu_id']); + $lieux = $hasFilters ? $query->paginate(50)->withQueryString() : null; - return view('lieux.index', compact('lieux', 'lieuTypes', 'lieuSelectionne')); + return view('lieux.index', compact('lieux', 'lieuTypes', 'lieuSelectionne', 'hasFilters')); } public function create(): View diff --git a/app/Http/Controllers/RechercheController.php b/app/Http/Controllers/RechercheController.php index 0a1e246..d9da665 100644 --- a/app/Http/Controllers/RechercheController.php +++ b/app/Http/Controllers/RechercheController.php @@ -6,6 +6,7 @@ use App\Enums\SourceStatus; use App\Models\Lieu; use App\Models\Releve; use App\Models\SourceType; +use App\Services\SiteSettingsService; use App\Support\DbCompat; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; @@ -18,6 +19,7 @@ class RechercheController extends Controller $sourceTypes = SourceType::orderBy('nom')->get(['id', 'nom']); $resultats = null; $total = null; + $limited = false; // Charger le lieu sélectionné pour pré-remplir le picker $lieuSelectionne = $request->filled('lieu_id') @@ -25,10 +27,10 @@ class RechercheController extends Controller : null; if ($request->anyFilled(['q', 'source_type_id', 'lieu_id', 'annee_debut', 'annee_fin'])) { - [$resultats, $total] = $this->search($request); + [$resultats, $total, $limited] = $this->search($request); } - return view('recherche.index', compact('sourceTypes', 'resultats', 'total', 'lieuSelectionne')); + return view('recherche.index', compact('sourceTypes', 'resultats', 'total', 'limited', 'lieuSelectionne')); } private function search(Request $request): array @@ -85,15 +87,17 @@ class RechercheController extends Controller $query->whereRaw("date_evenement <= ?", [$request->integer('annee_fin') . '-12-31']); } - // ── Tri + pagination ──────────────────────────────────────────────── - $total = $query->count(); + // ── Limite configurable par l'admin ───────────────────────────────── + $max = SiteSettingsService::searchMaxResults(); + $total = $query->count(); + $resultats = $query ->orderByRaw(DbCompat::nullsLast('nom')) ->orderByRaw(DbCompat::nullsLast('date_evenement')) - ->paginate(25) - ->withQueryString(); + ->limit($max) + ->get(); - return [$resultats, $total]; + return [$resultats, $total, $total > $max]; } /** diff --git a/app/Http/Controllers/SetupController.php b/app/Http/Controllers/SetupController.php index 8e84465..7f5e8a5 100644 --- a/app/Http/Controllers/SetupController.php +++ b/app/Http/Controllers/SetupController.php @@ -219,6 +219,17 @@ class SetupController extends Controller } } + // 4b. Lien de stockage public (symlink public/storage → storage/app/public) + // Non bloquant : l'installation continue même si le serveur interdit les symlinks. + if ($success) { + try { + \Illuminate\Support\Facades\Artisan::call('storage:link'); + $steps[] = ['ok' => true, 'label' => 'Lien de stockage public créé']; + } catch (\Exception $e) { + $steps[] = ['ok' => false, 'label' => 'Lien de stockage public (non bloquant — créez-le manuellement via « Administration → Paramètres »)', 'error' => $e->getMessage()]; + } + } + // 5. Paramètres du site if ($success) { try { diff --git a/app/Http/Controllers/SourceController.php b/app/Http/Controllers/SourceController.php index 6a37113..8c824e8 100644 --- a/app/Http/Controllers/SourceController.php +++ b/app/Http/Controllers/SourceController.php @@ -69,9 +69,16 @@ class SourceController extends Controller ? Lieu::find($request->integer('lieu_id'), ['id', 'nom', 'nom_long']) : null; - $sources = $query->orderBy('nom')->paginate(25)->withQueryString(); + // Pour les admins/responsables, exiger au moins un filtre avant d'afficher + // les résultats (ils peuvent voir potentiellement des milliers de sources). + // Les membres normaux voient toujours leurs sources (déjà filtrées par accès). + $hasFilters = $request->anyFilled(['status', 'source_type_id', 'lieu_id', 'annee_debut', 'annee_fin']); + $requiresFilter = $user->isSectionManager(); + $sources = ($requiresFilter && ! $hasFilters) + ? null + : $query->orderBy('nom')->paginate(25)->withQueryString(); - return view('sources.index', compact('sources', 'sourceTypes', 'lieuSelectionne')); + return view('sources.index', compact('sources', 'sourceTypes', 'lieuSelectionne', 'hasFilters')); } private function getLieuDescendantIds(int $lieuId): array diff --git a/app/Services/SiteSettingsService.php b/app/Services/SiteSettingsService.php index c77e7dd..c882d76 100644 --- a/app/Services/SiteSettingsService.php +++ b/app/Services/SiteSettingsService.php @@ -88,6 +88,13 @@ class SiteSettingsService return (bool) self::get('registration_enabled', false); } + // ── Recherche ──────────────────────────────────────────────────────────────── + + public static function searchMaxResults(): int + { + return max(10, (int) self::get('search_max_results', 200)); + } + // ── Mises à jour ────────────────────────────────────────────────────────── public static function updatesDisabled(): bool diff --git a/public/.htaccess b/public/.htaccess index 68b2ef9..60b0d3c 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -1,5 +1,5 @@ # ── Sécurité ────────────────────────────────────────────────────────────────── -Options -Indexes -MultiViews +Options -Indexes -MultiViews +FollowSymLinks # ── En-têtes HTTP transmis à PHP ─────────────────────────────────────────────── # Nécessaire pour que Laravel reçoive le token Authorization (API) et CSRF diff --git a/resources/views/admin/parametres/index.blade.php b/resources/views/admin/parametres/index.blade.php index d79b2f3..8df5a80 100644 --- a/resources/views/admin/parametres/index.blade.php +++ b/resources/views/admin/parametres/index.blade.php @@ -37,6 +37,24 @@ @enderror + {{-- Nombre maximum de résultats de recherche --}} +
+ + +

+ La page de recherche affiche au plus ce nombre de relevés. Si la limite est atteinte, un message invite l'utilisateur à affiner ses critères. (10 – 5000, défaut : 200) +

+ @error('search_max_results') +

{{ $message }}

+ @enderror +
+ {{-- Inscriptions --}}

Inscription publique des comptes

@@ -62,6 +80,22 @@
+ {{-- Lien de stockage public --}} +
+

Lien de stockage

+

+ Le lien symbolique public/storage permet de servir les fichiers (logo, etc.) via l'URL /storage/…. + S'il est absent, le logo sera invisible et d'autres fichiers seront inaccessibles. +

+
+ @csrf + +
+
+ {{-- Logo --}}

Logo du site

diff --git a/resources/views/admin/utilisateurs/create.blade.php b/resources/views/admin/utilisateurs/create.blade.php new file mode 100644 index 0000000..5d6e767 --- /dev/null +++ b/resources/views/admin/utilisateurs/create.blade.php @@ -0,0 +1,101 @@ + + +
+ ← Utilisateurs + / +

Nouvel utilisateur

+
+
+ +
+ +
+
+ @csrf + +
+ + + @error('name') +

{{ $message }}

+ @enderror +
+ +
+ + + @error('email') +

{{ $message }}

+ @enderror +
+ +
+ + + @error('password') +

{{ $message }}

+ @enderror +
+ +
+ + +
+ +
+ +
+ @foreach(\App\Enums\UserRole::cases() as $role) + + @endforeach +
+ @error('role') +

{{ $message }}

+ @enderror +
+ +
+ + + Annuler + +
+
+
+
+
diff --git a/resources/views/admin/utilisateurs/edit.blade.php b/resources/views/admin/utilisateurs/edit.blade.php index b0cc9db..ff2e5fa 100644 --- a/resources/views/admin/utilisateurs/edit.blade.php +++ b/resources/views/admin/utilisateurs/edit.blade.php @@ -16,27 +16,110 @@
{{ session('error') }}
@endif - {{-- Informations --}} -
-

Informations

-
-
Nom
-
{{ $user->name }}
-
E-mail
-
{{ $user->email }}
-
Inscrit le
-
{{ $user->created_at->format('d/m/Y') }}
-
Sections
-
- @if($user->sections->isNotEmpty()) - {{ $user->sections->pluck('nom')->join(', ') }} + {{-- Formulaire principal --}} +
+

Informations

+ +
+ @csrf @method('PUT') + +
+
+ + + @error('name') +

{{ $message }}

+ @enderror +
+ +
+ + + @error('email') +

{{ $message }}

+ @enderror +
+ +
+ +
+
+ + + @error('password') +

{{ $message }}

+ @enderror +
+
+ + +
+
+
+ + {{-- Rôle (masqué pour soi-même) --}} + @if($user->id !== auth()->id()) +
+ +
+ @foreach(\App\Enums\UserRole::cases() as $role) + + @endforeach +
+
@else - — + {{-- Champ caché pour ne pas perdre le rôle lors du submit --}} + +

+ Vous ne pouvez pas modifier votre propre rôle. +

@endif -
-
Sources assignées
-
{{ $user->sourcesAssignees->count() }}
-
+
+ +
+ + + Annuler + +
+
{{-- Statut actif / inactif --}} @@ -45,7 +128,7 @@

Statut du compte

@if($user->is_active) - Le compte est actif — l'utilisateur peut se connecter et être assigné à des sources. + Le compte est actif — l'utilisateur peut se connecter. @else Le compte est inactif — l'utilisateur ne peut pas se connecter. @endif @@ -67,44 +150,39 @@ @endif - {{-- Modifier le rôle --}} -

-

Rôle

-
- @csrf @method('PUT') -
- @foreach(\App\Enums\UserRole::cases() as $role) - - @endforeach -
-
- - - Annuler - -
-
+ {{-- Informations complémentaires --}} +
+

Détails

+
+
Inscrit le
+
{{ $user->created_at->format('d/m/Y') }}
+
Sections
+
+ {{ $user->sections->isNotEmpty() ? $user->sections->pluck('nom')->join(', ') : '—' }} +
+
Sources assignées
+
{{ $user->sourcesAssignees->count() ?: '—' }}
+
+ + {{-- Zone danger : suppression --}} + @if($user->id !== auth()->id()) +
+

Zone dangereuse

+

+ La suppression est définitive. Les relevés et assignations liés à cet utilisateur seront également supprimés. +

+
+ @csrf @method('DELETE') + +
+
+ @endif +
diff --git a/resources/views/admin/utilisateurs/index.blade.php b/resources/views/admin/utilisateurs/index.blade.php index d8c4e10..6b8731d 100644 --- a/resources/views/admin/utilisateurs/index.blade.php +++ b/resources/views/admin/utilisateurs/index.blade.php @@ -3,6 +3,10 @@

Gestion des utilisateurs

- {{-- Tableau --}} + {{-- Tableau (uniquement si un filtre est actif) --}} + @if($lieux !== null)
@@ -142,8 +143,7 @@ @empty @endforelse @@ -156,5 +156,14 @@ @endif + @else +
+ + + + +

Utilisez les filtres ci-dessus pour rechercher des lieux.

+
+ @endif diff --git a/resources/views/recherche/index.blade.php b/resources/views/recherche/index.blade.php index 54ceb05..b524541 100644 --- a/resources/views/recherche/index.blade.php +++ b/resources/views/recherche/index.blade.php @@ -102,14 +102,28 @@ {{-- Résultats --}} @if($resultats !== null)
-

- @if($total === 0) - Aucun relevé trouvé. - @else - {{ number_format($total) }} relevé{{ $total > 1 ? 's' : '' }} trouvé{{ $total > 1 ? 's' : '' }} - @if(request('q')) pour « {{ request('q') }} » @endif - @endif -

+
+

+ @if($total === 0) + Aucun relevé trouvé. + @else + {{ number_format($total) }} relevé{{ $total > 1 ? 's' : '' }} trouvé{{ $total > 1 ? 's' : '' }} + @if(request('q')) pour « {{ request('q') }} » @endif + @endif +

+
+ + @if($limited) +
+ + + +

+ Seuls les {{ number_format($resultats->count()) }} premiers résultats sur {{ number_format($total) }} sont affichés. + Affinez vos critères de recherche pour obtenir des résultats plus précis. +

+
+ @endif @if($resultats->isNotEmpty())
@@ -175,11 +189,6 @@
- @if($hasFilters) Aucun lieu ne correspond aux filtres. - @else Aucun lieu enregistré. @endif + Aucun lieu ne correspond aux filtres.
- @if($resultats->hasPages()) -
- {{ $resultats->links() }} -
- @endif
@endif diff --git a/resources/views/sources/index.blade.php b/resources/views/sources/index.blade.php index 57738f9..11f9f10 100644 --- a/resources/views/sources/index.blade.php +++ b/resources/views/sources/index.blade.php @@ -97,6 +97,14 @@ {{-- Tableau --}} + @if($sources === null) +
+ + + +

Utilisez les filtres ci-dessus pour afficher les sources.

+
+ @else
@@ -165,5 +173,6 @@
{{ $sources->links() }}
@endif + @endif diff --git a/routes/admin.php b/routes/admin.php index a69b385..aacc173 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -21,6 +21,7 @@ Route::middleware(['auth', 'role:admin'])->prefix('admin')->name('admin.')->grou Route::delete('parametres/smtp', [SettingController::class, 'deleteSmtp'])->name('parametres.smtp.delete'); Route::post('parametres/smtp/test', [SettingController::class, 'testSmtp'])->name('parametres.smtp.test'); Route::post('parametres/updates', [SettingController::class, 'updateUpdates'])->name('parametres.updates'); + Route::post('parametres/storage-link', [SettingController::class, 'storageLink'])->name('parametres.storage-link'); // Routes spécifiques avant la resource pour éviter les conflits de paramètre Route::get('utilisateurs/export', [UserController::class, 'export'])->name('utilisateurs.export'); @@ -28,7 +29,7 @@ Route::middleware(['auth', 'role:admin'])->prefix('admin')->name('admin.')->grou Route::post('utilisateurs/import', [UserController::class, 'import'])->name('utilisateurs.import.store'); Route::get('utilisateurs/import/modele', [UserController::class, 'importTemplate'])->name('utilisateurs.import.modele'); - Route::resource('utilisateurs', UserController::class)->only(['index', 'edit', 'update']); + Route::resource('utilisateurs', UserController::class)->only(['index', 'create', 'store', 'edit', 'update', 'destroy']); Route::post('utilisateurs/{utilisateur}/toggle-active', [UserController::class, 'toggleActive'])->name('utilisateurs.toggle-active'); Route::resource('lieu-types', LieuTypeController::class) ->parameters(['lieu-types' => 'lieuType'])