8.4 KiB
8.4 KiB
0.9.0 — Accounts, Sessions, Auto-Restart, SMTP, Audit-Retention — Design
Date: 2026-06-14 · Branch: feat/v1-foundation · Status: approved (build all, then release 0.9.0)
Operator-approved scope + decisions:
- Multi-User: additional accounts are all equal admins (no RBAC). The audit log already records the actor, so "who did what" is covered.
- Sessions: active-session list + per-user and global "log out everywhere", remember-token rotation. Requires switching the session store to the database (see §2).
- Auto-Restart: a restart sentinel — the app writes a request file, a host-side watcher restarts the stack. No Docker socket in the container.
- Settings extras: SMTP configuration + Audit-log retention (no theme, no API tokens now).
Foundational refactor (enables clean parallel work + scalable Settings):
§0. Settings → per-tab nested Livewire components
Settings\Index currently inlines Profil + Sicherheit in one big Blade with a $tab switch. Refactor
the tab bodies into nested full components so each area is isolated, testable, and independently
ownable:
Settings\Profile(name/email/password — lift the existing profile + password forms out of Index).Settings\Security(the 2FA/TOTP card + recovery modal trigger +<livewire:settings.webauthn-keys/>).- New
Settings\Users,Settings\Sessions,Settings\Email(§3/§2/§4).Settings\Indexkeeps only: the identity header, the#[Url] $tabtab nav (now: Profil · Sicherheit · Benutzer · Sitzungen · E-Mail), and@if($tab===…) <livewire:settings.x/> @endif. Thetabsarray gains the new keys + icons (user-plus, a sessions icon,mail). Each nested component owns its own state/validation/audit — no more one-giant-class.
§1. Auto-restart sentinel
Goal: replace the manual docker compose -f docker-compose.prod.yml restart notice with a one-click,
self-applying restart — without giving the container the Docker socket.
- App:
DeploymentService::requestRestart()writes a sentinel file to a path on a bind-mounted shared volume the host watches, e.g.storage/app/restart.requestmapped to a host dir (or a dedicated./run/restart.request).restartRequested(): boolchecks it exists. - Host watcher (installed once by
install.sh/documented): a tiny script + systemd path unit (clusev-restart.path→clusev-restart.service) that, when the sentinel appears, runsdocker compose -f docker-compose.prod.yml restart(orup -d) in the project dir and deletes the sentinel. Ship the unit files + script underdocker/restart-sentinel/+ wireinstall.sh. - UI: the "Neustart erforderlich" block loses the raw command; instead a button "Jetzt neu
starten" →
requestRestart()→ shows "Neustart wird ausgeführt …". If the host watcher isn't installed (sentinel lingers > N s), fall back to a muted "falls nichts passiert:… restartauf dem Host" hint. Domain/TLS/mode saves callrequestRestart()automatically per operator confirm. - Files:
app/Services/DeploymentService.php(+ maybeRestartService),docker/restart-sentinel/*,install.sh,docker-compose.prod.yml(the shared sentinel volume), the System view, lang/system.
§2. Database sessions + session management
- Switch the session store to the database so sessions are per-row + enumerable. Add the standard
Laravel
sessionstable migration (id, user_id, ip_address, user_agent, payload, last_activity). SetSESSION_DRIVER=databasein.env/.env.example/compose. One-time effect: existing Redis sessions are dropped → everyone re-logs-in once (documented in the changelog + the restart notice). Redis stays the cache/queue store. RegisterIlluminate\Session\Middleware\AuthenticateSessionso password changes invalidate other sessions. Settings\Sessionscomponent: lists the current user's active sessions from thesessionstable (this device flagged; ip, user-agent summarized, last activity relative). Actions:- "Andere Geräte abmelden" (self): delete the user's other session rows +
Auth::logoutOtherDevicespattern (rotate so other sessions die), keep the current one. - (Admin) "Diesen Account überall abmelden" per listed user (in
Settings\Users): delete all that user's session rows + rotate theirremember_token. - (Admin) "Alle abmelden (global)": truncate
sessions+ rotate every user'sremember_token(logs everyone out incl. self). Confirm via wire-elements/modal (R5), audited.
- "Andere Geräte abmelden" (self): delete the user's other session rows +
- Files: migration,
app/Livewire/Settings/Sessions.php+ view,app/Services/SessionService.php(the delete/rotate logic, testable),bootstrap/app.php(AuthenticateSession),.env*/compose,lang/{de,en}/settings.php.
§3. Multi-user accounts (all equal admins)
Settings\Userscomponent: lists all users (name, email, 2FA badge, last login if available); create (name, email unique, a generated temp password shown once +must_change_password=trueso the new admin rotates on first login); remove (R5 modal; cannot remove yourself or the last remaining user). Each action audited (user.create/user.delete, actor = current). Per-row "überall abmelden" (→ §2). All created users are full admins (no role column needed now).- New-user password: generate a strong temp password, show once in a modal (like recovery codes), do NOT email it unless SMTP configured (then offer "Zugangsdaten per E-Mail senden").
- Files:
app/Livewire/Settings/Users.php+ view, maybeapp/Livewire/Modals/CreateUser.phpfor the temp-password reveal,lang/{de,en}/settings.php. TheUserfactory/model already suffices.
§4. SMTP configuration
Settings\Emailcomponent: form for host, port, username, password (encrypted Setting), encryption (none/tls/ssl), from-address, from-name. Persist asSettingkeys (mail_*); the password via an encrypted Setting. Apply at runtime by overridingconfig(['mail.*' => …])inAppServiceProvider::boot()when the settings exist (mirrors the existing reverb override). A "Testmail senden" button sends a test to the current user + surfaces success/failure. When SMTP is unset, the app stays on thelogmailer (current behaviour) and forgot-password keeps the 2FA/CLI path.- Files:
app/Livewire/Settings/Email.php+ view,app/Providers/AppServiceProvider.php(runtime mail override),lang/{de,en}/settings.php. Password stored encrypted; never rendered back (show a "set" placeholder).
§5. Audit-log retention
- Setting
audit_retention_days(0/empty = keep forever; default keep-forever). A control in the Audit page header (or a small Settings block) to set it. A scheduled commandclusev:prune-auditdeletesaudit_events older than N days; register it on the scheduler (daily). Audited that a prune ran (count). Files:app/Console/Commands/PruneAudit.php, the scheduler (routes/console.php or Kernel), a control inaudit/indexorSettings,lang.
Security invariants
- New accounts are created by an authenticated admin only; temp password is
must_change_password. - Global logout / per-user logout rotate
remember_tokenso stolen cookies die. - SMTP password stored encrypted (APP_KEY), never rendered, never logged.
- The restart sentinel grants NO new container privilege (no Docker socket); the host watcher is the only thing that can restart, scoped to the project dir.
- Cannot delete yourself or the last admin (no lock-out).
- All destructive actions: wire-elements/modal confirm (R5) + AuditEvent.
Testing (per feature, TDD)
- Settings refactor: each nested component renders + its tab loads; Index hosts them by
$tab. - Sentinel:
requestRestart()writes the file,restartRequested()reads it; UI button calls it. - Sessions: list reflects
sessionsrows; per-user logout deletes that user's rows + rotates token; global logout truncates + rotates all; self "other devices" keeps current. - Users: create makes a
must_change_passwordadmin + audits; cannot delete self/last; remove audits. - Email: settings persist (password encrypted), runtime override applies, test-send path.
- Retention: prune deletes only old rows, keeps recent; command is scheduled.
- Full suite green, Pint, Codex clean, R12 (DE+EN, 3 breakpoints), then bump + tag v0.9.0.
Out of scope (later)
- RBAC/roles, API tokens, theme preference, per-event audit export, email templates beyond the test.