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 +Inscription publique des comptes
@@ -62,6 +80,22 @@
+ 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.
+
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
+ La suppression est définitive. Les relevés et assignations liés à cet utilisateur seront également supprimés. +
+ +| - @if($hasFilters) Aucun lieu ne correspond aux filtres. - @else Aucun lieu enregistré. @endif + Aucun lieu ne correspond aux filtres. |
Utilisez les filtres ci-dessus pour afficher les sources.
+