netops/netops-todo-node/CLAUDE.md
Jon Vanvik f6268440d9 Innebygd innlogging (AUTH_MODE=app) med neon-login og proxy-bypass
Valgfri app-auth ved siden av proxy-modus (default uendret). Signert
sesjons-cookie (scrypt-passord, HMAC, ingen avhengigheter), gating av API +
WebSocket, og en selvstendig neon-login (public/login.html) for uautentiserte.

Trusted bypass: en proxy-identitet (X-Remote-User / Basic Auth) regnes som
innlogget uten app-passord (TRUST_PROXY_HEADER, default på), så man kan
bootstrappe: logg inn via NPM Basic Auth → sett app-passord i konto-modalen →
skru av Basic Auth. Konto-UI for sett/endre passord + logg ut.

AUTH_MODE leses per createServer-instans (testbart). 34 integrasjonstester
(8 nye for app-auth), alle grønne. README/CLAUDE.md/setup-skript oppdatert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 14:27:13 +02:00

210 lines
12 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.

# NetOps To-Do (Node) — Handoff / Project context
> Egnet som `CLAUDE.md` for fremtidige Claude-sesjoner. Dette er
> videreutviklingen av PHP-versjonen i `../netops-todo-server/` — samme formål
> og datamodell, men ombygd til Node/npm med sanntidssynk og flere
> samarbeidsfunksjoner. PHP-versjonen ligger urørt ved siden av.
## Hva er dette
To-do-applikasjon for en network engineer / et lite driftsteam. Kategorier
L1L4, BGP, peering, security, NMS, automatisering, dokumentasjon, incident,
prosjekt. Hver oppgave har prioritet (P1P4), status, estimat, lokasjon, frist,
tags, eier, kommentarer og audit-felter.
Nytt mot PHP-versjonen:
- **WebSocket-sanntidssynk** (erstatter 60s-polling) + presence + "redigerer nå"
- **Kanban-tavle** med drag & drop, i tillegg til tabellvisning
- **Dagsplan** (tredje visning) — per bruker, per dato; ordnet liste man bygger
ved å plukke fra backlog. Drag-sortering, kapasitet i timer, navn, flytt hele
planen til en annen dato, og delbar (default delt; toggle for privat). Andres
delte planer kan ses skrivebeskyttet. Plan-membership vises som badge på
oppgavekort/-rader (dag + person).
- **Egendefinerte kategorier** — i tillegg til de 12 innebygde; lagres i
categories.json, deles via WS, valideres dynamisk.
- **Feltmodus** — `body.field-mode`-toggle for større berøringsmål på mobil.
- **Kommentar ved statusbytte** — valgfri `comment``PUT /api/tasks/:id`
festes som kommentar med `kind:'status'` og legges i aktivitets-detaljen.
- **Kommentarer** per oppgave
- **Aktivitetslogg** (hvem gjorde hva, server-side, capped til 400 entries)
- **Optimistisk låsing** — `v`-felt per oppgave, 409 ved konflikt
- **Hurtig-registrering** med token-parsing (`#kat @eier p1 !frist ~timer loc:sted`)
- **Angre sletting**, tastatursnarveier, persistente filtre (localStorage)
## Filer
```
netops-todo-node/
├── server.js Express + ws. REST-API, WebSocket-hub, auth-oppslag/gating.
├── lib/store.js Datalager: normalisering, validering, aktivitetslogg,
│ atomisk JSON-persistens. Kilden til sannhet for skjema.
├── lib/auth.js Innebygd auth: scrypt-passord, signert sesjons-token.
├── public/
│ ├── index.html Markup. Ingen templating — app.js fyller alt.
│ ├── app.js All frontend-logikk (vanilla JS, ingen rammeverk).
│ ├── login.html Selvstendig neon-login (AUTH_MODE=app). Inline CSS/JS.
│ └── style.css Mørkt tema, samme visuelle identitet som PHP-versjonen.
├── seed.json 16 eksempel-oppgaver.
├── setup-debian13.sh Engangs-oppsett på Debian 13 LXC (apt + npm ci + systemd).
├── test/api.test.js Integrasjonstester (node:test) — REST + WS ende-til-ende.
├── data/ Opprettes ved kjøring: tasks.json, activity.json,
│ users.json, plans.json, categories.json,
│ auth.json (passordhasher), auth.secret (HMAC-nøkkel).
└── README.md Installasjon, proxy-oppsett, API-referanse.
```
## Arkitektur
- **Alle mutasjoner via REST** (lett å curl-e/feilsøke). WebSocket (`/ws`)
brukes kun til push: endringer, presence, editing-signaler.
- Én node-prosess eier datakatalogen. Lagring i minnet, skriv til disk er
atomisk (tmp + rename) og serialisert gjennom en promise-kjede i `store.js`.
- **To auth-modi** (`AUTH_MODE`, lest per `createServer`-instans så det er
testbart):
- `proxy` (default): `X-Remote-User` > Basic Auth > `netops_user`-cookie >
`anon`. Ingen guard — alt åpent. Bakoverkompatibelt.
- `app`: innebygd innlogging. `resolveAuth(req)` → sesjons-cookie
(`netops_session`, verifisert i lib/auth.js) ELLER, hvis `TRUST_PROXY_HEADER`,
en proxy-identitet (`proxyUser()` = header/Basic, IKKE navn-cookie) som
"trusted bypass". En guard-middleware 401-er API og serverer `login.html`
navigasjon når uautentisert. WS-upgrade gates likt.
- Bypass-poenget: bootstrap fra Basic Auth → sett app-passord → skru av Basic
Auth. `POST /api/auth/password` tillater å sette passord når man er
proxy-innlogget; krever nåværende passord kun når man endrer via en app-sesjon.
- `resolveAuth` brukes både i HTTP-middleware og WS (`server.on('upgrade')` +
`wss.on('connection')`), så samme identitet/gating gjelder begge.
- Sesjon: signert cookie `b64url(user).exp.HMAC` (ingen server-side lager).
Hemmelighet fra `AUTH_SECRET` eller persistert `data/auth.secret`. Cookie er
HttpOnly, SameSite=Lax, Secure når `X-Forwarded-Proto: https`.
### WebSocket-protokoll (server → klient)
| type | Innhold | Når |
|---|---|---|
| `hello` | `user`, `online[]`, `editing{}` | Ved tilkobling |
| `sync` | `tasks[]`, `activity[]`, evt. `by` | Ved tilkobling + etter replace/seed |
| `task` | `op: create/update/delete`, `task`/`id`, `by`, `activity` | Etter hver mutasjon |
| `presence` | `online[]` | Når noen kobler til/fra |
| `editing` | `editing{}` = `{taskId: [brukere]}` | Når noen åpner/lukker modal |
| `plan` | `user`, `date`, `name`, `items[]`, `shared` | Etter plan-endring. Delte planer → alle; private → kun eieren (andre får tom `items` for å fjerne badge) |
| `categories` | `categories[]`, `by` | Etter `POST /api/categories` — til alle |
Klient → server: kun `{ type: 'editing', id, active }`.
### Task-skjema (kanonisk form — `normalizeTask()` i lib/store.js)
Som PHP-versjonen, pluss:
```json
{
"v": 3,
"comments": [{ "id": "c_…", "by": "jon", "at": "ISO", "text": "≤1000",
"kind": "status", "status": "done" }]
}
```
`v` bumpes ved hver felt-oppdatering (også når en `comment` følger med et
statusbytte via `update()`, siden det ER en oppdatering — men IKKE via det
separate `POST .../comments`-endepunktet, så en ren kommentar ikke gir falsk
konflikt for noen som redigerer felter samtidig). `kind`/`status` settes kun på
kommentarer som kommer fra et statusbytte; vanlige kommentarer mangler dem.
Klienten sender `v` den redigerte fra; mismatch → 409 med serverens task i
svaret. Frontend håndterer det i `handleSaveError()`: oppdaterer visningen,
viser varsel, og lar brukeren lagre på nytt (som da overskriver bevisst).
### Dagsplan-skjema (`plans.json`)
```json
{ "jon": { "2026-06-15": { "name": "Feltdag Bergen", "items": ["t_abc", "t_def"], "shared": true } } }
```
Per bruker, per dato. `shared` default `true` (planer er synlige for alle som
hovedregel). `_planEntry()` normaliserer både gammelt array-format og nytt
objektformat (migrering skjer i `_prunePlans()` ved oppstart). `getPlan()`
filtrerer bort slettede oppgaver; `getPlanFor(viewer, owner, date)` håndhever
tilgang (egen, eller delt → ellers null/403); `setPlan/setPlanName/setPlanShared`
muterer; `movePlan()` slår sammen til måldato uten datatap; `_scrubFromPlans()`
rydder ved sletting; `_prunePlans()` dropper datoer eldre enn 30 dager.
`planIndex(viewer)` returnerer `{taskId: [{user,date,name}]}` filtrert per viewer
(delte + egne private) — driver plan-badgene på kort/rader.
### Kategorier (`categories.json`)
`DEFAULT_CATEGORIES` (12, i koden) + egendefinerte fra `categories.json`
(`[{id,name,color,desc,custom:true}]`). `store.categories()`/`categoryIds()` gir
det effektive settet; `normalizeTask` validerer mot `opts.validCategories =
this.categoryIds()`. `addCategory()` genererer en unik id fra navnet og tildeler
farge fra en palett. Frontend får lista fra `/api/state` og via `categories`-WS;
`state.categories` + `catColor/catName/catById`-hjelperne erstatter den gamle
hardkodede `CATEGORIES`/`CAT_COLORS`.
## Designvalg verdt å vite
- **Ingen database, ingen frontend-rammeverk, to npm-avhengigheter**
(express, ws). Samme "lett å patche for neste vakthavende"-filosofi som før.
- **Broadcast går til alle, inkludert avsender.** Klienten upserter idempotent
på id, så dobbel oppdatering er harmløs. REST-svaret brukes også, slik at
UI-et fungerer selv om WS er nede (da poller frontend hvert 30s).
- **Delvis update er lov:** `PUT /api/tasks/:id` med bare `{v, status}`
normalisering merger mot eksisterende. Quick-actions bruker dette.
- **Statuskommentar går gjennom samme update-kall** (transient `comment`-felt),
ikke et eget endepunkt — så statusbytte + kommentar + aktivitetslinje blir
atomisk og ett enkelt WS-broadcast. `changeStatusWithComment()` i app.js viser
dialogen for ferdig/blokkert; tavle-drag og tabell-✓ ruter gjennom den.
- **Dagsplanen er optimistisk i frontend:** `addToPlan`/`removeFromPlan`/drag
muterer `state.plan.items` og kaller `savePlan()` (PUT) som reconciler mot
serverens validerte svar. Container-nivå drag-lyttere bindes én gang
(`bindPlanContainer`); per-element bindes ved hver render (`bindPlanDnD`).
- **Kommentar-sletting er begrenset til egen bruker** (eneste
autorisasjonsregel i appen — alt annet er åpent for alle innloggede).
- Chart.js fra CDN; frontend degraderer pent (skjuler grafer) uten nett.
## Sikkerhetsstatus
Som PHP-versjonen (XSS-escaping i alle render-paths, whitelist-enums,
strenglengde-klamping, brukernavn-regex `[A-Za-z0-9._@-]{1,64}`), pluss:
- `/api/login` avviser ugyldige navn i stedet for å sanitere stille.
- Cookie settes med `SameSite=Lax` — gir grunnleggende CSRF-vern for
cookie-auth-modus. Bak proxy-auth gjelder samme vurdering som før
(intern bruk OK; offentlig nett → legg på token).
- `express.json({ limit: '2mb' })` begrenser payload (import av store lister).
- **App-auth (AUTH_MODE=app):** scrypt-hashing + `timingSafeEqual`, signert
HMAC-sesjons-cookie (HttpOnly, SameSite=Lax, Secure over https), passord min.
6 tegn. Auth-filer (`auth.json`, `auth.secret`) skrives med mode `0600`.
`proxyUser()` (bypass-kilden) stoler IKKE på navn-cookien, kun proxy-satte
headere. Forutsetning for bypass: app-porten er kun nåbar fra proxyen — sett
`TRUST_PROXY_HEADER=0` for å fjerne bypass etter bootstrapping.
## Vanlige utvidelser
- **Nytt felt per oppgave:** input i modal (index.html) → `saveForm()` og
`parseQuickAdd()` om ønskelig (app.js) → `normalizeTask()` (store.js) →
`diffDetail()` for aktivitetsloggen → `renderCard()`/`renderTable()`.
- **Ny kategori:** egendefinerte legges til i UI-et («+ Ny kategori …») og
lagres i categories.json. Nye *innebygde* defaults endres i `DEFAULT_CATEGORIES`
(både store.js og app.js har lista; serveren er kilden, app.js har en fallback).
- **SQLite:** bytt persistens i `Store` (load/_persist) mot `node:sqlite`
resten av appen er uberørt siden alt går gjennom Store-metodene.
- **Varsler ved frist:** server har allerede deadline per task; en
`setInterval` i server.js som broadcaster `{type:'due', …}` + Notification
API i frontend er den naturlige veien.
## Testing-status
- `npm test`: 34 integrasjonstester (node:test), booter ekte servere på
tilfeldig port med temp-datakatalog (én i proxy-modus, én i app-modus). Dekker
normalisering, versjonskonflikt, kommentar-autorisasjon, statuskommentar,
dagsplan (CRUD, per-bruker, validering, scrub, dato-fallback, navn, flytt/merge,
deling + tilgang, planIndex per viewer), egendefinerte kategorier, aktivitetslogg,
WS-broadcast (delt vs. privat plan), persistens, cookie-auth, og app-auth
(401-guard, login-side på navigasjon, proxy-bypass, passord-bootstrap, sesjon,
feil passord, TRUST_PROXY_HEADER=0). Alle grønne per 2026-06-15 (Node 25).
- Manuell røyk-test over HTTP: kategori-opprettelse (auto-id + farge), navngitt
delt plan, annen brukers tilgang (delt 200 / privat 403), flytt med bevart
navn+delingsstatus, og planIndex verifisert.
- Ikke testet i nettleser med flere samtidige brukere — første ekte test bør
være to nettleservinduer mot `npm start`: del en dagsplan og se den hos den
andre brukeren + badge på oppgaven, flytt planen til en annen dag, legg til en
egendefinert kategori, og slå på feltmodus på en smal skjerm.