ed5cfcd275
- APP_KEY : généré directement en PHP (random_bytes) + écrit dans .env + propagé via config() et putenv() → évite le bug de pattern-matching de key:generate (la clé en mémoire ≠ clé dans le .env réécrit par writeEnv) - DB_* + APP_KEY : putenv() écrase l'env OS hérité au boot (pgsql/temp-key) pour que tout sous-processus futur hérite des bonnes valeurs - optimize supprimé de l'installation : config:cache re-boostrappe l'app via bootstrap/app.php dans un contexte où l'Encrypter peut lever MissingAppKeyException ; optimize:clear seul suffit — Laravel reconstruit ses caches à la première requête - key:generate converti en Artisan::call() puis remplacé par génération PHP directe Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
387 lines
15 KiB
PHP
387 lines
15 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;
|
|
}
|
|
}
|
|
|
|
// 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';
|
|
}
|
|
}
|