Skip to content

F1.D — Admin WebUI shell at /admin/* with login + CSRF + CSP #31

@HXYerror

Description

@HXYerror

Part of #23. Depends on F2.C.

Background

There is no admin surface today. We need a server-rendered shell with a login flow, CSRF protection, and a hardened CSP — the foundation for the keys / usage / traces pages.

Goal

/admin/* mounted on the same Hono app/port with hono/jsx baseline, login, session, navigation, layout, and security headers; zero new build pipeline.

Tasks

  • hono/jsx layout: src/admin/layout.tsx with header, nav (Models, Keys, Usage, Traces, Audit, Settings), footer
  • Mount adminApp at /admin in src/server.ts; static assets at /admin/assets/* (vendored uPlot ≈ 12 KB, inline CSS)
  • Index page /admin shows status: config version, db row counts, auth mode, current admin's id (last 4 of sha256)
  • Login flow (per security S11):
    • GET /admin/login — paste-key form
    • POST /admin/login — validates the key (must be admin tier), creates a server-side session row in SQLite (sessions table from a new migration), sets cookie sid with HttpOnly; Secure; SameSite=Strict; Path=/admin; Max-Age=28800
    • Refuse to serve /admin/* over plain HTTP on non-loopback addresses (require HTTPS or 127.0.0.1/::1)
    • Logout: DELETE /admin/session removes server row + clears cookie
    • 8h sliding expiry; key never echoed back, never stored client-side
  • CSRF (per security S9):
    • Double-submit pattern: csrf cookie (HMAC-bound to session id) + X-CSRF-Token header on every mutating request
    • Verify both Sec-Fetch-Site: same-origin AND token on every state-changing route
    • Token rendered into forms via JSX helper <Csrf />
  • CSP (per security S20): Content-Security-Policy: default-src 'self'; frame-ancestors 'none'; form-action 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'
  • Other security headers: X-Frame-Options: DENY, Referrer-Policy: no-referrer, X-Content-Type-Options: nosniff
  • Liveness / readiness (per backend review F1.A — config.json: schema, atomic write, fs.watch hot reload #24): /healthz (always 200, no auth) and /readyz (verifies DB + Copilot token freshness, no auth) outside the /admin prefix
  • Snapshot test of rendered HTML

Acceptance criteria

  • GET /admin redirects to /admin/login when not authenticated
  • Cookie has all three flags (HttpOnly, Secure, SameSite=Strict); CSP and frame-options present
  • CSRF rejection returns 403 with the specific reason logged (token missing / token mismatch / wrong origin)
  • Plain HTTP from non-loopback address is refused
  • No new build step in package.json; everything served by Bun at runtime

File pointers

  • New: src/admin/{layout,index,login,session}.tsx, src/admin/csrf.ts, src/admin/assets/{style.css,uplot.min.js,uplot.min.css}, src/lib/migrations/004_sessions.sql
  • Touch: src/server.ts

Dependencies

Depends on F2.C. Blocks F2.E, F3.B, F4.B.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions