Ajout de l'assistant d'installation web et corrections de navigation

- Wizard d'installation en 5 étapes (/setup) : prérequis PHP, base de données
  (PostgreSQL/MySQL avec test de connexion AJAX), paramètres app, compte admin,
  résultat — génère le .env, migre et crée l'administrateur
- CheckInstallation middleware : redirige vers /setup si non installé,
  protège /setup si déjà installé ; storage/installed comme marqueur
- Menu Administration : remplacé par le composant x-dropdown Breeze (même
  positionnement que le menu utilisateur — corrige le débordement en haut)
- Logo navbar : adaptatif via h-full/py-1.5 (s'adapte à la hauteur de la barre)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 18:39:55 +02:00
parent 236d37976c
commit caf7ad7fe2
12 changed files with 906 additions and 42 deletions
+326
View File
@@ -0,0 +1,326 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
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;
$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';
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CheckInstallation
{
public function handle(Request $request, Closure $next): Response
{
$installed = file_exists(storage_path('installed'));
$onSetup = $request->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);
}
}