M13a_PLAN.md 12 KB

M13a — authd + web skeleton + JWT gate

First sub-milestone of M13. Delivers the auth foundation and the SPA shell. After this lands, you can log in (super-admin bootstrap only) and see the empty UI with "coming soon" placeholders. No CRUD yet — that's M13b.

Status: planning (post-M13.0 spec) Target: M13_FRONTEND_SPEC.md §4.1 Estimate: 5-6 days with one engineer


0. Recap — what M13a ships

  • cmd/authd/ — Go service on port 8804. JWT IdP. All 7 endpoints.
  • web/ skeleton — Vite + React 19 + shadcn/ui + TanStack Query + RHF + Zod. Login → /me → logout works. Other routes are "coming soon" placeholders.
  • cmd/admind/ modified to require JWT on all /v1/* endpoints (except /health, /metrics, the auth passthroughs).
  • SPEC.md M13a row flipped to ✅.

What M13a is NOT:

  • Not CRUD (Companies, Sources, Telegram) — that's M13b.
  • Not live tail, DLQ, audit, K8s, Playwright — that's M13c.
  • Not SSO — out of scope for all of v1.

1. Workstreams

┌──────────────────────────┐    ┌──────────────────────────┐
│  W1: authd service       │    │  W2: web/ skeleton       │
│  (JWT IdP, port 8804,    │───▶│  Vite + React 19 +       │
│   users/refresh/invites) │    │  shadcn/ui + RHF + Zod   │
└──────────────┬───────────┘    └──────────────┬───────────┘
               │                               │
               ▼                               ▼
┌──────────────────────────────────────────────────────────────┐
│  W3: admind JWT gate — require JWT on /v1/*,                │
│      share secret with authd, /me middleware                │
└──────────────────────────────────────────────────────────────┘
                                               │
                                               ▼
┌──────────────────────────────────────────────────────────────┐
│  W4: docker-compose + env + bootstrap script + smoke        │
└──────────────────────────────────────────────────────────────┘

Four workstreams. W1 → W2 → W3 → W4. W3 is the only piece that touches existing admind code.


2. Workstream details

W1: authd service (JWT IdP)

Goal: Standalone Go service on port 8804. Owns users, refresh tokens, invites, login/refresh/logout/me. HS256 JWT signed with BA_AUTH_JWT_SECRET (shared env with admind).

Scope:

  • cmd/authd/main.go — HTTP server, slog, prometheus metrics.
  • internal/authd/ (new package): handlers, JWT, refresh store, argon2id.
  • internal/authd/middleware.go — JWT verify, scope injection.
  • internal/authd/ratelimit.go — Redis-backed login rate limiter (5/min per IP, 10/min per username).
  • Postgres migrations: migrations/NNNN_auth_schema.sql (auth.users, refresh_tokens, invites, audit_log).
  • Makefile target: make authd builds the binary.
  • docker-compose.yml adds authd service on :8804, depends_on postgres, redis.
  • Env vars: BA_AUTH_JWT_SECRET, BA_AUTHD_HTTP_ADDR, BA_AUTH_SMTP_* (optional).
  • cmd/authd/sso/ (empty dir, // 501 Not Implemented placeholder for SSO routes — keeps the post-v1 path visible).

Out of scope (pre-wire only):

  • RS256 / JWKS verification — JWKSVerifier stub exists but disabled via BA_AUTH_ASYMMETRIC=false default.
  • SSO routes return 501.
  • Email sender is best-effort; if SMTP env unset, invite API returns the URL directly.

Exit criteria:

  • authd starts, all 7 endpoints (/v1/auth/login, /v1/auth/refresh, /v1/auth/logout, /v1/auth/me, /v1/auth/invites, /v1/auth/invites/accept, /v1/auth/password/change) green via curl with a real Postgres + Redis.
  • argon2id hash verify works (test with a known hash).
  • Refresh token rotation: old token rejected after rotation.
  • 5 failed logins → 423 for 5min (per username, not per IP).
  • Audit log rows written for login, logout, invite_create, invite_accept, password_change.
  • Prometheus metrics: ba_authd_login_total{result=ok|error}, ba_authd_refresh_total{result=ok|error}, ba_authd_invite_total{result=ok|error}.
  • docker-compose authd service up, healthy.
  • cmd/authd/README.md with rotation playbook.

Estimated: 3-4 days.


W2: web/ skeleton

Goal: Vite + React 19 + TypeScript + Tailwind + shadcn/ui + pnpm workspace. Empty shell, login page, top bar, sidebar, routing, theme toggle. Real auth integration with authd.

Scope:

  • web/package.json (pnpm workspace member), pnpm-workspace.yaml.
  • web/vite.config.ts (proxy /v1/auth/* to http://localhost:8804 in dev; other /v1/* to :8803).
  • web/src/main.tsx, App.tsx.
  • web/src/routes/ (React Router v6):
    • /login
    • /forbidden
    • / (redirect to /companies)
    • /companies/* — "coming soon" placeholder
    • /sources/* — "coming soon"
    • /telegram/* — "coming soon"
    • /tail — "coming soon"
    • /dlq/* — "coming soon"
    • /audit — "coming soon"
    • * — 404 page
  • web/src/lib/auth.ts — token storage in memory + refresh cookie handling + auto-refresh on 401.
  • web/src/lib/api.ts — openapi-fetch client with refresh-on-401 interceptor.
  • web/src/components/ui/ — shadcn/ui init (Button, Input, Form, Toast/Sonner, DropdownMenu, Avatar, Sheet, Skeleton, Table, Dialog, Tooltip).
  • web/src/components/layout/ — TopBar (logo + company switcher placeholder + user menu), Sidebar (Companies, Sources, Telegram, Live Tail, DLQ, Audit Log), AppShell.
  • web/src/i18n/en.ts — single object, no i18next yet.
  • web/tests/setup.ts, vitest config.
  • Makefile target: make web-build → outputs to cmd/admind/web/dist/.
  • Makefile target: make web-dev → runs vite dev server on :5173.

Exit criteria:

  • pnpm install works in web/.
  • pnpm --filter web run dev serves on :5173.
  • pnpm --filter web run build produces web/dist/.
  • Login page renders, talks to authd (real network), stores access token in memory + refresh in httpOnly cookie.
  • Top bar + sidebar render with active state.
  • Dark mode toggle works.
  • "Coming soon" placeholders render for all 6 modules.
  • 404 page renders.
  • TypeScript strict mode, no any in committed code.
  • Bundle size: web/dist/ < 500 KB gzipped (without feature code, this is just shell + shadcn primitives).

Estimated: 2 days.


W3: admind JWT gate

Goal: Make admind require a valid JWT on every /v1/* endpoint (except the 5 public ones). Share BA_AUTH_JWT_SECRET with authd for HS256 verification. Add scope-based authorization (super-admin vs tenant-admin).

Scope:

  • internal/auth/verifier.go (new package):
    • Verifier interface.
    • HS256Verifier (default).
    • JWKSVerifier (stub, future).
    • Claims struct (sub, role, company_id, iat, exp).
  • cmd/admind/main.go:
    • Wire Verifier from env (BA_AUTH_JWT_SECRET, BA_AUTH_ASYMMETRIC).
    • Add authMiddleware that reads Authorization: Bearer ***, verifies, injects claims into request context.
    • Apply to all /v1/* except: /v1/auth/* (passthrough for login/refresh), /health, /metrics.
    • Add forbiddenHandler for 403 with consistent error JSON.
  • internal/httpserver/middleware.go (if it exists; otherwise add to cmd/admind/main.go): the auth middleware.
  • internal/dlq/dlq.go — every query adds WHERE company_id = $1 for tenant-admin (read from JWT context).
  • internal/audit/audit.go — every state change writes a row with the actor's user_id from JWT.
  • internal/config/config.go — read BA_AUTH_JWT_SECRET, BA_AUTH_ASYMMETRIC env vars.

Exit criteria:

  • Without Authorization header → 401 on all /v1/* except /v1/auth/*, /health, /metrics.
  • With valid super-admin JWT → 200 on everything.
  • With tenant-admin JWT, request scoped to their company_id:
    • GET /v1/companies/<their-id> → 200
    • GET /v1/companies/<other-id> → 403
  • HS256Verifier and JWKSVerifier both compile, controlled by env.
  • /openapi.json (new in M13a) lists every endpoint with the auth scheme.
  • M8 DLQ endpoints still work (same paths, now JWT-gated).
  • Audit log: state-changing calls write actor from JWT.

Estimated: 1-2 days (touches existing admind code; risk of regression; needs careful testing of the M8 DLQ flow).


W4: docker-compose + env + bootstrap + smoke

Goal: Full stack runs end-to-end locally. One smoke test that boots everything, logs in via the UI, hits /v1/auth/me, and verifies a 401 from a wrong token.

Scope:

  • docker-compose.yml — add authd service, wire env vars.
  • scripts/bootstrap_admin.sh — creates the first super-admin user via psql with a known password, prints the credentials.
  • scripts/m13a_smoke.sh — bash + curl:
    1. Bring up stack (docker compose up -d).
    2. Bootstrap super-admin.
    3. POST /v1/auth/login with creds → expect 200 + tokens.
    4. GET /v1/auth/me with token → expect 200 + super_admin.
    5. GET /v1/dlq (any) without token → expect 401.
    6. GET /v1/dlq with token → expect 200.
    7. Logout → refresh token revoked.
  • Makefile target: make m13a-smoke runs the above.
  • M13a_VERIFICATION.md — captures the smoke output, links to W1-W3 exit criteria.

Exit criteria:

  • make m13a-smoke exits 0 from a clean state.
  • 3 consecutive green runs.
  • Bootstrap script is idempotent (running twice doesn't create a duplicate admin).
  • All M8 functionality still works (DLQ list/replay/discard).
  • M13a_VERIFICATION.md published.

Estimated: 0.5 day.


3. Sequencing

W1 (3-4d) ──▶ W2 (2d) ──┬──▶ W3 (1-2d) ──▶ W4 (0.5d)
                         │
                         └── W3 can start as soon as W1 has
                             /v1/auth/login + /v1/auth/me green
                             (it doesn't need refresh/invite yet)
  • W1 alone: 3-4 days.
  • W2 after W1 login/me: 2 days.
  • W3: 1-2 days.
  • W4: 0.5 day.
  • Total: ~6.5-8.5 days, target 5-6 with overlap.

4. Risks specific to M13a

Risk Likelihood Impact Mitigation
W3 regression on M8 DLQ Medium High Smoke covers M8 paths; revert path is git revert of the W3 commit
argon2id parameters slow login Low Low Use argon2id recommended params (time=1, memory=64MB, threads=2); benchmark in W1
Refresh token race (two tabs) Medium Low Token rotation is atomic; loser gets 401 + must re-login. Documented.
BA_AUTH_JWT_SECRET not set in dev Low Low authd refuses to start with a clear error; .env.example ships a dev value
pnpm workspace + Go embed.FS interaction Low Low Build copies web/dist/ to cmd/admind/web/dist/ via Makefile, no symlinks

5. Definition of done — M13a

  • cmd/authd/ exists, builds, runs, healthy on :8804.
  • web/ builds with pnpm --filter web run build, output lands in cmd/admind/web/dist/.
  • cmd/admind/main.go requires JWT on all /v1/* (except /v1/auth/*, /health, /metrics).
  • Login flow works end-to-end (curl and SPA).
  • make m13a-smoke green for 3 consecutive runs.
  • M8 DLQ functionality not regressed (smoke covers it).
  • M13a_VERIFICATION.md published with the smoke log and screenshots of the SPA shell (login page, top bar, sidebar, "coming soon" pages).
  • SPEC.md M13a row flipped to ✅ shipped YYYY-MM-DD.

Next step: start W1 (authd). Once W1's /v1/auth/login and /v1/auth/me work, W2 (web skeleton) and W3 (admind JWT gate) can start.