chore(release): v0.9.5

- Initial admin now uses the standard default password "clusev" (operator decision) with
  must_change_password forcing a new password on first login; banner/MOTD/README tell the
  operator to change it immediately. Idempotent no-op when an admin already exists.
- Richer host MOTD (CLUSEV wordmark, version, live stack status, login + management hints).
- README: install block now installs git (minimal Debian/Ubuntu lack it) and lists it as a
  prerequisite — found by a full fresh-VM install test (install + idempotent re-run verified).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat/v1-foundation v0.9.5
boban 2026-06-19 16:43:04 +02:00
parent b9b4b333e1
commit 9215a5b908
7 changed files with 73 additions and 20 deletions

View File

@ -11,7 +11,15 @@ getaggte Releases (Kanal `stable`, optional `beta`) — niemals Entwicklungs-Bui
## [Unreleased]
_Keine offenen Änderungen — der nächste Stand wird hier gesammelt und als `vX.Y.Z` getaggt._
## [0.9.5] - 2026-06-17
### Geändert
- **Erst-Admin nutzt jetzt das Standard-Passwort `clusev`** (statt eines Einmal-Zufallspassworts) —
vorhersehbarer Login. **`must_change_password` erzwingt den Wechsel beim ersten Login**, und
Banner/MOTD/README weisen ausdrücklich darauf hin, es **sofort** zu ändern (ein bekanntes Default
ist nur durch den erzwungenen Wechsel vertretbar). Idempotenz unverändert (kein Admin bei Re-Run).
- **Host-MOTD deutlich aufgewertet.** Statt zwei Zeilen jetzt eine orange CLUSEV-Wortmarke, die
Version, ein **Live-Stack-Status** („X/Y Dienste aktiv", grün/rot — bei jedem Login frisch aus
`docker compose ps`), die Dashboard-URL, eine Login-Hinweiszeile (woher die Zugangsdaten kommen —

View File

@ -49,15 +49,18 @@ The installer is idempotent (safe to re-run) and, in one pass:
checks whether DNS already points here: if so a certificate is obtained automatically; if not, it
warns you and lets you take the domain anyway (HTTP until DNS is correct) or continue on the IP.
4. Generates all secrets (once — never regenerated on re-run), builds the image, starts the stack,
runs the migrations, and creates the first administrator with a one-time random password.
5. Installs a themed host login banner (MOTD) showing the dashboard address.
runs the migrations, and creates the first administrator with the **default password `clusev`**
(you are forced to change it on first login).
5. Installs a themed host login banner (MOTD) showing the dashboard address and live stack status.
When it finishes, the terminal prints a single summary: the **dashboard URL**, the **admin login**
(e-mail + one-time password), and the **`clusev` host user** + its password. These passwords are shown
**only once** — note them down.
(your e-mail + the default password **`clusev`**), and the **`clusev` host user** + its random
password (shown **only once** — note it down).
On first login the panel forces you to set your own password. 2FA is **optional but recommended**
enable TOTP and/or a security key from **Settings → Security** whenever you like.
Log in with your admin e-mail and the password **`clusev`** — the panel then **forces you to set a
new password immediately**. Do this right away: `clusev` is a known default, so the account is only
safe once you've changed it. 2FA is **optional but recommended** — enable TOTP and/or a security key
from **Settings → Security** whenever you like.
## Access, domain & TLS

View File

@ -4,23 +4,27 @@ 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.
* Creates the initial admin with the standard default password (operator decision: a
* known, memorable default so login is never blocked by a missed one-time secret).
* `must_change_password` is set, so the onboarding gate FORCES a new password on the
* first login the default is only valid until then. Re-running once an admin exists is
* a hard no-op. NOTE: a known default credential is only acceptable because of the forced
* rotation; the operator must change it immediately (banner / MOTD / README say so).
*/
class Install extends Command
{
/** Standard default admin password — forced to change on first login (must_change_password). */
private const DEFAULT_PASSWORD = 'clusev';
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';
protected $description = 'Idempotenter Erstlauf: legt den ersten Admin mit Standard-Passwort an (Zwangswechsel beim ersten Login)';
public function handle(): int
{
@ -39,22 +43,22 @@ class Install extends Command
return self::FAILURE;
}
$password = Str::password(20);
$password = self::DEFAULT_PASSWORD;
$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
'must_change_password' => true, // forces a new password 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.
// install.sh greps these two lines for the closing banner. The password is the
// standard default and must be changed on first login (must_change_password).
$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.');
$this->info('Admin angelegt. Standard-Passwort — beim ersten Login sofort aendern.');
return self::SUCCESS;
}

View File

@ -3,7 +3,7 @@
return [
// First tagged release is v0.1.0 (semantic, not -dev). The live build hash
// is resolved from .git at runtime (see App\Livewire\Versions\Index).
'version' => '0.9.4',
'version' => '0.9.5',
// Default user channel. Only 'stable' and 'beta' are ever offered to users.
'channel' => 'stable',

View File

@ -40,7 +40,7 @@ printf ' %s%s▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀ ▀ %s %sFlee
printf ' %s──────────────────────────────────────────────────────────%s\n' "$D" "$R"
printf ' %sDashboard%s %s%s%s\n' "$D" "$R" "$O" "$URL" "$R"
printf ' %sStack%s %s\n' "$D" "$R" "$(stack_status)"
printf ' %sLogin%s %sAdmin-Zugang stand einmalig im Install-Banner · zuruecksetzen: clusev:reset-admin%s\n' "$D" "$R" "$D" "$R"
printf ' %sLogin%s %sStandard-Passwort: clusev — beim ersten Login aendern · Reset: clusev:reset-admin%s\n' "$D" "$R" "$D" "$R"
printf ' %sVerwalten%s %sdocker compose -f docker-compose.prod.yml ps | logs -f | restart%s\n' "$D" "$R" "$D" "$R"
printf ' %s──────────────────────────────────────────────────────────%s\n' "$D" "$R"
printf '%s\n' ""

View File

@ -320,7 +320,7 @@ info "+----------------------------------------------------------+"
info " Dashboard: ${ACCESS_URL}"
info " Admin-Login: ${ADMIN_MAIL:-<bestehend>}"
if [ -n "$ADMIN_PW" ]; then
info " Admin-Passwort: ${ADMIN_PW}"
info " Admin-Passwort: ${ADMIN_PW} (Standard — beim ersten Login sofort aendern!)"
else
info " Admin-Passwort: <bereits installiert — kein neuer Admin angelegt>"
fi

View File

@ -0,0 +1,38 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Tests\TestCase;
/**
* clusev:install (install.sh phase 8) first-admin bootstrap. The operator chose a known
* default password ('clusev') for predictable login; must_change_password forces a new one
* on first login, which is what keeps the default acceptable.
*/
class InstallCommandTest extends TestCase
{
use RefreshDatabase;
public function test_creates_admin_with_default_password_and_forced_change(): void
{
$this->artisan('clusev:install', ['--email' => 'admin@example.test'])->assertSuccessful();
$user = User::where('email', 'admin@example.test')->first();
$this->assertNotNull($user);
$this->assertTrue(Hash::check('clusev', $user->password), 'admin password must be the default "clusev"');
$this->assertTrue((bool) $user->must_change_password, 'first login must be forced to change it');
}
public function test_is_a_noop_when_an_admin_already_exists(): void
{
User::factory()->create();
$this->artisan('clusev:install', ['--email' => 'second@example.test'])->assertSuccessful();
$this->assertSame(1, User::count());
$this->assertNull(User::where('email', 'second@example.test')->first());
}
}