Files
deploy-felhom-compose/TASK.md
T

14 KiB
Raw Blame History

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:

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.jsonpassword_hash (customer changed it)
  2. controller.yamlweb.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.goValidateDump):

  • After successful validation, save result to settings.json via 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/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.jsonpassword_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