docs(spec): 0.9.0 accounts, sessions, auto-restart, SMTP, audit-retention
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>feat/v1-foundation
parent
cfa442d084
commit
5eb3deff0b
|
|
@ -0,0 +1,113 @@
|
|||
# 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\Index` keeps only: the identity header, the `#[Url] $tab` tab nav (now: Profil · Sicherheit
|
||||
· Benutzer · Sitzungen · E-Mail), and `@if($tab===…) <livewire:settings.x/> @endif`. The `tabs` array
|
||||
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.request` mapped to a host dir (or a
|
||||
dedicated `./run/restart.request`). `restartRequested(): bool` checks 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, runs
|
||||
`docker compose -f docker-compose.prod.yml restart` (or `up -d`) in the project dir and deletes the
|
||||
sentinel. Ship the unit files + script under `docker/restart-sentinel/` + wire `install.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: `… restart` auf dem
|
||||
Host" hint. Domain/TLS/mode saves call `requestRestart()` automatically per operator confirm.
|
||||
- Files: `app/Services/DeploymentService.php` (+ maybe `RestartService`), `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 `sessions` table migration (id, user_id, ip_address, user_agent, payload, last_activity).
|
||||
Set `SESSION_DRIVER=database` in `.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. Register `Illuminate\Session\Middleware\AuthenticateSession` so
|
||||
password changes invalidate other sessions.
|
||||
- **`Settings\Sessions` component:** lists the current user's active sessions from the `sessions` table
|
||||
(this device flagged; ip, user-agent summarized, last activity relative). Actions:
|
||||
- **"Andere Geräte abmelden"** (self): delete the user's other session rows + `Auth::logoutOtherDevices`
|
||||
pattern (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 their `remember_token`.
|
||||
- **(Admin) "Alle abmelden (global)"**: truncate `sessions` + rotate every user's `remember_token`
|
||||
(logs everyone out incl. self). Confirm via wire-elements/modal (R5), audited.
|
||||
- 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\Users` component:** lists all users (name, email, 2FA badge, last login if available);
|
||||
**create** (name, email unique, a generated temp password shown once + `must_change_password=true`
|
||||
so 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, maybe `app/Livewire/Modals/CreateUser.php` for the
|
||||
temp-password reveal, `lang/{de,en}/settings.php`. The `User` factory/model already suffices.
|
||||
|
||||
## §4. SMTP configuration
|
||||
- **`Settings\Email` component:** form for host, port, username, password (encrypted Setting),
|
||||
encryption (none/tls/ssl), from-address, from-name. Persist as `Setting` keys (`mail_*`); the password
|
||||
via an encrypted Setting. **Apply at runtime** by overriding `config(['mail.*' => …])` in
|
||||
`AppServiceProvider::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 the `log` mailer (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 command **`clusev:prune-audit`**
|
||||
deletes `audit_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 in `audit/index` or `Settings`, `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_token` so 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 `sessions` rows; 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_password` admin + 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.
|
||||
Loading…
Reference in New Issue