From da589d0627623a126d2cb57b23056b5bbd9df187 Mon Sep 17 00:00:00 2001 From: mrKamoo Date: Thu, 20 Aug 2026 08:58:29 +0200 Subject: [PATCH] =?UTF-8?q?Ajoute=20un=20menu=20admin=20pour=20=C3=A9diter?= =?UTF-8?q?=20les=20variables=20.env=20de=20l'app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nouvelle page "Configuration (.env)" réservée au rôle Administrateur : - EnvFileManager lit/réécrit le .env ligne à ligne en préservant commentaires et mise en forme, avec écriture atomique (fichier temporaire + rename). - Variables critiques (APP_KEY, DB_*, SESSION_DRIVER) en lecture seule pour éviter de casser la connexion base de données ou de bloquer l'accès admin — protection appliquée aussi côté serveur, pas seulement dans l'affichage. - Valeurs sensibles (mots de passe, jetons, clés) masquées par défaut avec un bouton de révélation par champ. Co-Authored-By: Claude Sonnet 5 --- app/Services/EnvFileManager.php | 160 ++++++++++++++++++ .../views/components/layouts/app.blade.php | 4 + .../views/components/settings/⚡env.blade.php | 134 +++++++++++++++ resources/views/settings/env.blade.php | 3 + routes/web.php | 4 + 5 files changed, 305 insertions(+) create mode 100644 app/Services/EnvFileManager.php create mode 100644 resources/views/components/settings/⚡env.blade.php create mode 100644 resources/views/settings/env.blade.php diff --git a/app/Services/EnvFileManager.php b/app/Services/EnvFileManager.php new file mode 100644 index 0000000..32fe3df --- /dev/null +++ b/app/Services/EnvFileManager.php @@ -0,0 +1,160 @@ +path = $path ?: base_path('.env'); + } + + /** + * @return array + */ + public function read(): array + { + if (! is_file($this->path)) { + throw new RuntimeException('Le fichier .env est introuvable.'); + } + + $lines = preg_split('/\r\n|\r|\n/', file_get_contents($this->path)); + $parsed = []; + + foreach ($lines as $line) { + $trimmed = trim($line); + + if ($trimmed === '') { + $parsed[] = ['type' => 'blank']; + + continue; + } + + if (str_starts_with($trimmed, '#')) { + $parsed[] = ['type' => 'comment', 'text' => ltrim(substr($trimmed, 1))]; + + continue; + } + + if (! str_contains($trimmed, '=')) { + $parsed[] = ['type' => 'raw', 'raw' => $line]; + + continue; + } + + [$key, $rawValue] = explode('=', $trimmed, 2); + $key = trim($key); + + $parsed[] = [ + 'type' => 'var', + 'key' => $key, + 'value' => $this->unquote(trim($rawValue)), + 'protected' => in_array($key, self::PROTECTED_KEYS, true), + 'sensitive' => $this->isSensitive($key), + ]; + } + + return $parsed; + } + + public function isSensitive(string $key): bool + { + return (bool) preg_match('/PASSWORD|SECRET|TOKEN|KEY/i', $key); + } + + /** + * Rewrites only the given keys' values, leaving every other line (order, + * comments, blanks, untouched keys) exactly as-is. Protected keys are + * silently ignored even if present in $updates — never trust the form. + * + * @param array $updates + */ + public function update(array $updates): void + { + foreach ($updates as $key => $value) { + if (str_contains($value, "\n") || str_contains($value, "\r")) { + throw new RuntimeException("La valeur de {$key} ne peut pas contenir de retour à la ligne."); + } + } + + if (! is_writable($this->path)) { + throw new RuntimeException("Le fichier .env n'est pas accessible en écriture par le serveur web."); + } + + $lines = preg_split('/\r\n|\r|\n/', file_get_contents($this->path)); + $output = []; + + foreach ($lines as $line) { + $trimmed = trim($line); + $replaced = false; + + if ($trimmed !== '' && ! str_starts_with($trimmed, '#') && str_contains($trimmed, '=')) { + [$key] = explode('=', $trimmed, 2); + $key = trim($key); + + if (array_key_exists($key, $updates) && ! in_array($key, self::PROTECTED_KEYS, true)) { + $output[] = $key.'='.$this->quote($updates[$key]); + $replaced = true; + } + } + + if (! $replaced) { + $output[] = $line; + } + } + + // Write to a temp file in the same directory first, then rename — + // rename() is atomic, so a request reading .env mid-save never sees + // a half-written file. + $tmpPath = $this->path.'.tmp-'.bin2hex(random_bytes(4)); + file_put_contents($tmpPath, implode(PHP_EOL, $output)); + rename($tmpPath, $this->path); + } + + protected function unquote(string $value): string + { + if (strlen($value) >= 2 && $value[0] === '"' && str_ends_with($value, '"')) { + return stripcslashes(substr($value, 1, -1)); + } + + if (strlen($value) >= 2 && $value[0] === "'" && str_ends_with($value, "'")) { + return substr($value, 1, -1); + } + + return $value; + } + + protected function quote(string $value): string + { + if ($value === '' || preg_match('/[\s#"]/', $value)) { + return '"'.addcslashes($value, '"\\').'"'; + } + + return $value; + } +} diff --git a/resources/views/components/layouts/app.blade.php b/resources/views/components/layouts/app.blade.php index c4d3e0f..4af9964 100644 --- a/resources/views/components/layouts/app.blade.php +++ b/resources/views/components/layouts/app.blade.php @@ -131,6 +131,10 @@ class="block rounded px-3 py-2 hover:bg-slate-800 {{ request()->routeIs('users.*') ? 'bg-slate-800 text-white' : '' }}"> Utilisateurs + + Configuration (.env) + @endrole diff --git a/resources/views/components/settings/⚡env.blade.php b/resources/views/components/settings/⚡env.blade.php new file mode 100644 index 0000000..e6e4091 --- /dev/null +++ b/resources/views/components/settings/⚡env.blade.php @@ -0,0 +1,134 @@ +> */ + public array $lines = []; + + /** @var array */ + public array $values = []; + + public function mount(): void + { + $this->loadLines(); + } + + public function save(): void + { + foreach ($this->values as $key => $value) { + if (str_contains($value, "\n") || str_contains($value, "\r")) { + $this->addError('values', "La valeur de {$key} ne peut pas contenir de retour à la ligne."); + + return; + } + } + + try { + app(EnvFileManager::class)->update($this->values); + } catch (\Throwable $e) { + session()->flash('error', $e->getMessage()); + + return; + } + + session()->flash('status', 'Configuration enregistrée. Les nouvelles valeurs seront actives dès la prochaine requête.'); + + $this->loadLines(); + } + + protected function loadLines(): void + { + $this->lines = app(EnvFileManager::class)->read(); + $this->values = []; + + foreach ($this->lines as $line) { + if ($line['type'] === 'var' && ! $line['protected']) { + $this->values[$line['key']] = $line['value']; + } + } + } +}; +?> + +
+

Configuration (.env)

+

+ Modifie directement les variables d'environnement de l'application. Les changements + prennent effet à la prochaine requête — aucun redémarrage n'est nécessaire. +

+ + @if (session('status')) +
+ {{ session('status') }} +
+ @endif + + @if (session('error')) +
+ {{ session('error') }} +
+ @endif + + @error('values') +
+ {{ $message }} +
+ @enderror + +
+ @foreach ($lines as $line) + @if ($line['type'] === 'comment') + @if ($line['text'] !== '') +

+ {{ $line['text'] }} +

+ @endif + @elseif ($line['type'] === 'var') +
+ + + @if ($line['protected']) +
+ + + 🔒 lecture seule + +
+ @elseif ($line['sensitive']) +
+ + +
+ @else + + @endif +
+ @endif + @endforeach + +
+ +
+
+
diff --git a/resources/views/settings/env.blade.php b/resources/views/settings/env.blade.php new file mode 100644 index 0000000..4841c2b --- /dev/null +++ b/resources/views/settings/env.blade.php @@ -0,0 +1,3 @@ + + + diff --git a/routes/web.php b/routes/web.php index d363c2e..3ea56d3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -116,6 +116,10 @@ Route::middleware(['auth'])->group(function () { Route::get('/{type}/edit', fn (FlexibleAssetType $type) => view('asset-types.edit', ['type' => $type]))->name('edit'); }); + Route::get('/settings/env', fn () => view('settings.env')) + ->name('settings.env') + ->middleware('role:Administrateur'); + Route::get('/organizations/{organization}/assets', function (Organization $organization) { return view('assets.picker', [ 'organization' => $organization,