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

316 lines
14 KiB
Markdown

# 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
```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`:
```html
{{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>`:
```html
<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**:
```go
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`):
```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:
```html
<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`:
```html
<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** |