14 KiB
TASK: Phase 1 — Authentication, Persistence & Settings Page
Version target: 0.7.0
Repo: deploy-felhom-compose (controller)
Overview
Three workstreams in this phase:
- Implement login/logout authentication for the controller dashboard
- Persist DB validation results across container restarts
- 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:
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.RWMutexfor concurrent access (backup goroutine writes validations, web reads them) - Log on every save:
[DEBUG] Settings saved to <path>
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:
settings.json→password_hash(customer changed it)controller.yaml→web.password_hash(operator provisioned)- 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:
<expiry_unix>.<hmac_sha256_hex>— 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_sessioncookie → 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 /logoutorPOST /logout→ set cookie MaxAge=-1 → redirect to/login- Add logout link to sidebar bottom (near version display):
<version> | 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=/backupsquery 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.jsonvia settings package:
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/falsebased on cached result - Set
DumpValidation.Messageto 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:
- Validate current password against effective hash (bcrypt compare)
- Validate new password: min 8 chars, both fields match
- Generate bcrypt hash (cost 10) for new password
- Save to
settings.json→password_hash - Invalidate current session (regenerate session secret so all cookies become invalid)
- Redirect to
/loginwith 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:
- "Rendszer konfiguráció" — info-grid with labels + values
- "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.gowith 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_hashin 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 persistencecontroller/internal/web/templates/login.html— Login pagecontroller/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 handlercontroller/internal/web/templates/sidebar partial or base template — Add "Beállítások" menu item at bottom, logout linkcontroller/internal/backup/backup.go— Load cached validations in RefreshCachecontroller/internal/backup/dbdump.go— Save validation results to settings.jsoncontroller/internal/config/config.go— Possibly add data_dir path helpercontroller/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
Notificationsfield 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