Add a real signup path instead of the accidental one

SAAS_PLAN.md 2.2: there was no /signup page. The only self-serve path was
Google sign-in on /login, which auto-provisions a Firebase account with no
role or org claim at all - previously that meant "viewer role, full read
access" the moment the AuthProvider cookie logic (previous commit) let it
through. That's closed now regardless; this commit is the other side of it
- giving people an actual way in.

app/signup/page.tsx: email/password (createUserWithEmailAndPassword) or
Google, same visual language as /login. It only creates the Firebase
account - org naming is deliberately not on this page, so every path that
produces an account with no org (this one, and Google-via-/login) converges
on the same next screen.

app/onboarding/page.tsx: that screen. Shown to any signed-in user with no
orgId (ChromeSwitcher's redirect, previous commit), collects an org name,
calls the new c2api.signup() -> POST /auth/signup (routers/links.py,
already shipped), then refreshClaims() to force-refetch the ID token so
orgId picks up immediately and the same redirect effect sends them on to
/dashboard - no manual reload needed.

lib/c2api.ts also gained getOrg/updateOrg and the enrollment-token
mint/list/revoke calls (routers/org.py, already shipped on the backend)
and joinWaitlist (routers/waitlist.py) - none consumed yet, wired in ahead
of the settings/legal commits that use them so this stays one add per
concept rather than scattering client additions across later commits.

/login gained a "Don't have an account? Sign up" link to /signup. This is
signup plumbing, not marketing copy - pricing/plan copy (app/pricing,
lib/billing.ts) is untouched in this pass, that's a separate, still-open
decision (SAAS_PLAN.md section 6).

Typecheck: clean (tsc --noEmit via the WSL-native ~/drb-frontend copy).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-08-18 20:34:04 -04:00
co-authored by Claude Opus 5
parent c7f985df42
commit 2a1d52b7af
4 changed files with 251 additions and 0 deletions
+31
View File
@@ -222,4 +222,35 @@ export const c2api = {
// Session recording — called on each explicit sign-in
recordSession: () =>
request<{ ok: boolean }>("/auth/session", { method: "POST" }),
// Org provisioning (SAAS_PLAN.md B4) — called once from /onboarding right
// after a Firebase account exists but before it has an org_id claim.
signup: (orgName: string) =>
request<{ org_id: string; org_name: string; already_provisioned: boolean }>("/auth/signup", {
method: "POST",
body: JSON.stringify({ org_name: orgName }),
}),
// Organization profile
getOrg: () =>
request<{ org_id: string; name: string; created_at: string }>("/org"),
updateOrg: (name: string) =>
request<{ ok: boolean; name: string }>("/org", { method: "PATCH", body: JSON.stringify({ name }) }),
// Per-org enrollment tokens (SAAS_PLAN.md B2b)
listEnrollmentTokens: () =>
request<{ token_id: string; label: string; created_at: string; revoked: boolean; uses: number }[]>(
"/org/enrollment-tokens"
),
mintEnrollmentToken: (label: string) =>
request<{ token_id: string; token: string; label: string }>("/org/enrollment-tokens", {
method: "POST",
body: JSON.stringify({ label }),
}),
revokeEnrollmentToken: (tokenId: string) =>
request(`/org/enrollment-tokens/${tokenId}`, { method: "DELETE" }),
// Public waitlist — no auth, see routers/waitlist.py
joinWaitlist: (body: { email: string; org_name?: string; note?: string }) =>
request<{ ok: boolean }>("/waitlist", { method: "POST", body: JSON.stringify(body) }),
};