Files
deploy-felhom-compose/TASK.md
T

340 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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