diff --git a/.gitignore b/.gitignore index b71b1ea..6440ebb 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ /public/hot /public/storage /storage/*.key +/storage/installed /storage/pail /vendor Homestead.json diff --git a/app/Http/Controllers/SetupController.php b/app/Http/Controllers/SetupController.php new file mode 100644 index 0000000..c4e7f27 --- /dev/null +++ b/app/Http/Controllers/SetupController.php @@ -0,0 +1,326 @@ +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; + $php = $this->phpBinary(); + $artisan = $php . ' ' . escapeshellarg(base_path('artisan')); + + // 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 + if ($success) { + [$ok, $out] = $this->artisanRun($artisan, 'key:generate --force'); + $steps[] = ['ok' => $ok, 'label' => 'Génération de la clé de chiffrement (APP_KEY)', 'error' => $ok ? null : $out]; + if (! $ok) $success = false; + } + + // 3. Migrations + if ($success) { + [$ok, $out] = $this->artisanRun($artisan, 'migrate --force'); + $steps[] = ['ok' => $ok, 'label' => 'Migration de la base de données', 'error' => $ok ? null : $out]; + if (! $ok) $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; + } + } + + // 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. Optimisation des caches + if ($success) { + $this->artisanRun($artisan, 'optimize:clear'); + $this->artisanRun($artisan, 'optimize'); + } + + // 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'; + } +} diff --git a/app/Http/Middleware/CheckInstallation.php b/app/Http/Middleware/CheckInstallation.php new file mode 100644 index 0000000..3d946d6 --- /dev/null +++ b/app/Http/Middleware/CheckInstallation.php @@ -0,0 +1,30 @@ +is('setup') || $request->is('setup/*'); + + if (! $installed && ! $onSetup) { + // Laisser passer les health checks et réponses JSON + if ($request->is('up') || $request->expectsJson()) { + return $next($request); + } + return redirect('/setup'); + } + + if ($installed && $onSetup) { + return redirect('/'); + } + + return $next($request); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 8549938..440f8cb 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -18,6 +18,7 @@ return Application::configure(basePath: dirname(__DIR__)) $middleware->alias([ 'role' => \App\Http\Middleware\RoleMiddleware::class, ]); + $middleware->appendToGroup('web', \App\Http\Middleware\CheckInstallation::class); $middleware->appendToGroup('web', \App\Http\Middleware\EnsureUserIsActive::class); }) ->withExceptions(function (Exceptions $exceptions): void { diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php index 283ec87..fac9459 100644 --- a/resources/views/layouts/navigation.blade.php +++ b/resources/views/layouts/navigation.blade.php @@ -3,11 +3,12 @@
Ce compte aura les droits complets sur l'application. Vous pourrez en créer d'autres ensuite.
+ + @if($errors->any()) +{{ $e }}
@endforeach +Configurez le nom du site et l'URL d'accès.
+ + @if($errors->any()) +{{ $e }}
@endforeach +MesRelevés est prêt à être utilisé.
+ @else +Corrigez les problèmes ci-dessous puis relancez l'installation.
+ @endif +{{ $step['error'] }}
+ @endif
+ Entrez les paramètres de connexion. Testez la connexion avant de continuer.
+ + @if($errors->any()) +{{ $e }}
@endforeach +Assurez-vous que l'environnement serveur est compatible avant de continuer.
+ +Assistant d'installation
+