Skip to content

M2: invite-only access flow — issuance, redemption, role gates - #3

Merged
mattdani21 merged 2 commits into
mainfrom
autopilot/m2-invite-only
Aug 7, 2026
Merged

M2: invite-only access flow — issuance, redemption, role gates#3
mattdani21 merged 2 commits into
mainfrom
autopilot/m2-invite-only

Conversation

@mattdani21

Copy link
Copy Markdown
Owner

M2 — Invite-only access flow

What

Atelier is now invite-only end to end:

  1. Data modelInviteCode (code, role, maxUses, usedCount, expiresAt, createdById) and InviteRedemption tables.
  2. Admin issuancePOST /api/admin/invites (ADMIN-only) generates unambiguous codes (ATELIER-XXXXXX, no 0/O/1/I/L), configurable role / uses / expiry / count; GET lists them; new /admin/invites page with a copy-ready issue form + usage list.
  3. Redemption — public POST /api/invites/redeem validates code/expiry and claims atomically (updateMany on usedCount < maxUses, so concurrent redemptions can't oversell). It creates the user with the invited role, or upgrades the existing role — never downgrades, ADMIN is absolute.
  4. Sign-in gate — the magic-link provider (src/lib/auth.ts) refuses to send a link to any email without an invited user row (sendVerificationRequest throws for unknown/GUEST emails). The sign-in page redeems the code first, so uninvited users get a clear inline rejection before any email is attempted.
  5. Role gates — middleware: /dashboard = MAKER/CURATOR/ADMIN only, purchase surfaces (/cart, /orders, /commissions) = any non-GUEST; API: guestDenied() helper wired into cart, checkout, orders, commissions, and applications routes (401 unauth / 403 GUEST). Browsing stays public by design.

Why

M2 in GOAL.md: no one can register without an invite; role gates on purchase and maker surfaces.

How tested

  • Unit (12 new, 75 total): src/lib/__tests__/invites.test.ts — code format/uniqueness, redemption happy path, invalid/expired/exhausted rejection, atomic claim guard, claim rollback on failure, upgrade semantics.
  • E2E (7 new, 29 total): e2e/invites.spec.ts — admin issuance, non-admin 403, redemption creates user with role + counts usage (DB-verified via e2e/setup/db.ts), invalid/expired/exhausted rejections, GUEST blocked from cart (403), MAKER invite upgrades an existing GUEST, sign-in page UX (bad code rejected inline, good code accepted). Full suite: 29/29 green locally against Postgres 16.
  • Build: npm run build passes (was broken on main — see below).
  • npx tsc --noEmit and tsconfig.build.json clean; lint adds zero new errors (91 pre-existing no-explicit-any remain, unchanged from main).

Incidental fix (blocker): production build broken on main

npm run build failed on clean main: new Stripe(process.env.STRIPE_SECRET_KEY!, …) at module scope in 5 files throws Neither apiKey nor config.authenticator provided when Next collects route data, and CI/Docker builds set no key. Stripe is now lazily constructed via getStripe() (src/lib/stripe.ts); requests without a key still fail fast with a clear error. This unblocks CI on every PR and the Docker image builds in the deploy workflows.

Notes for Matt

  • Schema is db push only — there's no prisma/migrations/ directory, so npx prisma migrate deploy in both deploy workflows is a silent no-op. Before staging deploys, a migration baseline (or switching the deploy steps to db push) is needed, or schema changes like this M2 one won't reach the DB.
  • P1.3 provisioning (GCP services + secrets) still blocks M1's staging deploy; docs/P1.3-provisioning.md has the commands.
  • After deploy, admins issue codes at /admin/invites. Seed includes ATELIER-E2EINVITE (100 uses) for smoke tests.

- InviteCode/InviteRedemption models; admin issuance API + /admin/invites UI
- Public redemption with atomic maxUses guard; role upgrades only (never downgrade, ADMIN absolute)
- Magic-link provider gate: refuses to email addresses without an invited user
- Role gates: middleware (dashboard = maker/staff, purchase surfaces = non-GUEST) + guestDenied() in purchase/maker API routes
- Sign-in page redeems the code before requesting the magic link
- 7 new E2E tests (29/29 total), 12 new unit tests (75 total)
- Fix: npm run build broken on main — Stripe at module scope; now lazy via getStripe()
- GOAL.md M2 checked; STATE.md updated
@mattdani21
mattdani21 merged commit 59505a5 into main Aug 7, 2026
1 of 2 checks passed
@mattdani21
mattdani21 deleted the autopilot/m2-invite-only branch August 7, 2026 06:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant