Files
mesreleves-php/app/Http/Controllers/SetupController.php
T
yann64 6a73a2f001 Gestion utilisateurs, limites recherche, filtres lieux/sources, fix logo prod
- 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 <noreply@anthropic.com>
2026-06-07 03:39:06 +02:00

398 lines
16 KiB
PHP

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use PDO;
use PDOException;
class SetupController extends Controller
{
public function index()
{
$checks = $this->checkPrerequisites();
$allOk = collect($checks)->every(fn ($c) => $c['ok'] || ($c['optional'] ?? false));
return view('setup.index', compact('checks', 'allOk'));
}
public function database(Request $request)
{
return view('setup.database', [
'saved' => $request->session()->get('setup.database', []),
]);
}
public function saveDatabase(Request $request)
{
$data = $request->validate([
'driver' => 'required|in:pgsql,mysql',
'host' => 'required|string|max:255',
'port' => 'required|integer|min:1|max:65535',
'database' => 'required|string|max:255',
'username' => 'required|string|max:255',
'password' => 'nullable|string|max:255',
]);
$request->session()->put('setup.database', $data);
return redirect()->route('setup.application');
}
public function testDatabase(Request $request)
{
$data = $request->validate([
'driver' => 'required|in:pgsql,mysql',
'host' => 'required|string',
'port' => 'required|integer',
'database' => 'required|string',
'username' => 'required|string',
'password' => 'nullable|string',
]);
try {
$dsn = $data['driver'] === 'pgsql'
? "pgsql:host={$data['host']};port={$data['port']};dbname={$data['database']}"
: "mysql:host={$data['host']};port={$data['port']};dbname={$data['database']};charset=utf8mb4";
new PDO($dsn, $data['username'], $data['password'] ?? '', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_TIMEOUT => 5,
]);
return response()->json(['ok' => true, 'message' => 'Connexion réussie !']);
} catch (PDOException $e) {
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
}
}
public function application(Request $request)
{
if (! $request->session()->has('setup.database')) {
return redirect()->route('setup.database');
}
return view('setup.application', [
'saved' => $request->session()->get('setup.application', [
'app_name' => 'MesRelevés',
'app_url' => url('/'),
'registration_enabled' => false,
]),
]);
}
public function saveApplication(Request $request)
{
$data = $request->validate([
'app_name' => 'required|string|max:100',
'app_url' => 'required|url|max:255',
]);
$data['registration_enabled'] = $request->boolean('registration_enabled');
$request->session()->put('setup.application', $data);
return redirect()->route('setup.admin');
}
public function admin(Request $request)
{
if (! $request->session()->has('setup.database')) {
return redirect()->route('setup.database');
}
if (! $request->session()->has('setup.application')) {
return redirect()->route('setup.application');
}
return view('setup.admin');
}
public function install(Request $request)
{
$adminData = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|max:255',
'password' => 'required|string|min:8|confirmed',
]);
$dbData = $request->session()->get('setup.database');
$appData = $request->session()->get('setup.application');
if (! $dbData || ! $appData) {
return redirect()->route('setup.index')
->withErrors(['error' => 'Données manquantes, veuillez recommencer.']);
}
$steps = [];
$success = true;
// 1. Écriture du .env
try {
$this->writeEnv($dbData, $appData);
$steps[] = ['ok' => true, 'label' => 'Écriture du fichier de configuration (.env)'];
} catch (\Exception $e) {
$steps[] = ['ok' => false, 'label' => 'Écriture du fichier de configuration (.env)', 'error' => $e->getMessage()];
$success = false;
}
// 2. Génération de la clé APP_KEY — directement en PHP, sans passer par key:generate.
//
// Artisan key:generate remplace APP_KEY=<clé_en_mémoire> dans le .env grâce à un
// pattern regex. Mais writeEnv() vient d'écrire APP_KEY= (vide) alors qu'en mémoire
// la clé est celle de l'auto-création (TEMP_KEY) → le pattern ne matche pas → la
// clé reste vide dans le .env et la config:cache en hérite.
// Solution : générer la clé nous-mêmes, l'écrire directement dans le .env, et la
// propager en mémoire + env OS dès maintenant.
$appKey = null;
if ($success) {
try {
$appKey = 'base64:' . base64_encode(random_bytes(32));
$envPath = base_path('.env');
$env = file_get_contents($envPath);
$env = preg_replace('/^APP_KEY=.*/m', 'APP_KEY=' . $appKey, $env);
file_put_contents($envPath, $env);
config(['app.key' => $appKey]);
$steps[] = ['ok' => true, 'label' => 'Génération de la clé de chiffrement (APP_KEY)'];
} catch (\Exception $e) {
$steps[] = ['ok' => false, 'label' => 'Génération de la clé de chiffrement (APP_KEY)', 'error' => $e->getMessage()];
$success = false;
}
}
// 2b. Reconfiguration de la connexion BDD — processus courant ET sous-processus.
//
// putenv() écrase l'env OS hérité au boot (pgsql + TEMP_KEY) pour que tous les
// sous-processus futurs (config:cache interne à optimize…) reçoivent les bonnes
// valeurs. config() + DB::purge() reconfigure le processus courant en mémoire.
if ($success) {
putenv("APP_KEY={$appKey}");
putenv("DB_CONNECTION={$dbData['driver']}");
putenv("DB_HOST={$dbData['host']}");
putenv("DB_PORT={$dbData['port']}");
putenv("DB_DATABASE={$dbData['database']}");
putenv("DB_USERNAME={$dbData['username']}");
putenv('DB_PASSWORD=' . ($dbData['password'] ?? ''));
$connConfig = $dbData['driver'] === 'pgsql'
? ['driver' => 'pgsql', 'host' => $dbData['host'], 'port' => (int) $dbData['port'],
'database' => $dbData['database'], 'username' => $dbData['username'],
'password' => $dbData['password'] ?? '', 'charset' => 'utf8', 'prefix' => '',
'schema' => 'public', 'sslmode' => 'prefer']
: ['driver' => 'mysql', 'host' => $dbData['host'], 'port' => (int) $dbData['port'],
'database' => $dbData['database'], 'username' => $dbData['username'],
'password' => $dbData['password'] ?? '', 'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci', 'prefix' => '', 'strict' => true];
config([
'database.default' => $dbData['driver'],
"database.connections.{$dbData['driver']}" => $connConfig,
]);
DB::purge($dbData['driver']);
}
// 3. Migrations (dans le processus courant, config BDD déjà écrasée ci-dessus)
if ($success) {
try {
$exitCode = Artisan::call('migrate', ['--force' => true]);
$out = trim(Artisan::output());
$ok = $exitCode === 0;
$steps[] = ['ok' => $ok, 'label' => 'Migration de la base de données', 'error' => $ok ? null : $out];
if (! $ok) $success = false;
} catch (\Exception $e) {
$steps[] = ['ok' => false, 'label' => 'Migration de la base de données', 'error' => $e->getMessage()];
$success = false;
}
}
// 4. Création du compte administrateur
if ($success) {
try {
$this->createAdminUser($dbData, $adminData);
$steps[] = ['ok' => true, 'label' => 'Création du compte administrateur'];
} catch (\Exception $e) {
$steps[] = ['ok' => false, 'label' => 'Création du compte administrateur', 'error' => $e->getMessage()];
$success = false;
}
}
// 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 {
$dir = storage_path('app');
if (! is_dir($dir)) mkdir($dir, 0755, true);
$settings = ['registration_enabled' => (bool) ($appData['registration_enabled'] ?? false)];
file_put_contents(storage_path('app/site_settings.json'), json_encode($settings, JSON_PRETTY_PRINT));
$steps[] = ['ok' => true, 'label' => 'Paramètres du site enregistrés'];
} catch (\Exception $e) {
$steps[] = ['ok' => false, 'label' => 'Paramètres du site', 'error' => $e->getMessage()];
}
}
// 6. Nettoyage des caches
// optimize:clear supprime tout cache résiduel (config, routes, vues, events).
// On n'appelle PAS optimize : config:cache re-boostrappe l'app depuis bootstrap/app.php
// dans un contexte qui peut ne pas avoir accès à notre APP_KEY via putenv, ce qui
// provoque MissingAppKeyException. Laravel reconstruit ses caches à la première
// requête — pas besoin de les préchauffer pendant l'installation.
if ($success) {
Artisan::call('optimize:clear');
}
// 7. Marquage installation
if ($success) {
file_put_contents(storage_path('installed'), date('Y-m-d H:i:s') . PHP_EOL);
$steps[] = ['ok' => true, 'label' => 'Application marquée comme installée'];
$request->session()->forget('setup');
}
return view('setup.complete', compact('steps', 'success'));
}
// ─── Private ─────────────────────────────────────────────────────────────
private function checkPrerequisites(): array
{
$checks = [];
$checks[] = [
'label' => 'PHP 8.2 ou supérieur',
'ok' => PHP_VERSION_ID >= 80200,
'value' => PHP_VERSION,
'optional' => false,
];
$extensions = [
'pdo' => false,
'mbstring' => false,
'tokenizer' => false,
'xml' => false,
'ctype' => false,
'json' => false,
'bcmath' => false,
'openssl' => false,
'fileinfo' => false,
'zip' => false,
'pdo_pgsql' => true,
'pdo_mysql' => true,
];
foreach ($extensions as $ext => $optional) {
$loaded = extension_loaded($ext);
$checks[] = [
'label' => "Extension PHP : {$ext}",
'ok' => $loaded,
'value' => $loaded ? 'Présente' : 'Manquante',
'optional' => $optional,
];
}
$dirs = [
'storage/' => storage_path(),
'bootstrap/cache/' => base_path('bootstrap/cache'),
'Racine (écriture .env)' => base_path(),
];
foreach ($dirs as $label => $path) {
$writable = is_writable($path);
$checks[] = [
'label' => "Répertoire accessible en écriture : {$label}",
'ok' => $writable,
'value' => $writable ? 'OK' : 'Non accessible',
'optional' => false,
];
}
return $checks;
}
private function writeEnv(array $db, array $app): void
{
$envPath = base_path('.env');
$envExamplePath = base_path('.env.example');
$overrides = [
'APP_NAME' => '"' . str_replace('"', '\\"', $app['app_name']) . '"',
'APP_URL' => rtrim($app['app_url'], '/'),
'APP_ENV' => 'production',
'APP_DEBUG' => 'false',
'APP_KEY' => '',
'DB_CONNECTION' => $db['driver'],
'DB_HOST' => $db['host'],
'DB_PORT' => (string) $db['port'],
'DB_DATABASE' => $db['database'],
'DB_USERNAME' => $db['username'],
'DB_PASSWORD' => '"' . str_replace('"', '\\"', $db['password'] ?? '') . '"',
'SESSION_DRIVER' => 'database',
'CACHE_STORE' => 'database',
'QUEUE_CONNECTION' => 'database',
];
$env = file_exists($envExamplePath) ? file_get_contents($envExamplePath) : '';
foreach ($overrides as $key => $value) {
$pattern = '/^' . preg_quote($key, '/') . '=.*/m';
if (preg_match($pattern, $env)) {
$env = preg_replace($pattern, "{$key}={$value}", $env);
} else {
$env .= "\n{$key}={$value}";
}
}
if (file_put_contents($envPath, $env) === false) {
throw new \RuntimeException("Impossible d'écrire le fichier .env — vérifiez les permissions du dossier.");
}
}
private function createAdminUser(array $db, array $admin): void
{
$dsn = $db['driver'] === 'pgsql'
? "pgsql:host={$db['host']};port={$db['port']};dbname={$db['database']}"
: "mysql:host={$db['host']};port={$db['port']};dbname={$db['database']};charset=utf8mb4";
$pdo = new PDO($dsn, $db['username'], $db['password'] ?? '', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$isActive = $db['driver'] === 'pgsql' ? 'true' : '1';
$stmt = $pdo->prepare(
"INSERT INTO users (name, email, password, role, is_active, email_verified_at, created_at, updated_at)
VALUES (:name, :email, :password, 'admin', {$isActive}, NOW(), NOW(), NOW())"
);
$stmt->execute([
':name' => $admin['name'],
':email' => $admin['email'],
':password' => Hash::make($admin['password']),
]);
}
private function artisanRun(string $artisan, string $command): array
{
$out = [];
$code = 0;
exec("{$artisan} {$command} 2>&1", $out, $code);
return [$code === 0, implode("\n", $out)];
}
private function phpBinary(): string
{
$bin = PHP_BINARY;
return is_executable($bin) ? $bin : 'php';
}
}