Skip to content

v0.2.0 — Phase 2: Brand Dashboard

Choose a tag to compare

@ChronoCoders ChronoCoders released this 02 Oct 23:05
· 13 commits to main since this release

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 by COOKIE_SECURE env, off for local HTTP dev).
  • Composed /v1/* middleware accepts the JWT cookie or X-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 metrics
  • GET /v1/products — paginated list (limit, offset, clamped to 200)
  • GET /v1/products/:id/events — chronological event list, tenant-scoped
  • GET /v1/keys — list keys for the authenticated brand
  • GET /v1/anchors — paginated BrandAnchorView list

Migrations

  • 0004_users.sql — users table with brand_id FK
  • 0005_anchor_brand_hashes.sql — per-brand hash projection with ON DELETE CASCADE from both anchor_batches and brands

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.