340 lines
14 KiB
Markdown
340 lines
14 KiB
Markdown
# 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 <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:
|
||
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: `<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_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):
|
||
```
|
||
<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=/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 |