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
b3587b776f
|
|
@ -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 —
|
||||
|
|
|
|||
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
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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' ""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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