Add support for static invite codes through edge config

This commit is contained in:
Jared Schoeny 2025-11-05 23:25:35 -10:00
parent b7220ca057
commit b5857bda4c
3 changed files with 72 additions and 24 deletions

31
package-lock.json generated
View File

@ -16,6 +16,7 @@
"@supabase/ssr": "^0.7.0",
"@supabase/supabase-js": "^2.74.0",
"@types/serialize-javascript": "^5.0.4",
"@vercel/edge-config": "^1.4.3",
"chart.js": "^4.5.1",
"embla-carousel-react": "8.6.0",
"minio": "^8.0.6",
@ -1483,6 +1484,36 @@
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"license": "ISC"
},
"node_modules/@vercel/edge-config": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@vercel/edge-config/-/edge-config-1.4.3.tgz",
"integrity": "sha512-8vTDATodRrH49wMzKEjZ8/5H2qs1aPkD0uRK585f/Fx4YN2wfHfY/3td9OFrh+gdnCq07z8A5f0hoY6xhBcPkg==",
"license": "Apache-2.0",
"dependencies": {
"@vercel/edge-config-fs": "0.1.0"
},
"engines": {
"node": ">=14.6"
},
"peerDependencies": {
"@opentelemetry/api": "^1.7.0",
"next": ">=1"
},
"peerDependenciesMeta": {
"@opentelemetry/api": {
"optional": true
},
"next": {
"optional": true
}
}
},
"node_modules/@vercel/edge-config-fs": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@vercel/edge-config-fs/-/edge-config-fs-0.1.0.tgz",
"integrity": "sha512-NRIBwfcS0bUoUbRWlNGetqjvLSwgYH/BqKqDN7vK1g32p7dN96k0712COgaz6VFizAm9b0g6IG6hR6+hc0KCPg==",
"license": "Apache-2.0"
},
"node_modules/@webassemblyjs/ast": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",

View File

@ -17,6 +17,7 @@
"@supabase/ssr": "^0.7.0",
"@supabase/supabase-js": "^2.74.0",
"@types/serialize-javascript": "^5.0.4",
"@vercel/edge-config": "^1.4.3",
"chart.js": "^4.5.1",
"embla-carousel-react": "8.6.0",
"minio": "^8.0.6",

View File

@ -3,6 +3,7 @@
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { AuthError } from '@supabase/supabase-js'
import { get } from '@vercel/edge-config'
import { createClient, createServiceClient } from '@/utils/supabase/server'
import { validateEmail, validatePassword } from '@/utils/auth'
@ -54,16 +55,27 @@ export async function signup(state: AuthActionState, payload: FormData) {
return { error: 'An invite code is required to sign up.' }
}
// Pre-check: ensure invite exists and is unused before attempting signup
const { data: availableInvite, error: inviteCheckError } = await service
.from('invite_codes')
.select('code')
.eq('code', inviteCode)
.is('used_by', null)
.maybeSingle()
// Allow static invite codes via Edge Config to bypass DB checks
let isStaticInvite = false
try {
const staticCodes = (await get<string[] | null>('staticInviteCodes')) || []
if (Array.isArray(staticCodes)) {
isStaticInvite = staticCodes.includes(inviteCode)
}
} catch {}
if (inviteCheckError || !availableInvite) {
return { error: 'Invalid or already used invite code.' }
if (!isStaticInvite) {
// Pre-check: ensure invite exists and is unused before attempting signup
const { data: availableInvite, error: inviteCheckError } = await service
.from('invite_codes')
.select('code')
.eq('code', inviteCode)
.is('used_by', null)
.maybeSingle()
if (inviteCheckError || !availableInvite) {
return { error: 'Invalid or already used invite code.' }
}
}
const { data: signUpResult, error } = await supabase.auth.signUp(data)
@ -73,23 +85,27 @@ export async function signup(state: AuthActionState, payload: FormData) {
}
const userId = signUpResult.user?.id || null
// Finalize: set used_by to the new user id iff still unused (atomic)
const { data: finalized, error: finalizeError } = await service
.from('invite_codes')
.update({ used_by: userId ?? null })
.eq('code', inviteCode)
.is('used_by', null)
.select('code')
.maybeSingle()
if (isStaticInvite) {
console.log('[signup] Static invite code used:', { inviteCode, userId })
} else {
// Finalize: set used_by to the new user id iff still unused (atomic)
const { data: finalized, error: finalizeError } = await service
.from('invite_codes')
.update({ used_by: userId ?? null })
.eq('code', inviteCode)
.is('used_by', null)
.select('code')
.maybeSingle()
if (finalizeError || !finalized) {
// The code claim could not be finalized (race). Roll back user creation.
if (userId) {
try {
await service.auth.admin.deleteUser(userId)
} catch {}
if (finalizeError || !finalized) {
// The code claim could not be finalized (race). Roll back user creation.
if (userId) {
try {
await service.auth.admin.deleteUser(userId)
} catch {}
}
return { error: 'Invite code is no longer available. Please try again.' }
}
return { error: 'Invite code is no longer available. Please try again.' }
}
revalidatePath('/', 'layout');