M2: invite-only access flow — issuance, redemption, role gates - #3
Merged
Conversation
- 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
# Conflicts: # STATE.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
M2 — Invite-only access flow
What
Atelier is now invite-only end to end:
InviteCode(code,role,maxUses,usedCount,expiresAt,createdById) andInviteRedemptiontables.POST /api/admin/invites(ADMIN-only) generates unambiguous codes (ATELIER-XXXXXX, no 0/O/1/I/L), configurable role / uses / expiry / count;GETlists them; new/admin/invitespage with a copy-ready issue form + usage list.POST /api/invites/redeemvalidates code/expiry and claims atomically (updateManyonusedCount < 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.src/lib/auth.ts) refuses to send a link to any email without an invited user row (sendVerificationRequestthrows 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./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
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/invites.spec.ts— admin issuance, non-admin 403, redemption creates user with role + counts usage (DB-verified viae2e/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.npm run buildpasses (was broken on main — see below).npx tsc --noEmitandtsconfig.build.jsonclean; lint adds zero new errors (91 pre-existingno-explicit-anyremain, unchanged from main).Incidental fix (blocker): production build broken on main
npm run buildfailed on clean main:new Stripe(process.env.STRIPE_SECRET_KEY!, …)at module scope in 5 files throwsNeither apiKey nor config.authenticator providedwhen Next collects route data, and CI/Docker builds set no key. Stripe is now lazily constructed viagetStripe()(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
db pushonly — there's noprisma/migrations/directory, sonpx prisma migrate deployin both deploy workflows is a silent no-op. Before staging deploys, a migration baseline (or switching the deploy steps todb push) is needed, or schema changes like this M2 one won't reach the DB.docs/P1.3-provisioning.mdhas the commands./admin/invites. Seed includesATELIER-E2EINVITE(100 uses) for smoke tests.