Ajoute un menu admin pour éditer les variables .env de l'app

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 <noreply@anthropic.com>
This commit is contained in:
mrKamoo
2026-08-20 08:58:29 +02:00
co-authored by Claude Sonnet 5
parent 2f576ca286
commit da589d0627
5 changed files with 305 additions and 0 deletions
@@ -0,0 +1,134 @@
<?php
use App\Services\EnvFileManager;
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();
}
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'];
}
}
}
};
?>
<div class="max-w-3xl">
<h1 class="mb-2 text-2xl font-semibold text-slate-900">Configuration (.env)</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.
</p>
@if (session('status'))
<div class="mb-4 rounded border border-emerald-200 bg-emerald-50 px-4 py-2 text-sm text-emerald-700">
{{ session('status') }}
</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>
@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>
</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
</div>
@endif
@endforeach
<div class="flex items-center gap-3 pt-5">
<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">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
<span wire:loading.remove wire:target="save">Enregistrer</span>
<span wire:loading wire:target="save">Enregistrement…</span>
</button>
</div>
</form>
</div>