Repository navigation
v0.2.0 — Phase 2: Brand Dashboard
Phase 2 — shipped
Auth
POST /auth/register— creates brand (auto-slug) + first user, hashes password with argon2id, signs a session JWT.POST /auth/login— argon2id verify; constant-time dummy verify when user is missing (closes user-existence timing side channel).POST /auth/logout— clears the cookie.GET /auth/me— returns current user + brand, behind a session-cookie middleware.- JWT: HS256, 24h expiry, no refresh tokens.
- Cookie:
gf_session,HttpOnly; SameSite=Lax; Path=/; Max-Age=86400+Secure(gated byCOOKIE_SECUREenv, off for local HTTP dev). - Composed
/v1/*middleware accepts the JWT cookie orX-API-Key— dashboard and machine integrations share one route surface.
Dashboard pages
All pages dark-by-default, Tailwind only, no component library, DM Serif Display + DM Sans, accent #1D9E75:
/dashboard— product/event counts + last anchor + recent events/products— paginated list (50/page)/products/new— register product (name, external_ref, JSON metadata)/products/[id]— product card + provenance timeline (anchored vs pending dot)/api-keys— list, create (plain key shown once), revoke/anchors— anchor batch log with Basescan tx links/settings— brand + account read-only view/verify/[product_id]— public, mobile-optimized, no auth required
Next.js proxy (no CORS)
next.config.mjs rewrites /api/:path* to ${INTERNAL_API_URL ?? "http://localhost:8080"}/:path*. The browser only ever talks to the Next host, so the session cookie stays first-party. lib/api.ts::apiBase() resolves /api in the browser and the absolute backend URL on the server (server components can't proxy through themselves).
Per-brand anchor hash projection
New table anchor_brand_hashes(anchor_batch_id, brand_id, brand_hash) carries a SHA-256 over only that brand's events in the batch. After every confirmation (fresh submit and recovery-confirmed pending alike), the poller groups events by brand and inserts one row per (batch, brand) pair. Every endpoint that previously surfaced the global records_hash — GET /v1/anchors, GET /v1/dashboard.last_anchor, GET /verify/:id.latest_anchor — now returns BrandAnchorView with the brand-scoped brand_hash. Two brands sharing a batch can no longer correlate their anchor activity by comparing hashes.
Backend endpoints added for the dashboard
GET /v1/dashboard— aggregate metricsGET /v1/products— paginated list (limit,offset, clamped to 200)GET /v1/products/:id/events— chronological event list, tenant-scopedGET /v1/keys— list keys for the authenticated brandGET /v1/anchors— paginatedBrandAnchorViewlist
Migrations
0004_users.sql—userstable withbrand_idFK0005_anchor_brand_hashes.sql— per-brand hash projection withON DELETE CASCADEfrom bothanchor_batchesandbrands
Standards
#![deny(warnings)] workspace-wide, no unwrap/expect/println in production paths, tracing throughout, cargo clippy --workspace -- -D warnings clean. tsc --noEmit + next build clean across 12 routes + middleware.