clusev/app/Console/Commands/Install.php

62 lines
2.1 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
/**
* Idempotent first-run bootstrap (called by install.sh, phase 7).
*
* Creates the initial admin with a one-time random password that is printed
* exactly once on stdout — never written to .env, a file, or the database in
* plaintext. Re-running once an admin exists is a hard no-op (INSTALL_LOCK
* idiom): no default credential, no "first-visitor-wins" claim window.
*/
class Install extends Command
{
protected $signature = "clusev:install
{--email= : Admin-Login + Let's-Encrypt-E-Mail}
{--name=Administrator : Anzeigename des Admins}";
protected $description = 'Idempotenter Erstlauf: legt den ersten Admin mit Einmal-Zufallspasswort an';
public function handle(): int
{
if (User::query()->exists()) {
$this->info('Clusev ist bereits installiert — kein Admin angelegt.');
return self::SUCCESS;
}
$host = parse_url((string) config('app.url'), PHP_URL_HOST) ?: 'localhost';
$email = (string) ($this->option('email') ?: "admin@{$host}");
if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
$this->error("Ungueltige Admin-E-Mail: {$email}");
return self::FAILURE;
}
$password = Str::password(20);
$user = User::create([
'name' => (string) ($this->option('name') ?: 'Administrator'),
'email' => $email,
'password' => $password, // 'hashed' cast hashes on save
'must_change_password' => true, // forces rotate + 2FA on first login
]);
// One-time password marker — the ONLY place the plaintext appears.
// install.sh greps these two lines for the closing banner; never stored.
$this->newLine();
$this->line('CLUSEV_ADMIN_EMAIL='.$user->email);
$this->line('CLUSEV_ADMIN_PASSWORD='.$password);
$this->newLine();
$this->info('Admin angelegt. Passwort erscheint nur jetzt und wird nicht gespeichert.');
return self::SUCCESS;
}
}