netops/netops-todo-node/CLAUDE.md
Jon Vanvik 3f69cd4248 Auth-herding: roller, kontoadmin, token-revokering + søkbar planliste + neon
Auth (app-modus):
- Roller admin/standard. Admin oppretter/sletter kontoer, resetter passord,
  endrer roller (konto-modal). Eksisterende brukere → admin ved migrering;
  bootstrap-bruker via proxy-bypass → admin. Siste admin vernet.
- Tilbakekalling via per-bruker epoke i tokenet: passordbytte, admin-reset,
  «Logg ut alle enheter» og sletting dreper alle aktive sesjoner. Token-format
  endret (3- → 4-delt) — alle logges ut én gang ved oppgradering.

Dagsplan: søkbar/velgbar planliste («📋 Planer») avledet fra planIndex —
finn plan på navn/dato/person uten å gjette dato.

GUI: login-neon trukket inn i hovedappen (header/knapper/KPI-kanter), og
login-siden fikk mer tech (HUD-hjørner, glitch-tittel, scanlines).

40 integrasjonstester (6 nye), alle grønne. README + CLAUDE.md oppdatert.

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

13 KiB
Raw Permalink Blame History

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.
  • Feltmodusbody.field-mode-toggle for større berøringsmål på mobil.
  • Kommentar ved statusbytte — valgfri commentPUT /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åsingv-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 på 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.epoch.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.
  • Roller + tilbakekalling: hver bruker i auth.json har role (admin/standard) og epoch. Tokenet bærer epoken; verifyToken avviser hvis epoken ikke matcher brukerens nåværende (eller kontoen er slettet). setPassword, admin-reset, bumpEpoch() («logg ut alle enheter») og sletting tilbakekaller dermed alle aktive sesjoner. Migrering: eksisterende brukere → admin, epoch=1. Ny bruker via proxy-bypass → admin (bootstrap). isAdminReq() i server.js regner proxy-bypass uten konto som admin. Siste admin kan ikke slettes/degraderes.
  • Token-format endret med herdingen (3- → 4-delt), så alle eksisterende sesjoner blir ugyldige én gang ved oppgradering — brukerne logger inn på nytt.

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:

{
  "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)

{ "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).
  • Søkbar planliste (derivePlanList()) bygges på klienten ved å gruppere state.planIndex på (bruker, dato) — ingen eget endepunkt, holder seg i synk via plan-WS-meldingene. «📋 Planer»-knappen åpner en søkbar dropdown.
  • Neon-aksenter fra login-siden er trukket inn i hoved-GUI via en liten blokk i style.css (header-glød, primærknapp, aktiv visning, KPI-kanter).
  • 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: 40 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, proxy-bypass, passord-bootstrap, sesjon, feil passord, TRUST_PROXY_HEADER=0, roller/admin-CRUD, epoke-tilbakekalling ved passordbytte/ reset/logout-all, vern av siste admin). 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.