docs(webui): spec for admin system config (branding, login terms, security, backup)
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
# System Config — Admin System Parameters — Design
|
||||
|
||||
Date: 2026-08-11
|
||||
Status: Approved (approach A: WebUI-owned JSON store, user confirmed each section)
|
||||
|
||||
## Goal
|
||||
|
||||
Add a "System config" entry to the top-right Configuration dropdown, opening an
|
||||
admin-only dialog with four tabs — General (branding), Login terms, Security,
|
||||
Data backup. Parameters are a WebUI-owned override layer stored in
|
||||
`~/.evoscientist/system-config.json`; unset fields fall back to env/code
|
||||
defaults. No Python backend changes.
|
||||
|
||||
Decisions locked in during brainstorming:
|
||||
|
||||
- **Login terms:** display-only on the login page (no agreement checkbox).
|
||||
- **Security:** captcha policy, session & password policy, auth on/off toggle.
|
||||
Session expiry stays **fixed** (no sliding renewal — rejected 2026-08-08).
|
||||
- **Backup:** WebUI + backend data, manual export AND scheduled automatic
|
||||
backups to a server directory, with a history list (download/delete).
|
||||
- **Permission:** admin role only (UI hides entry; server double-checks).
|
||||
- **Approach A:** WebUI self-managed storage, no backend changes.
|
||||
|
||||
## 1. Storage — `src/lib/server/systemConfig.ts`
|
||||
|
||||
Follows the `userStore` pattern: read/write `~/.evoscientist/system-config.json`
|
||||
(via `webuiDataDir()`), atomic write (temp file + rename), in-memory cache with
|
||||
mtime invalidation.
|
||||
|
||||
```ts
|
||||
interface SystemConfig {
|
||||
branding: { wordmark: string; logoFile: string | null; faviconFile: string | null };
|
||||
loginTerms: { enabled: boolean; markdown: string };
|
||||
security: {
|
||||
authEnabled: boolean | null; // null = follow env
|
||||
sessionTtlHours: number | null;
|
||||
passwordMinLength: number | null;
|
||||
captchaEnabled: boolean | null;
|
||||
captchaThreshold: number | null; // failures before captcha
|
||||
};
|
||||
backup: {
|
||||
backendDataDir: string;
|
||||
schedule: { enabled: boolean; intervalHours: number; keepCount: number };
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
`null` = no override → fall back to env/code default. The config file is never
|
||||
required to exist; `getSystemConfig()` returns defaults when absent.
|
||||
|
||||
`effectiveSecurity()` (same module) is the single derivation point:
|
||||
auth.ts / captcha.ts / loginFailures.ts call it instead of reading the file
|
||||
directly. Unit-testable without touching auth internals.
|
||||
|
||||
## 2. API layer
|
||||
|
||||
| Route | Auth | Purpose |
|
||||
|---|---|---|
|
||||
| `GET/PUT /api/system/config` | admin | full config read/update |
|
||||
| `POST /api/system/branding` | admin | multipart logo/favicon upload → `~/.evoscientist/branding/{logo,favicon}.<ext>`; `DELETE` restores default |
|
||||
| `GET /api/system/config/public` | none | public subset: `{ wordmark, logoVersion, faviconVersion, loginTerms: { enabled, markdown } }` (version = file mtime, for cache-busting) |
|
||||
| `GET /api/system/branding/logo` `/favicon` | none | stream branding file, `Cache-Control: public, max-age=60`; 404 → caller falls back to bundled asset |
|
||||
| `POST /api/system/backup` | admin | run backup now |
|
||||
| `GET /api/system/backup` | admin | history list (name, size, mtime, origin manual/auto) |
|
||||
| `GET/DELETE /api/system/backup/[file]` | admin | download / delete an archive (filename validated: `^backup-\d{8}T\d{6}\.tar\.gz$`, no path traversal) |
|
||||
|
||||
Admin check = `requireActor(request)` + `roleForUsername(username) === "admin"`,
|
||||
else 403. Cross-origin guard (`isCrossOrigin`) on all routes, per the
|
||||
`/api/system/version` pattern.
|
||||
|
||||
## 3. General tab (branding)
|
||||
|
||||
- **Wordmark:** text input, non-empty, ≤30 chars. Applied to the header
|
||||
wordmark (`page.tsx`, currently hardcoded "AI4Scientist") and the login page
|
||||
title.
|
||||
- **Logo:** file picker (PNG/SVG/JPEG, ≤512KB), local preview before save.
|
||||
Header `<Image>` becomes `<img src="/api/system/branding/logo?v=<logoVersion>">`
|
||||
(dynamic src — next/image doesn't fit), falling back to
|
||||
`/evoscientist-logo.png` when no custom logo. "Restore default" button.
|
||||
- **Favicon:** same upload flow. Root layout injects
|
||||
`<link rel="icon" href="/api/system/branding/favicon?v=<faviconVersion>">`
|
||||
when a custom favicon exists (server component reads config at render).
|
||||
- **Consumers:** header and login page SWR-fetch `/api/system/config/public`
|
||||
(works unauthenticated — the login page needs it). Saving in the dialog
|
||||
revalidates the SWR key so branding updates immediately.
|
||||
|
||||
## 4. Login terms tab
|
||||
|
||||
- Enable toggle + Markdown textarea with rendered preview (`MarkdownContent`).
|
||||
- When enabled, the login page renders a scrollable terms card
|
||||
(`max-h`, `MarkdownContent`) below the title, sourced from
|
||||
`/api/system/config/public`. Display-only — no checkbox, no gating.
|
||||
|
||||
## 5. Security tab
|
||||
|
||||
Every control is an override with a "restore default" affordance:
|
||||
|
||||
- **Auth toggle:** tri-state — follow env (default) / force on / force off.
|
||||
`isAuthenticationEnabled()` checks the override first. Force-off gets a
|
||||
confirm dialog (danger: opens access). The existing env-disabled code paths
|
||||
(login page bypass, `authEnabled=false` UI) are reused as-is.
|
||||
- **Session TTL:** hours, 1–720. Applies to newly issued sessions only;
|
||||
existing sessions expire on their original fixed `expiresAt`.
|
||||
- **Password min length:** 6–64. Enforced in `userStore.setUserPassword`;
|
||||
existing passwords unaffected.
|
||||
- **Captcha:** tri-state (default/on/off) + failure threshold 1–10.
|
||||
`loginFailures`/`captcha` read `effectiveSecurity()`. Threshold ignored when
|
||||
captcha is off.
|
||||
|
||||
## 6. Data backup tab
|
||||
|
||||
- **Manual:** "Back up now" → `POST /api/system/backup` → server runs
|
||||
`tar -czf` over `webuiDataDir()` + configured `backendDataDir`, excluding
|
||||
`backups/` itself and `*.tmp`. Output:
|
||||
`~/.evoscientist/backups/backup-<YYYYMMDDTHHmmss>.tar.gz`.
|
||||
- **Scheduled:** enable + interval hours (1–168) + keep count (1–100).
|
||||
In-process `setInterval` singleton on `globalThis` (survives dev hot reload
|
||||
without duplicates), lazily started on first config read. Each run uses the
|
||||
same archive function as manual (origin recorded as `auto`), then prunes
|
||||
oldest archives beyond keep count. A mutex prevents overlapping runs.
|
||||
- **History:** table of archives (name, size, date, origin) with download and
|
||||
delete.
|
||||
- **Backend data dir:** path text input, validated on save (must exist and be
|
||||
readable). Used by both manual and scheduled backups.
|
||||
|
||||
Errors (missing dir, disk full, tar failure) go to `errorLogStore` and surface
|
||||
as toasts; scheduler exceptions are caught and logged, never crash the process.
|
||||
|
||||
## 7. Dialog & entry point
|
||||
|
||||
- Configuration dropdown gains "System config" (admin only — role from
|
||||
`/api/auth/me`; non-admin simply doesn't see it).
|
||||
- `SystemConfigDialog.tsx`: four tabs (General / Login terms / Security /
|
||||
Backup), one form state loaded via `GET /api/system/config`, single Save per
|
||||
tab (`PUT`), unsaved-changes guard on close.
|
||||
|
||||
## Error handling
|
||||
|
||||
- Config file missing/corrupt → defaults + errorLog entry; never 500 the page.
|
||||
- Upload rejects oversize/wrong type with 400 + toast.
|
||||
- Non-admin API calls → 403 (UI never offers the entry).
|
||||
- Backup failures → errorLog + toast; scheduler keeps running.
|
||||
|
||||
## Testing & verification
|
||||
|
||||
- Unit: `systemConfig.ts` (defaults, round-trip, atomic write, corrupt file),
|
||||
`effectiveSecurity()` precedence, backup archive (temp-dir fixture: tar
|
||||
contents, exclusions, keep-count pruning, mutex), route tests (403 for
|
||||
non-admin, 400 validation, filename traversal rejected).
|
||||
- Existing auth/captcha/loginFailures tests stay green.
|
||||
- Manual gate (dev :4716): admin changes branding → header + login page +
|
||||
favicon update; non-admin sees no menu item and gets 403; scheduled backup
|
||||
fires and appears in history; restore-default flows.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Backend-owned config storage (approach B — rejected).
|
||||
- Login-terms agreement checkbox / re-consent flow (display-only chosen).
|
||||
- Sliding/idle session renewal (rejected 2026-08-08).
|
||||
- Backup restore UI, remote/off-host backup targets.
|
||||
- Multi-admin audit trail of config changes.
|
||||
Reference in New Issue
Block a user