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>
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
- Migrasjon: Opprett
internal/database/migrations/002_add_fave_notes.sql - Modell: Legg til
Notes stringetterImagePathimodel.Fave - Store — Create: Endre signatur til
Create(userID, description, url, imagePath, notes, privacy), oppdater INSERT - Store — GetByID: Legg til
f.notesi SELECT og&f.Notesi Scan - Store — Update: Endre signatur, legg til
notes = ?i UPDATE - Store — Alle list-metoder: Legg til
f.notesi SELECT forListByUser,ListPublicByUser,ListPublic,ListByTag - Store — scanFaves: Legg til
&f.Notesi Scan - Handler — fave.go: Les
notesfra form ihandleFaveCreateoghandleFaveUpdate. Passfave.NotesihandleFaveEdittemplate data - Handler — api/api.go: Legg til
Notesi request/response-structs,faveJSON, import - Handler — import_export.go: Legg til notes i
ExportFave, CSV header/rader, import-parsing - Handler — feed.go: Sett
item.Description = f.Notesnår det ikke er tomt ifavesToFeedItems - Template — fave_form.html: Legg til
<textarea id="notes" name="notes">mellom URL og bilde - Template — fave_detail.html: Vis
{{if .Notes}}<div class="fave-notes">{{.Notes}}</div>{{end}}, forbedreog: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
- Ikoner: Opprett
web/static/icons/med 192px, 512px, 512px-maskable PNG-er - Service worker (
web/static/sw.js): Bruk{{BASE_PATH}}-plassholder, cache-first for/static/, network-first for HTML - pwa.go — handleManifest: Dynamisk JSON med BasePath injisert i
start_url,scope, ikonruter ogshare_target.action. Share target bruker GET for å unngå CSRF-problemer - pwa.go — handleServiceWorker: Les sw.js fra embedded FS, erstatt
{{BASE_PATH}}, settCache-Control: no-cache - pwa.go — handleShare: GET-handler bak
requireLogin. Lesurl,text,titlefra query. Mange Android-apper sender URL itext-feltet — sjekk begge. Redirect til/faves/new?url=...&description=...¬es=... - Ruter:
GET /manifest.json(offentlig),GET /sw.js(offentlig),GET /share(krever login) - handleFaveNew: Les
url,description,notesfra query-parametre for pre-fill - base.html:
<meta name="theme-color" content="#1095c1">,<link rel="manifest">,<meta name="base-path"> - 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,TestServiceWorkerBasePathTestShareRedirectsToFaveNew,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,TestTogglePrivacyNotOwnerTestFaveListShowsEditButton,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,TestDeleteUserCascadeTestAdminSetRoleSuccess,TestAdminSetRoleSelfTestAdminDeleteUserSuccess,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 |