Commit graph

4 commits

Author SHA1 Message Date
1c95dbed00 feat(activity): Markdown rendering for descriptions
Activity descriptions now render through a small marked + DOMPurify
pipeline. Client-side only — the server keeps storing raw markdown
source, private descriptions stay inside the encrypted payload.

frontend/src/lib/markdown.ts exposes a single renderMarkdown(src)
helper. Allowlist: p / br / hr / strong / em / del / s / code / pre /
ul / ol / li / blockquote / a / h3–h6. URL scheme allowlist:
http(s) and mailto. Images are deliberately stripped — external
image URLs leak the viewer's IP to the linker's host. Raw HTML
pass-through is off. A DOMPurify afterSanitizeAttributes hook forces
target="_blank" rel="noopener noreferrer ugc" on every <a>, matching
the existing external-link pattern in PublicList. h1/h2 in the
source get downshifted to h3 via walkTokens so the description's
heading hierarchy doesn't collide with the SPA's own <h1>/<h2>.

Render sites: the two description spots in ActivityRow.svelte (one
for the decrypted private branch, one for non-private). New
.md class in styles.css gives the rendered block tight spacing,
discreet code/pre/blockquote treatment, accent-coloured links.

UX: a "Du kan bruke Markdown — **fet**, *kursiv*, [lenke](https://…),
lister med -" hint under the textarea in ActivityForm. No preview
pane to keep scope contained.

Tests: 14 cases in tests/markdown.test.ts covering the allowlist
(bold/italic/strike/lists/links/mailto), the sanitisation surface
(javascript: / data: / script / iframe / on* handlers / images),
and the h1/h2 → h3 downshift. happy-dom is registered locally in
beforeAll (and the markdown module dynamic-imported) rather than as
a project-wide preload — the latter overrides fetch/Request/Response
and breaks Bun-fetch-based API tests in other files.

Bundle impact: marked + dompurify add ~60KB to the SPA bundle.
2026-05-25 21:15:31 +02:00
ef02b3f585 feat(ops): emergency password-reset CLI + deployment docs
New CLI: bun run reset-password <email>

Two modes selected interactively:

- Recovery mode: if you still have the user's recovery code, unwrap
  the existing DEK with it and re-wrap with the new password. No data
  loss; the recovery code stays valid (mirrors /auth/recovery-complete).
- Nuke mode: if both password AND recovery code are gone, generate a
  fresh DEK + new recovery code (printed once), and DELETE the user's
  private activities — their ciphertext is permanently unrecoverable.
  Public/semi/friends rows and engagement (hearts/bookmarks/done) are
  preserved.

Both modes invalidate the user's sessions.

Password length matches the signup/recovery rule (12 chars min).
Wrong-recovery-code path aborts before any DB writes. Hand-rolled
line reader sidesteps a Bun quirk where node:readline only delivers
the first answer when stdin is piped.

Also expand README's "Deployment" section: container snippet stays,
plus new subsections for env vars, TLS termination (with a Caddyfile
example), backup/restore via sqlite3 .backup, the /api/health
healthcheck, upgrade flow, and a walkthrough of the reset CLI.
2026-05-25 20:04:57 +02:00
59e2f95767 Drag-reorder works on touch via svelte-dnd-action
HTML5 native drag-and-drop doesn't fire on touchscreens — mobile
users couldn't reorder the list at all. Swapped the manual DnD
wiring (dragstart/dragover/drop) for svelte-dnd-action, which uses
Pointer Events and handles mouse, touch, AND keyboard reorder
uniformly. Linear-quality reorder UX for ~11 KB gzipped.

Replacement details:
  - bun add svelte-dnd-action (0.9.69)
  - Home.svelte: ~70 lines of manual handler code deleted, replaced
    with ~30 lines wiring up `use:dndzone` + `onconsider` +
    `onfinalize`. The midpoint-position math for sort_position is
    unchanged (finalize gives us the new neighbour list directly).
  - ActivityRow.svelte: the drag handle's `onpointerdown` flips a
    parent-owned dragDisabled flag to false — the library then
    takes over. Standard "handle-only drag" recipe; clicks on the
    title/buttons inside the card don't initiate drag because
    dragDisabled stays true everywhere else.
  - dndItems is a buffer copy of `filtered` that the library
    mutates during a drag. An $effect re-syncs it from `filtered`
    between drags (so new activities still float to the top, etc).
  - Shadow item (the library's placeholder while dragging) is
    rendered at 30% opacity so the drop target is visible without
    flashing.

Accessibility wins for free:
  - Keyboard reorder: focus an item, press Space to "pick up",
    arrow keys to move, Space to drop. Screen readers get
    polite-live announcements of each move from the library.
  - Touch reorder works on iOS Safari and Android Chrome.

96 tests still pass; typecheck clean; build ok.
Bundle: 122 KB → 154 KB (gzipped 42 → 53 KB, ~+11 KB).
2026-05-25 16:59:43 +02:00
47963c9225 Scaffold Vinterliste — end-to-end encrypted winter activity list
Foundation for an E2E-encrypted activity list per
winter-list-claude-code-prompt.md.

Server (Bun + Hono):
- bun:sqlite with WAL and the spec's schema (idempotent migration)
- opaque server-stored sessions, httpOnly cookie
- signup / challenge / login / logout / me / password / recovery-challenge /
  recovery-complete
- activity CRUD with strict visibility rules: private uses ciphertext+nonce,
  semi never serializes owner_id, public attributes the owner
- tag store with normalisation + autocomplete (semi/public only)

Frontend (Svelte 5 + Vite):
- libsodium-wrappers-sumo for client crypto (Argon2id + XChaCha20-Poly1305).
  SUMO is required because the standard build omits crypto_pwhash.
- IndexedDB-backed private tag index (never leaves the browser)
- in-memory DEK (no localStorage); page reload re-prompts for password
- signup shows the recovery code once; tag input merges server + private
  sources with clear labelling
- Bokmål UI

Crypto module (shared/crypto.ts):
- pure, runs in both Bun and the browser via a runtime-conditional loader
  that papers over libsodium-wrappers-sumo's broken ESM entry (createRequire
  on server, Vite alias in the browser)
- DEK wrap/unwrap, AEAD payload encryption, recovery code generation with
  a visually-unambiguous alphabet

Verification:
- 22 crypto round-trip tests (wrap/unwrap, AEAD tamper rejection, password
  change preserves ciphertexts, recovery still works after rotation)
- typecheck passes for server and frontend
- Vite production build succeeds; libsodium SUMO chunk is ~315 KB gzipped

Single-image Containerfile for podman: builds frontend in a builder stage,
runs Bun in a slim runtime; one volume for the SQLite file; BUILD_DATE /
GIT_REVISION baked into OCI labels and /etc/build-info.

Known limitation deferred for this commit: the recovery endpoint has no
server-side proof of the recovery code (anyone who knows an email can lock
out the legitimate user, though they can't read any data). Closed in the
next commit.
2026-05-25 12:27:14 +02:00