favoritter/PLANS-v1.1.md
Ole-Morten Duesund a8f3aa6f7e test: add comprehensive test suite (44 → 169 tests) and v1.1 plan
Add 125 new test functions across 10 new test files, covering:
- CSRF middleware (8 tests): double-submit cookie validation
- Auth middleware (12 tests): SessionLoader, RequireAdmin, context helpers
- API handlers (28 tests): auth, faves CRUD, tags, users, export/import
- Web handlers (41 tests): signup, login, password reset, fave CRUD,
  admin panel, feeds, import/export, profiles, settings
- Config (8 tests): env parsing, defaults, trusted proxies, normalization
- Database (6 tests): migrations, PRAGMAs, idempotency, seeding
- Image processing (10 tests): JPEG/PNG, resize, EXIF strip, path traversal
- Render (6 tests): page/error/partial rendering, template functions
- Settings store (3 tests): CRUD operations
- Regression tests for display name fallback and CSP-safe autocomplete

Also adds CSRF middleware to testServer chain for end-to-end CSRF
verification, TESTPLAN.md documenting coverage, and PLANS-v1.1.md
with implementation plans for notes+OG, PWA, editing UX, and admin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 00:18:01 +02:00

14 KiB

Implementeringsplan v1.1 — Favoritter

Fire funksjoner som utvider Favoritter med notater og OG-metadata, PWA-installering med Android-delingsstøtte, bedre redigeringsflyt, og utvidede admin-verktøy.

Oversikt

# Funksjon Omfang Avhengigheter
1 OG-støtte + fritekstfelt (notes) Migrasjon, modell, store, alle handlere, maler, eksport/import, feed Ingen
2 PWA + Android share intent Service worker, manifest, nye handlere, base-mal, JS Avhenger av 1 (deling pre-fyller notes)
3 Redigeringsforbedringer (UX) Maler, CSS, ny toggle-rute, ny partial Avhenger av 1 (notes vises i kort)
4 Admin-forbedringer Store, handlere, maler, ruter Ingen

Funksjon 1: OG-støtte + fritekstfelt (notes)

Beskrivelse

Legger til et valgfritt langtekstfelt «notes» på hver favoritt. Forbedrer Open Graph-metatagger på detaljsiden slik at og:description bruker notes når det er tilgjengelig.

Berørte filer

Fil Endring
internal/database/migrations/002_add_fave_notes.sql Ny fil: ALTER TABLE faves ADD COLUMN notes
internal/model/fave.go Legg til Notes string i Fave-struct
internal/store/fave.go Oppdater alle SQL-spørringer, Create/Update-signaturer, scanFaves
internal/handler/fave.go Les notes fra skjema i create/edit/update
internal/handler/api/api.go Legg til notes i request/response-structs og faveJSON
internal/handler/import_export.go Legg til notes i ExportFave, CSV-kolonner, import
internal/handler/feed.go Bruk notes som feed item description
web/templates/pages/fave_form.html Legg til textarea for notes mellom URL og bilde
web/templates/pages/fave_detail.html Vis notes, forbedre og:description

Migrasjons-SQL

-- 002_add_fave_notes.sql
-- Legger til et valgfritt notatfelt på favoritter.
ALTER TABLE faves ADD COLUMN notes TEXT NOT NULL DEFAULT '';

Implementeringssteg

  1. Migrasjon: Opprett internal/database/migrations/002_add_fave_notes.sql
  2. Modell: Legg til Notes string etter ImagePath i model.Fave
  3. Store — Create: Endre signatur til Create(userID, description, url, imagePath, notes, privacy), oppdater INSERT
  4. Store — GetByID: Legg til f.notes i SELECT og &f.Notes i Scan
  5. Store — Update: Endre signatur, legg til notes = ? i UPDATE
  6. Store — Alle list-metoder: Legg til f.notes i SELECT for ListByUser, ListPublicByUser, ListPublic, ListByTag
  7. Store — scanFaves: Legg til &f.Notes i Scan
  8. Handler — fave.go: Les notes fra form i handleFaveCreate og handleFaveUpdate. Pass fave.Notes i handleFaveEdit template data
  9. Handler — api/api.go: Legg til Notes i request/response-structs, faveJSON, import
  10. Handler — import_export.go: Legg til notes i ExportFave, CSV header/rader, import-parsing
  11. Handler — feed.go: Sett item.Description = f.Notes når det ikke er tomt i favesToFeedItems
  12. Template — fave_form.html: Legg til <textarea id="notes" name="notes"> mellom URL og bilde
  13. Template — fave_detail.html: Vis {{if .Notes}}<div class="fave-notes">{{.Notes}}</div>{{end}}, forbedre og:description:
{{if .Notes}}
    <meta property="og:description" content="{{truncate 200 .Notes}}">
{{else if .URL}}
    <meta property="og:description" content="{{.URL}}">
{{else}}
    <meta property="og:description" content="En favoritt av {{.DisplayName}} på {{$.SiteName}}">
{{end}}

Tester

  • Oppdater TestFaveCRUD (Create/Update-kall med notes)
  • Ny TestFaveNotes (CRUD med notes-felt)
  • Oppdater API CRUD-tester, ny TestAPICreateFaveWithNotes
  • ~5 nye, ~7 endrede

Funksjon 2: PWA + Android share intent

Beskrivelse

Gjør Favoritter installerbar som Progressive Web App med offline-støtte for statiske ressurser. Web Share Target API lar Android-brukere dele lenker direkte til Favoritter fra hvilken som helst app.

Berørte filer

Fil Endring
web/static/sw.js Ny fil: Service worker (cache-first statisk, network-first HTML)
web/static/icons/icon-192.png Ny fil: PWA-ikon 192x192
web/static/icons/icon-512.png Ny fil: PWA-ikon 512x512
web/static/icons/icon-512-maskable.png Ny fil: Maskable ikon for Android
internal/handler/pwa.go Ny fil: 3 handlere (manifest, SW, share)
internal/handler/handler.go 3 nye ruter
internal/handler/fave.go handleFaveNew leser url/description/notes fra query-parametre
web/templates/layouts/base.html Manifest-link, theme-color, base-path meta
web/static/js/app.js SW-registrering

Implementeringssteg

  1. Ikoner: Opprett web/static/icons/ med 192px, 512px, 512px-maskable PNG-er
  2. Service worker (web/static/sw.js): Bruk {{BASE_PATH}}-plassholder, cache-first for /static/, network-first for HTML
  3. pwa.go — handleManifest: Dynamisk JSON med BasePath injisert i start_url, scope, ikonruter og share_target.action. Share target bruker GET for å unngå CSRF-problemer
  4. pwa.go — handleServiceWorker: Les sw.js fra embedded FS, erstatt {{BASE_PATH}}, sett Cache-Control: no-cache
  5. pwa.go — handleShare: GET-handler bak requireLogin. Les url, text, title fra query. Mange Android-apper sender URL i text-feltet — sjekk begge. Redirect til /faves/new?url=...&description=...&notes=...
  6. Ruter: GET /manifest.json (offentlig), GET /sw.js (offentlig), GET /share (krever login)
  7. handleFaveNew: Les url, description, notes fra query-parametre for pre-fill
  8. base.html: <meta name="theme-color" content="#1095c1">, <link rel="manifest">, <meta name="base-path">
  9. app.js: SW-registrering via navigator.serviceWorker.register(), les base-path fra meta-tag

Designbeslutninger

  • GET for share target: Android kan ikke levere CSRF-tokens, så POST er utelukket. GET-handler redirecter til fave-skjema
  • Dynamisk manifest: Nødvendig for å injisere BasePath korrekt ved subpath-deployment
  • SW Cache-Control: no-cache: Nettleseren må kunne sjekke for oppdateringer

Tester

  • TestManifestJSON, TestServiceWorkerContent, TestServiceWorkerBasePath
  • TestShareRedirectsToFaveNew, TestShareTextFieldFallback, TestShareRequiresLogin
  • ~6 nye

Funksjon 3: Redigeringsforbedringer (UX)

Beskrivelse

Backend-redigering fungerer allerede (rediger, slett, personvernveksling). Problemet er at handlingene kun er synlige på detaljsiden. Denne funksjonen gjør dem tilgjengelige fra listevisninger.

Tier 1 — Synlige handlinger i listevisninger

Fil Endring
web/templates/pages/fave_list.html Legg til rediger/slett-knapper i hver card footer
web/templates/pages/profile.html Legg til rediger-lenke for eiers egne kort
web/static/css/style.css Kompakte .fave-card-actions-stiler

fave_list.html: Legg til etter tags-footer, før </article>:

<footer class="fave-card-actions">
    <a href="{{basePath}}/faves/{{.ID}}/edit" class="fave-action-link">Rediger</a>
    <button hx-delete="{{basePath}}/faves/{{.ID}}"
            hx-confirm="Er du sikker på at du vil slette denne favoritten?"
            hx-target="closest article" hx-swap="outerHTML"
            class="fave-action-btn secondary">Slett</button>
</footer>

Eksisterende handleFaveDelete returnerer allerede tom response for HTMX-forespørsler, så hx-target="closest article" hx-swap="outerHTML" fjerner kortet fra DOM.

profile.html: Vis kun for eier med {{if $d.IsOwner}}.

Tier 2 — Rask personvernveksler

Fil Endring
internal/store/fave.go Ny UpdatePrivacy(id, privacy) metode
internal/handler/fave.go Ny handleFaveTogglePrivacy handler
internal/handler/handler.go Ny rute: POST /faves/{id}/privacy
web/templates/partials/privacy_toggle.html Ny HTMX-partial for toggle-knapp
web/templates/pages/fave_list.html Bruk toggle i stedet for statisk badge
web/templates/pages/fave_detail.html Bruk toggle for eier

Store — UpdatePrivacy:

func (s *FaveStore) UpdatePrivacy(id int64, privacy string) error {
    _, err := s.db.Exec(
        `UPDATE faves SET privacy = ?,
            updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
        WHERE id = ?`, privacy, id)
    return err
}

Handler: Verifiserer eierskap, veksler mellom "public"/"private", returnerer oppdatert partial.

Partial (privacy_toggle.html):

<span class="privacy-toggle" id="privacy-{{.ID}}">
    <button hx-post="{{basePath}}/faves/{{.ID}}/privacy"
            hx-target="#privacy-{{.ID}}" hx-swap="outerHTML"
            class="fave-action-btn {{if eq .Privacy "private"}}secondary{{end}}">
        {{if eq .Privacy "public"}}Offentlig{{else}}Privat{{end}}
    </button>
</span>

CSRF-token håndteres automatisk av htmx:configRequest-hooken i app.js.

Tester

  • TestUpdatePrivacy, TestTogglePrivacyOwner, TestTogglePrivacyNotOwner
  • TestFaveListShowsEditButton, TestFaveListDeleteViaHTMX
  • ~5 nye

Funksjon 4: Admin-forbedringer

Beskrivelse

Utvider admin-panelet med mulighet til å endre brukerroller og permanent slette brukerkontoer. Eksisterende admin-funksjonalitet som allerede fungerer:

  • Lukke registreringer (/admin/settings → signup_mode)
  • Låse/deaktivere brukere (/admin/users/{id}/toggle-disabled)
  • Tilbakestille passord (/admin/users/{id}/reset-password)
  • Godkjenne/avvise registreringsforespørsler
  • Opprette brukere

Del A: Endre brukerrolle

Fil Endring
internal/store/user.go Ny SetRole(userID, role) metode
internal/handler/admin.go Ny handleAdminSetRole handler
internal/handler/handler.go Ny rute: POST /admin/users/{id}/role
web/templates/pages/admin_users.html Rolle-dropdown per brukerrad

Store — SetRole: Validerer at role er "user" eller "admin", oppdaterer databasen.

Handler: Forhindrer at admin endrer egen rolle. Oppdaterer rolle og viser bekreftelse.

Template: <select name="role"> med onchange="this.form.submit()" i handlingskolonnen.

NB: onchange inline handler blokkeres av CSP (script-src 'self'). Bruk i stedet en delegert event listener i app.js, eller legg til en submit-knapp:

<form method="POST" action="{{basePath}}/admin/users/{{.ID}}/role" class="inline-form">
    <input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
    <select name="role" class="inline-input">
        <option value="user" {{if eq .Role "user"}}selected{{end}}>Bruker</option>
        <option value="admin" {{if eq .Role "admin"}}selected{{end}}>Admin</option>
    </select>
    <button type="submit" class="nav-button outline">Lagre</button>
</form>

Del B: Slette brukerkontoer

Fil Endring
internal/store/user.go Ny Delete(userID) metode
internal/handler/admin.go Ny handleAdminDeleteUser handler
internal/handler/handler.go Ny rute: POST /admin/users/{id}/delete
web/templates/pages/admin_users.html Slett-knapp med hx-confirm

Store — Delete: DELETE FROM users WHERE id = ?. Kaskadesletting via foreign keys fjerner sessions, faves og fave_tags automatisk.

Handler: Forhindrer selvsletting. Sletter brukerens bilder fra disk før databasesletting (fordi kaskaden fjerner fave-radene som refererer til bildefilene). Viser bekreftelse.

Template: HTMX-knapp med hx-confirm:

<button hx-post="{{basePath}}/admin/users/{{.ID}}/delete"
        hx-confirm="Er du HELT sikker? Dette sletter brukeren og alle favorittene permanent."
        hx-target="closest tr" hx-swap="outerHTML"
        class="nav-button outline" style="color: var(--pico-del-color);">Slett</button>

Tester

  • TestSetRole, TestSetRoleInvalid, TestDeleteUser, TestDeleteUserCascade
  • TestAdminSetRoleSuccess, TestAdminSetRoleSelf
  • TestAdminDeleteUserSuccess, TestAdminDeleteUserSelf, TestAdminDeleteUserCascadesData
  • ~9 nye

Samlet implementeringsrekkefølge

Fase 1: Funksjon 1 + Funksjon 4 (parallelt, ingen avhengigheter)
  ├── 1a: Migrasjon + modell + store (notes)
  ├── 1b: Handlere og maler (notes + OG)
  ├── 4a: SetRole + handler + mal
  └── 4b: Delete + handler + mal

Fase 2: Funksjon 3 (krever notes fra Fase 1)
  ├── 3a: Tier 1 — rediger/slett-knapper i listevisninger
  └── 3b: Tier 2 — personvernveksler

Fase 3: Funksjon 2 (krever notes fra Fase 1)
  ├── 2a: Ikoner + service worker + manifest-handler
  ├── 2b: Share-handler + pre-fill
  └── 2c: Base-mal + SW-registrering

Nye endepunkter

Metode Rute Handler Auth
GET /manifest.json handleManifest Nei
GET /sw.js handleServiceWorker Nei
GET /share handleShare Ja
POST /faves/{id}/privacy handleFaveTogglePrivacy Ja
POST /admin/users/{id}/role handleAdminSetRole Admin
POST /admin/users/{id}/delete handleAdminDeleteUser Admin

Nye filer

Fil Beskrivelse
internal/database/migrations/002_add_fave_notes.sql Migrasjon: notes-kolonne
internal/handler/pwa.go PWA-handlere (manifest, SW, share)
web/static/sw.js Service worker
web/static/icons/icon-*.png PWA-ikoner (3 stk)
web/templates/partials/privacy_toggle.html HTMX personvernveksler

Estimert testtillegg

Funksjon Nye tester
1: Notes + OG ~5
2: PWA + share ~6
3: Redigering UX ~5
4: Admin ~9
Totalt ~25