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
parent
b9b4b333e1
commit
9215a5b908
|
|
@ -11,7 +11,15 @@ getaggte Releases (Kanal `stable`, optional `beta`) — niemals Entwicklungs-Bui
|
||||||
|
|
||||||
## [Unreleased]
|
## [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
|
### 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
|
- **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
|
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 —
|
`docker compose ps`), die Dashboard-URL, eine Login-Hinweiszeile (woher die Zugangsdaten kommen —
|
||||||
|
|
|
||||||
15
README.md
15
README.md
|
|
@ -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
|
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.
|
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,
|
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.
|
runs the migrations, and creates the first administrator with the **default password `clusev`**
|
||||||
5. Installs a themed host login banner (MOTD) showing the dashboard address.
|
(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**
|
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
|
(your e-mail + the default password **`clusev`**), and the **`clusev` host user** + its random
|
||||||
**only once** — note them down.
|
password (shown **only once** — note it down).
|
||||||
|
|
||||||
On first login the panel forces you to set your own password. 2FA is **optional but recommended** —
|
Log in with your admin e-mail and the password **`clusev`** — the panel then **forces you to set a
|
||||||
enable TOTP and/or a security key from **Settings → Security** whenever you like.
|
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
|
## Access, domain & TLS
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,27 @@ namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Str;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Idempotent first-run bootstrap (called by install.sh, phase 7).
|
* Idempotent first-run bootstrap (called by install.sh, phase 7).
|
||||||
*
|
*
|
||||||
* Creates the initial admin with a one-time random password that is printed
|
* Creates the initial admin with the standard default password (operator decision: a
|
||||||
* exactly once on stdout — never written to .env, a file, or the database in
|
* known, memorable default so login is never blocked by a missed one-time secret).
|
||||||
* plaintext. Re-running once an admin exists is a hard no-op (INSTALL_LOCK
|
* `must_change_password` is set, so the onboarding gate FORCES a new password on the
|
||||||
* idiom): no default credential, no "first-visitor-wins" claim window.
|
* 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
|
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
|
protected $signature = "clusev:install
|
||||||
{--email= : Admin-Login + Let's-Encrypt-E-Mail}
|
{--email= : Admin-Login + Let's-Encrypt-E-Mail}
|
||||||
{--name=Administrator : Anzeigename des Admins}";
|
{--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
|
public function handle(): int
|
||||||
{
|
{
|
||||||
|
|
@ -39,22 +43,22 @@ class Install extends Command
|
||||||
return self::FAILURE;
|
return self::FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
$password = Str::password(20);
|
$password = self::DEFAULT_PASSWORD;
|
||||||
|
|
||||||
$user = User::create([
|
$user = User::create([
|
||||||
'name' => (string) ($this->option('name') ?: 'Administrator'),
|
'name' => (string) ($this->option('name') ?: 'Administrator'),
|
||||||
'email' => $email,
|
'email' => $email,
|
||||||
'password' => $password, // 'hashed' cast hashes on save
|
'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. The password is the
|
||||||
// install.sh greps these two lines for the closing banner; never stored.
|
// standard default and must be changed on first login (must_change_password).
|
||||||
$this->newLine();
|
$this->newLine();
|
||||||
$this->line('CLUSEV_ADMIN_EMAIL='.$user->email);
|
$this->line('CLUSEV_ADMIN_EMAIL='.$user->email);
|
||||||
$this->line('CLUSEV_ADMIN_PASSWORD='.$password);
|
$this->line('CLUSEV_ADMIN_PASSWORD='.$password);
|
||||||
$this->newLine();
|
$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;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
return [
|
return [
|
||||||
// First tagged release is v0.1.0 (semantic, not -dev). The live build hash
|
// 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).
|
// 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.
|
// Default user channel. Only 'stable' and 'beta' are ever offered to users.
|
||||||
'channel' => 'stable',
|
'channel' => 'stable',
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ printf ' %s%s▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀ ▀ %s %sFlee
|
||||||
printf ' %s──────────────────────────────────────────────────────────%s\n' "$D" "$R"
|
printf ' %s──────────────────────────────────────────────────────────%s\n' "$D" "$R"
|
||||||
printf ' %sDashboard%s %s%s%s\n' "$D" "$R" "$O" "$URL" "$R"
|
printf ' %sDashboard%s %s%s%s\n' "$D" "$R" "$O" "$URL" "$R"
|
||||||
printf ' %sStack%s %s\n' "$D" "$R" "$(stack_status)"
|
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 ' %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──────────────────────────────────────────────────────────%s\n' "$D" "$R"
|
||||||
printf '%s\n' ""
|
printf '%s\n' ""
|
||||||
|
|
|
||||||
|
|
@ -320,7 +320,7 @@ info "+----------------------------------------------------------+"
|
||||||
info " Dashboard: ${ACCESS_URL}"
|
info " Dashboard: ${ACCESS_URL}"
|
||||||
info " Admin-Login: ${ADMIN_MAIL:-<bestehend>}"
|
info " Admin-Login: ${ADMIN_MAIL:-<bestehend>}"
|
||||||
if [ -n "$ADMIN_PW" ]; then
|
if [ -n "$ADMIN_PW" ]; then
|
||||||
info " Admin-Passwort: ${ADMIN_PW}"
|
info " Admin-Passwort: ${ADMIN_PW} (Standard — beim ersten Login sofort aendern!)"
|
||||||
else
|
else
|
||||||
info " Admin-Passwort: <bereits installiert — kein neuer Admin angelegt>"
|
info " Admin-Passwort: <bereits installiert — kein neuer Admin angelegt>"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue