# TASK: Phase 1 — Authentication, Persistence & Settings Page **Version target:** 0.7.0 **Repo:** `deploy-felhom-compose` (controller) ## Overview Three workstreams in this phase: 1. Implement login/logout authentication for the controller dashboard 2. Persist DB validation results across container restarts 3. Add "Beállítások" (Settings) page with config display and password change All user-editable state is stored in a single `settings.json` file at: `/opt/docker/felhom-controller/data/settings.json` This path is already bind-mounted into the container (the `data/` dir holds `restic-password` etc.). The controller.yaml config remains the source of truth for operator-provisioned values; `settings.json` holds customer-modifiable overrides. --- ## 1. settings.json — Shared Persistence Layer ### 1.1 Create `internal/settings/settings.go` ``` File: controller/internal/settings/settings.go ``` Define the settings struct and load/save logic: ```go type Settings struct { mu sync.RWMutex `json:"-"` // Auth PasswordHash string `json:"password_hash,omitempty"` // bcrypt hash, overrides controller.yaml // Notification preferences (Phase 2 — define struct now, leave empty) Notifications *NotificationPrefs `json:"notifications,omitempty"` // Cached state DBValidations map[string]DBValidationCache `json:"db_validations,omitempty"` } type NotificationPrefs struct { // placeholder for Phase 2 } type DBValidationCache struct { ValidatedAt string `json:"validated_at"` // RFC3339 TableCount int `json:"table_count"` HasHeader bool `json:"has_header"` Error string `json:"error,omitempty"` } ``` **Behavior:** - `Load(path string) (*Settings, error)` — reads file, returns empty Settings{} if file doesn't exist (not an error) - `Save() error` — atomic write (write to `.tmp`, rename). Path stored internally from Load. - Use `sync.RWMutex` for concurrent access (backup goroutine writes validations, web reads them) - Log on every save: `[DEBUG] Settings saved to ` **File location:** The controller's docker-compose.yml already mounts `./data:/opt/docker/felhom-controller/data`. The settings file path should be passed to the Settings manager on init. **Testing:** On startup, if file doesn't exist, log `[INFO] No settings.json found, using defaults`. Do NOT create the file until something actually needs saving. --- ## 2. Authentication (Login / Logout) ### 2.1 Password resolution The effective password hash is determined with this priority: 1. `settings.json` → `password_hash` (customer changed it) 2. `controller.yaml` → `web.password_hash` (operator provisioned) 3. Empty string → no auth required (current testing behavior) On startup, log which source is active: ``` [INFO] Auth: using password from settings.json [INFO] Auth: using password from controller.yaml [INFO] Auth: no password configured — dashboard is open ``` ### 2.2 Session management Use a **signed session cookie** approach (not just `"authenticated"` string): - Generate a random 32-byte session secret on startup (store in memory only, not persisted — restarts invalidate sessions, which is fine) - Cookie name: `felhom_session` - Cookie value: `.` — HMAC of expiry timestamp with session secret - `HttpOnly: true`, `SameSite: Strict`, `Secure: false` (local network, self-signed certs) - `MaxAge: 7 days` (configurable later) - `Path: /` ### 2.3 Routes Add to `internal/web/server.go` routing: | Method | Path | Auth? | Handler | |--------|-------------|-------|--------------------| | GET | `/login` | No | Show login form | | POST | `/login` | No | Validate + set cookie + redirect to `/` | | GET/POST | `/logout` | No | Clear cookie + redirect to `/login` | **Auth middleware** (wrap all routes except `/login`, `/logout`, `/style.css`, `/assets/*`, `/api/*`): - Check `felhom_session` cookie → validate HMAC + check expiry - If invalid/missing → redirect to `/login` (for browser) or return 401 (for API) - If no password configured → pass through (no auth) ### 2.4 Login page template ``` File: controller/internal/web/templates/login.html ``` - Standalone page (no sidebar), same dark theme as rest of dashboard - Felhom logo centered at top - Single password field + "Bejelentkezés" button - On error: "Hibás jelszó" message (red, below form) - Customer name shown below logo (from config) - No username field — single-user system ### 2.5 Logout - `GET /logout` or `POST /logout` → set cookie MaxAge=-1 → redirect to `/login` - Add logout link to sidebar bottom (near version display): ``` | Kijelentkezés ``` Only show "Kijelentkezés" if auth is enabled. ### 2.6 Edge cases - If password_hash is empty in both sources → no auth, no login page, no logout link - If user is on a page and session expires → next request redirects to `/login`, after login redirect back to original page (use `?next=/backups` query param) - Cookie cleared on logout must work even if server secret rotated (clear by MaxAge=-1) --- ## 3. DB Validation Persistence ### 3.1 Problem After container restart, the backup page "Érvényesítés" column shows "–" for all databases until the next backup cycle runs validation. The v0.6.2 cross-check helps once RefreshCache runs, but the initial load after restart still shows no data. ### 3.2 Solution **On validation completion** (`internal/backup/dbdump.go` → `ValidateDump`): - After successful validation, save result to `settings.json` via settings package: ```go settings.SetDBValidation("immich-postgres.sql", DBValidationCache{ ValidatedAt: time.Now().Format(time.RFC3339), TableCount: 60, HasHeader: true, }) // SetDBValidation acquires write lock, updates map, calls Save() ``` **On startup / RefreshCache** (`internal/backup/backup.go`): - Load cached validations from `settings.json` - For each dump file that exists on disk AND has a cached validation: - Use the cached validation data - Set `DumpValidation.Valid = true/false` based on cached result - Set `DumpValidation.Message` to include cached info: e.g., `"60 tábla (utolsó: 08:04)"` - The next actual validation run overwrites the cache with fresh data **Important:** The cache is keyed by dump filename (e.g., `immich-postgres.sql`). If a dump file no longer exists, its cached validation is ignored (stale data cleanup). ### 3.3 Template update No template changes needed — the existing 4-branch guard from v0.6.2 already handles showing validation status correctly. The only difference is now the data will be populated from cache on startup instead of being zero-valued. --- ## 4. "Beállítások" (Settings) Page ### 4.1 Sidebar menu item Add "Beállítások" as the last menu item, visually separated from the main navigation — placed at the bottom of the sidebar, just above the version/logout section. Use a gear/cog icon (⚙ or SVG). Sidebar order: ``` Vezérlőpult Alkalmazások Biztonsági mentés Rendszermonitor ─── (spacer / flex-grow) ─── ⚙ Beállítások ← new, pinned to bottom v0.7.0 | Kijelentkezés ``` ### 4.2 Route | Method | Path | Auth? | Handler | |--------|-------------|-------|---------------------| | GET | `/settings` | Yes | Show settings page | | POST | `/settings/password` | Yes | Change password | ### 4.3 Settings page layout The page has two sections: #### Section A: "Rendszer konfiguráció" (System Configuration) — Read-only Display key values from `controller.yaml` in a clean info-grid. These are read-only — the customer can see what's configured but can't change it here. | Label (Hungarian) | Source in controller.yaml | Display format | |--------------------------|----------------------------------|----------------| | Ügyfél azonosító | `customer.id` | `demo-felhom` | | Ügyfél neve | `customer.name` | `Demo Ügyfél` | | Domain | `customer.domain` | `demo-felhom.eu` | | Alkalmazás sablon forrás | `git.repo_url` | URL (truncated) | | Sablon szinkronizálás | `git.sync_interval` | `15m` | | Biztonsági mentés | `backup.enabled` | ✅ Aktív / ❌ Inaktív | | Mentés ütemezés | `backup.db_dump_schedule` + `backup.restic_schedule` | `02:30 / 03:00` | | Monitoring | `monitoring.enabled` | ✅ Aktív / ❌ Inaktív | | Healthchecks URL | `monitoring.healthchecks_base` | URL or "–" | | Hub jelentés | `hub.enabled` (if exists) | ✅ Aktív / ❌ Inaktív / "–" | | Controller verzió | built-in Version constant | `0.7.0` | Use green checkmark / red X styling for boolean states, consistent with the backup page. #### Section B: "Jelszó módosítás" (Change Password) — Editable Only shown if auth is enabled (password_hash is set). If no auth, show an info message: "A jelszavas védelem nincs beállítva. Kérd az üzemeltetőt a beállításhoz." Form fields: - Jelenlegi jelszó (current password) — required - Új jelszó (new password) — required, min 8 chars - Új jelszó megerősítése (confirm new password) — required, must match **POST `/settings/password` handler:** 1. Validate current password against effective hash (bcrypt compare) 2. Validate new password: min 8 chars, both fields match 3. Generate bcrypt hash (cost 10) for new password 4. Save to `settings.json` → `password_hash` 5. Invalidate current session (regenerate session secret so all cookies become invalid) 6. Redirect to `/login` with success flash message: "Jelszó sikeresen módosítva. Kérjük, jelentkezzen be az új jelszóval." **Error handling:** - Wrong current password → "Hibás jelenlegi jelszó" (stay on page) - Passwords don't match → "A két jelszó nem egyezik" - Too short → "A jelszónak legalább 8 karakter hosszúnak kell lennie" - Show errors inline, don't clear form ### 4.4 Template ``` File: controller/internal/web/templates/settings.html ``` Follow the same card-based layout as the backup page. Two cards: 1. "Rendszer konfiguráció" — info-grid with labels + values 2. "Jelszó módosítás" — form card --- ## 5. Implementation Order Follow this sequence to keep each step testable: ### Step 1: settings.json package - Create `internal/settings/settings.go` with Settings struct, Load/Save - Add settings instance to the controller's main app struct - Load on startup, log result - **Test:** Start controller, check logs for settings load message ### Step 2: Authentication - Add session secret generation on startup - Add auth middleware - Add login.html template - Add login/logout handlers - Add logout link to sidebar - Wire up routes in server.go - Set `web.password_hash` in controller.yaml on demo to test - **Test:** Navigate to dashboard → redirected to /login → enter password → dashboard loads → /logout → back to /login ### Step 3: DB validation persistence - After ValidateDump completes, save results to settings.json - On RefreshCache, load cached validations for initial display - **Test:** Deploy apps with DBs → trigger backup → check Érvényesítés column shows data → restart container → check column still shows data ### Step 4: Settings page - Add settings.html template - Add settings route + handler - Add sidebar menu item with bottom-pinning - Implement password change POST handler - **Test:** Open /settings → see config values → change password → re-login with new password ### Step 5: Cleanup & version bump - Update CONTEXT.md - Bump version to 0.7.0 - Build + deploy + verify on demo-felhom.eu --- ## 6. Files to Create / Modify ### New files: - `controller/internal/settings/settings.go` — Settings persistence - `controller/internal/web/templates/login.html` — Login page - `controller/internal/web/templates/settings.html` — Settings page ### Modified files: - `controller/internal/web/server.go` — Add auth middleware, new routes (/login, /logout, /settings, /settings/password), session management, settings page handler - `controller/internal/web/templates/` sidebar partial or base template — Add "Beállítások" menu item at bottom, logout link - `controller/internal/backup/backup.go` — Load cached validations in RefreshCache - `controller/internal/backup/dbdump.go` — Save validation results to settings.json - `controller/internal/config/config.go` — Possibly add data_dir path helper - `controller/cmd/controller/main.go` — Initialize settings, pass to web server and backup manager --- ## 7. Design Decisions & Notes ### Why settings.json (not SQLite)? - Single file, human-debuggable, easy to backup/restore - Tiny data volume (password hash + a few validation entries) - No query needs — just load/save whole struct - Customers or operators can inspect/reset it easily ### Why not modify controller.yaml for password changes? - controller.yaml is operator-provisioned config, risky to programmatically rewrite YAML - settings.json is a clean override layer: operator sets initial password in yaml, customer changes it in json - If settings.json is deleted, system falls back to controller.yaml password (recovery path) ### Session secret is ephemeral (memory only) - Container restart = all sessions invalidated = users must re-login - This is acceptable and actually desirable for security - No need to persist session state ### Notifications (Phase 2 prep) - The Settings struct includes a `Notifications` field placeholder - Phase 2 will add: email relay via k3s (similar to contact-mailer pattern), notification preferences UI - The relay approach: customer controller sends HTTP POST to a central notification API on k3s, which handles Resend delivery. Avoids storing Resend API keys on customer hardware. - This keeps secrets centralized and customer nodes lightweight. ### Settings page scope — what goes where - "Beállítások" = actual settings (things the user can configure or needs to know about their setup) - "Rendszermonitor" = live system state (hostname, uptime, CPU, RAM, disk, Docker containers) - No overlap — config is static, monitoring is dynamic