mirror of
https://github.com/Hackdex-App/hackdex-website.git
synced 2026-08-22 08:34:13 -05:00
Remove invite code requirement for signup
This commit is contained in:
@@ -3,9 +3,8 @@
|
||||
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 { createClient } from '@/utils/supabase/server'
|
||||
import { validateEmail, validatePassword } from '@/utils/auth'
|
||||
import { sendDiscordMessageEmbed } from '@/utils/discord'
|
||||
|
||||
@@ -34,13 +33,11 @@ export interface AuthActionState {
|
||||
|
||||
export async function signup(state: AuthActionState, payload: FormData) {
|
||||
const supabase = await createClient()
|
||||
const service = await createServiceClient()
|
||||
|
||||
const data = {
|
||||
email: payload.get('email') as string,
|
||||
password: payload.get('password') as string,
|
||||
}
|
||||
const inviteCode = (payload.get('inviteCode') as string | null)?.trim() || ''
|
||||
|
||||
const { error: emailError } = validateEmail(data.email);
|
||||
if (emailError) {
|
||||
@@ -52,33 +49,6 @@ export async function signup(state: AuthActionState, payload: FormData) {
|
||||
return { error: passwordError };
|
||||
}
|
||||
|
||||
if (!inviteCode) {
|
||||
return { error: 'An invite code is required to sign up.' }
|
||||
}
|
||||
|
||||
// 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 (!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)
|
||||
|
||||
if (error) {
|
||||
@@ -86,52 +56,17 @@ export async function signup(state: AuthActionState, payload: FormData) {
|
||||
}
|
||||
|
||||
const userId = signUpResult.user?.id || null
|
||||
if (isStaticInvite) {
|
||||
console.log('[signup] Static invite code used:', { inviteCode, userId })
|
||||
if (process.env.DISCORD_WEBHOOK_ADMIN_URL) {
|
||||
await sendDiscordMessageEmbed(process.env.DISCORD_WEBHOOK_ADMIN_URL, [
|
||||
{
|
||||
title: 'New User Signup',
|
||||
description: `A new user (\`${userId}\`) has signed up using the static invite code: \`${inviteCode}\``,
|
||||
color: 0x40f56a,
|
||||
footer: {
|
||||
text: 'A notification will be sent when this user has created their profile'
|
||||
}
|
||||
},
|
||||
]);
|
||||
}
|
||||
} 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 {}
|
||||
}
|
||||
return { error: 'Invite code is no longer available. Please try again.' }
|
||||
} else {
|
||||
if (process.env.DISCORD_WEBHOOK_ADMIN_URL) {
|
||||
await sendDiscordMessageEmbed(process.env.DISCORD_WEBHOOK_ADMIN_URL, [
|
||||
{
|
||||
title: 'New User Signup',
|
||||
description: `A new user (\`${userId}\`) has signed up using the invite code: \`${inviteCode}\``,
|
||||
color: 0x40f56a,
|
||||
footer: {
|
||||
text: 'A notification will be sent when this user has created their profile'
|
||||
}
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
if (process.env.DISCORD_WEBHOOK_ADMIN_URL) {
|
||||
await sendDiscordMessageEmbed(process.env.DISCORD_WEBHOOK_ADMIN_URL, [
|
||||
{
|
||||
title: 'New User Signup',
|
||||
description: `A new user (\`${userId}\`) has signed up.`,
|
||||
color: 0x40f56a,
|
||||
footer: {
|
||||
text: 'A notification will be sent when this user has created their profile'
|
||||
}
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
revalidatePath('/', 'layout');
|
||||
|
||||
@@ -14,14 +14,13 @@ export default function SignupForm() {
|
||||
const [email, setEmail] = React.useState("");
|
||||
const [password, setPassword] = React.useState("");
|
||||
const [confirm, setConfirm] = React.useState("");
|
||||
const [invite, setInvite] = React.useState<string>("");
|
||||
const [showPassword, setShowPassword] = React.useState(false);
|
||||
const [emailError, setEmailError] = React.useState<string | null>(null);
|
||||
const [passwordError, setPasswordError] = React.useState<string | null>(null);
|
||||
|
||||
const [state, formAction, isPending] = useActionState<AuthActionState, FormData>(signup, { error: null });
|
||||
const passwordsMatch = password === confirm;
|
||||
const isValid = !emailError && !passwordError && passwordsMatch && Boolean(invite);
|
||||
const isValid = !emailError && !passwordError && passwordsMatch;
|
||||
|
||||
useEffect(() => {
|
||||
const { error } = validateEmail(email);
|
||||
@@ -35,13 +34,6 @@ export default function SignupForm() {
|
||||
|
||||
const redirectTo = searchParams.get("redirectTo");
|
||||
|
||||
useEffect(() => {
|
||||
const inviteFromParams = searchParams.get("invite") || "";
|
||||
if (inviteFromParams) {
|
||||
setInvite(inviteFromParams);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Redirect if user already authenticated (e.g., opened signup while logged in)
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
@@ -60,27 +52,6 @@ export default function SignupForm() {
|
||||
{state?.error}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="inviteCode" className="text-sm text-foreground/80">Invite code</label>
|
||||
<input
|
||||
id="inviteCode"
|
||||
name="inviteCode"
|
||||
type="text"
|
||||
value={invite}
|
||||
onChange={(e) => setInvite(e.target.value)}
|
||||
placeholder="Enter your invite code"
|
||||
className={`h-11 rounded-md bg-[var(--surface-2)] px-3 text-sm ring-1 ring-inset ring-[var(--border)] focus:outline-none focus:ring-2 focus:ring-[var(--ring)] ${
|
||||
invite ? "bg-[var(--surface-2)] ring-[var(--border)]" : "bg-[var(--surface-2)] ring-[var(--border)]"
|
||||
}`}
|
||||
required
|
||||
inputMode="text"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
{!invite && (
|
||||
<span className="text-xs text-foreground/60">An invite code is required to create an account.</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="email" className="text-sm text-foreground/80">Email</label>
|
||||
<input
|
||||
|
||||
Reference in New Issue
Block a user