Remplace l'édition directe du .env par des réglages en base de données

L'édition de fichier nécessitait que le process web ait un accès en
écriture sur .env — problématique en prod (permissions, plusieurs
instances). À la place :

- Table `settings` (clé/valeur) + SettingsRegistry, une liste blanche
  explicite des réglages modifiables (mail, OnlyOffice, GLPI, nom/URL de
  l'app) mappés vers leur chemin de config Laravel.
- Volontairement exclu : tout ce qui touche à l'infrastructure (APP_KEY,
  DB_*, SESSION_DRIVER, CACHE_STORE, QUEUE_CONNECTION...) — ces valeurs
  sont nécessaires avant même qu'une connexion BDD existe, donc une
  surcharge en base ne serait pas fiable et reste réservée au .env.
- AppServiceProvider applique les surcharges au boot (cache 5 min,
  invalidé immédiatement à l'enregistrement) ; le reste du code continue
  de lire config() normalement, sans changement ailleurs.
- Tant qu'aucun réglage n'est enregistré, le comportement reste identique
  au .env actuel — transition sans rupture.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
mrKamoo
2026-08-20 09:21:15 +02:00
co-authored by Claude Sonnet 5
parent da589d0627
commit c35833ed0b
8 changed files with 211 additions and 226 deletions
@@ -1,19 +1,18 @@
<?php
use App\Services\EnvFileManager;
use App\Models\Setting;
use App\Services\SettingsRegistry;
use Illuminate\Support\Facades\Cache;
use Livewire\Component;
new class extends Component
{
/** @var array<int, array<string, mixed>> */
public array $lines = [];
/** @var array<string, string> */
public array $values = [];
public function mount(): void
{
$this->loadLines();
$this->loadValues();
}
public function save(): void
@@ -26,38 +25,51 @@ new class extends Component
}
}
try {
app(EnvFileManager::class)->update($this->values);
} catch (\Throwable $e) {
session()->flash('error', $e->getMessage());
foreach (SettingsRegistry::configMap() as $envKey => $path) {
$value = trim($this->values[$envKey] ?? '');
Setting::updateOrCreate(['key' => $envKey], ['value' => $value === '' ? null : $value]);
}
Cache::forget(SettingsRegistry::CACHE_KEY);
session()->flash('status', 'Configuration enregistrée. Les nouvelles valeurs sont actives immédiatement.');
$this->loadValues();
}
public function resetToDefault(string $envKey): void
{
if (! array_key_exists($envKey, SettingsRegistry::configMap())) {
return;
}
session()->flash('status', 'Configuration enregistrée. Les nouvelles valeurs seront actives dès la prochaine requête.');
Setting::where('key', $envKey)->delete();
Cache::forget(SettingsRegistry::CACHE_KEY);
$this->loadLines();
session()->flash('status', "Valeur par défaut restaurée pour {$envKey}.");
$this->loadValues();
}
protected function loadLines(): void
protected function loadValues(): void
{
$this->lines = app(EnvFileManager::class)->read();
$this->values = [];
$stored = Setting::query()->pluck('value', 'key');
foreach ($this->lines as $line) {
if ($line['type'] === 'var' && ! $line['protected']) {
$this->values[$line['key']] = $line['value'];
}
foreach (SettingsRegistry::configMap() as $envKey => $path) {
$this->values[$envKey] = $stored[$envKey] ?? (string) (config($path) ?? '');
}
}
};
?>
<div class="max-w-3xl">
<h1 class="mb-2 text-2xl font-semibold text-slate-900">Configuration (.env)</h1>
<h1 class="mb-2 text-2xl font-semibold text-slate-900">Configuration</h1>
<p class="mb-6 text-sm text-slate-500">
Modifie directement les variables d'environnement de l'application. Les changements
prennent effet à la prochaine requête — aucun redémarrage n'est nécessaire.
Ces réglages sont stockés en base de données et prennent effet immédiatement, sans
redémarrage. Les paramètres d'infrastructure (base de données, clé de chiffrement,
pilote de session…) ne sont pas modifiables ici — ils doivent être changés directement
dans le fichier <code class="rounded bg-slate-100 px-1 py-0.5">.env</code> du serveur.
</p>
@if (session('status'))
@@ -66,60 +78,52 @@ new class extends Component
</div>
@endif
@if (session('error'))
<div class="mb-4 rounded border border-red-200 bg-red-50 px-4 py-2 text-sm text-red-700">
{{ session('error') }}
</div>
@endif
@error('values')
<div class="mb-4 rounded border border-red-200 bg-red-50 px-4 py-2 text-sm text-red-700">
{{ $message }}
</div>
@enderror
<form wire:submit="save" class="space-y-1 rounded border border-slate-200 bg-white p-6">
@foreach ($lines as $line)
@if ($line['type'] === 'comment')
@if ($line['text'] !== '')
<p class="mb-1 mt-5 text-xs font-semibold uppercase tracking-wide text-slate-400 first:mt-0">
{{ $line['text'] }}
</p>
@endif
@elseif ($line['type'] === 'var')
<div class="flex items-center gap-3 py-1.5">
<label for="env-{{ $line['key'] }}" class="w-56 shrink-0 truncate font-mono text-xs text-slate-600" title="{{ $line['key'] }}">
{{ $line['key'] }}
</label>
<form wire:submit="save" class="space-y-8 rounded border border-slate-200 bg-white p-6">
@foreach (\App\Services\SettingsRegistry::SETTINGS as $section => $keys)
<div>
<p class="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-400">{{ $section }}</p>
@if ($line['protected'])
<div class="flex flex-1 items-center gap-2">
<input type="text" value="{{ $line['sensitive'] ? '••••••••••••' : $line['value'] }}" disabled
class="w-full rounded border-slate-200 bg-slate-50 font-mono text-sm text-slate-400">
<span class="shrink-0 rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-500" title="Non modifiable ici — nécessite un accès direct au serveur">
🔒 lecture seule
</span>
</div>
@elseif ($line['sensitive'])
<div x-data="{ show: false }" class="flex flex-1 items-center gap-2">
<input :type="show ? 'text' : 'password'" id="env-{{ $line['key'] }}"
wire:model="values.{{ $line['key'] }}" autocomplete="off"
class="w-full rounded border-slate-300 font-mono text-sm shadow-sm focus:border-slate-500 focus:ring-slate-500">
<button type="button" x-on:click="show = !show"
class="shrink-0 text-xs text-slate-500 hover:text-slate-900">
<span x-text="show ? 'Masquer' : 'Révéler'"></span>
<div class="space-y-1.5">
@foreach ($keys as $envKey => $meta)
<div class="flex items-center gap-3 py-1">
<label for="setting-{{ $envKey }}" class="w-48 shrink-0 text-sm text-slate-700">
{{ $meta['label'] }}
</label>
@if (\App\Services\SettingsRegistry::isSensitive($envKey))
<div x-data="{ show: false }" class="flex flex-1 items-center gap-2">
<input :type="show ? 'text' : 'password'" id="setting-{{ $envKey }}"
wire:model="values.{{ $envKey }}" autocomplete="off"
class="w-full rounded border-slate-300 font-mono text-sm shadow-sm focus:border-slate-500 focus:ring-slate-500">
<button type="button" x-on:click="show = !show"
class="shrink-0 text-xs text-slate-500 hover:text-slate-900">
<span x-text="show ? 'Masquer' : 'Révéler'"></span>
</button>
</div>
@else
<input type="text" id="setting-{{ $envKey }}"
wire:model="values.{{ $envKey }}"
class="flex-1 rounded border-slate-300 font-mono text-sm shadow-sm focus:border-slate-500 focus:ring-slate-500">
@endif
<button type="button" wire:click="resetToDefault('{{ $envKey }}')"
wire:confirm="Revenir à la valeur par défaut de {{ $envKey }} (celle du .env) ?"
class="shrink-0 text-xs text-slate-400 hover:text-slate-700">
Réinitialiser
</button>
</div>
@else
<input type="text" id="env-{{ $line['key'] }}"
wire:model="values.{{ $line['key'] }}"
class="flex-1 rounded border-slate-300 font-mono text-sm shadow-sm focus:border-slate-500 focus:ring-slate-500">
@endif
@endforeach
</div>
@endif
</div>
@endforeach
<div class="flex items-center gap-3 pt-5">
<div class="flex items-center gap-3 border-t border-slate-100 pt-6">
<button type="submit" wire:loading.attr="disabled" wire:target="save"
class="inline-flex items-center gap-2 rounded bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-700 disabled:cursor-not-allowed disabled:opacity-60">
<svg wire:loading wire:target="save" class="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">