From bfae105e5f00ded721abda3f2f2d1671c97b89c7 Mon Sep 17 00:00:00 2001 From: diced Date: Sat, 6 Sep 2025 16:29:24 -0700 Subject: [PATCH] fix: invites not working --- src/client/pages/auth/register.tsx | 55 ++++++++++++------ src/lib/api/response.ts | 2 + src/server/routes/api/auth/invites/web.ts | 69 +++++++++++------------ 3 files changed, 72 insertions(+), 54 deletions(-) diff --git a/src/client/pages/auth/register.tsx b/src/client/pages/auth/register.tsx index 3b392527..b3efedb6 100644 --- a/src/client/pages/auth/register.tsx +++ b/src/client/pages/auth/register.tsx @@ -15,7 +15,7 @@ import { Title, } from '@mantine/core'; import { useForm } from '@mantine/form'; -import { notifications } from '@mantine/notifications'; +import { notifications, showNotification } from '@mantine/notifications'; import { IconLogin, IconPlus, IconUserPlus, IconX } from '@tabler/icons-react'; import { useEffect, useState } from 'react'; import { Link, redirect, useLocation, useNavigate } from 'react-router-dom'; @@ -30,7 +30,6 @@ export function Component() { const navigate = useNavigate(); const [loading, setLoading] = useState(true); - const [invite, setInvite] = useState(null); const { data: config, @@ -44,6 +43,19 @@ export function Component() { }); const code = new URLSearchParams(location.search).get('code') ?? undefined; + const { + data: invite, + error: inviteError, + isLoading: inviteLoading, + } = useSWR( + location.search.includes('code') ? `/api/auth/invites/web${location.search}` : null, + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenHidden: false, + revalidateIfStale: false, + }, + ); const form = useForm({ initialValues: { @@ -69,20 +81,8 @@ export function Component() { }, []); useEffect(() => { - (async () => { - if (!code) return; + if (!config) return; - const res = await fetch(`/api/auth/invite/web?code=${code}`); - if (res.ok) { - const json = await res.json(); - setInvite(json.invite); - } else { - redirect('/auth/login'); - } - })(); - }, [code]); - - useEffect(() => { if (!config?.features.userRegistration) { navigate('/auth/login'); } @@ -138,6 +138,22 @@ export function Component() { ); } + if (code && inviteError) { + if (inviteError) { + showNotification({ + id: 'invalid-invite', + message: 'Invalid or expired invite.', + color: 'red', + }); + + navigate('/auth/login'); + + return null; + } + + if (inviteLoading) return ; + } + return (
{config.website.loginBackground && ( @@ -183,8 +199,13 @@ export function Component() { {invite && ( - You’ve been invited to join {config?.website?.title ?? 'Zipline'} by{' '} - {invite.inviter?.username} + You’ve been invited to join {config?.website?.title ?? 'Zipline'} + {invite.inviter && ( + <> + {' '} + by {invite.inviter.username} + + )} )} diff --git a/src/lib/api/response.ts b/src/lib/api/response.ts index 37ce3da0..57bfb70d 100755 --- a/src/lib/api/response.ts +++ b/src/lib/api/response.ts @@ -1,5 +1,6 @@ import { ApiAuthInvitesResponse } from '@/server/routes/api/auth/invites'; import { ApiAuthInvitesIdResponse } from '@/server/routes/api/auth/invites/[id]'; +import { ApiAuthInvitesWebResponse } from '@/server/routes/api/auth/invites/web'; import { ApiLoginResponse } from '@/server/routes/api/auth/login'; import { ApiLogoutResponse } from '@/server/routes/api/auth/logout'; import { ApiAuthOauthResponse } from '@/server/routes/api/auth/oauth'; @@ -45,6 +46,7 @@ import { ApiVersionResponse } from '@/server/routes/api/version'; export type Response = { '/api/auth/invites/[id]': ApiAuthInvitesIdResponse; '/api/auth/invites': ApiAuthInvitesResponse; + '/api/auth/invites/web': ApiAuthInvitesWebResponse; '/api/auth/register': ApiAuthRegisterResponse; '/api/auth/webauthn': ApiAuthWebauthnResponse; '/api/auth/oauth': ApiAuthOauthResponse; diff --git a/src/server/routes/api/auth/invites/web.ts b/src/server/routes/api/auth/invites/web.ts index 2d10df3a..1d322929 100644 --- a/src/server/routes/api/auth/invites/web.ts +++ b/src/server/routes/api/auth/invites/web.ts @@ -1,60 +1,55 @@ import { config } from '@/lib/config'; import { prisma } from '@/lib/db'; import { Invite } from '@/lib/db/models/invite'; -import { log } from '@/lib/logger'; import { secondlyRatelimit } from '@/lib/ratelimits'; -import { administratorMiddleware } from '@/server/middleware/administrator'; -import { userMiddleware } from '@/server/middleware/user'; import fastifyPlugin from 'fastify-plugin'; -export type ApiAuthInvitesResponse = Invite | Invite[]; +export type ApiAuthInvitesWebResponse = Invite & { + inviter: { + username: string; + }; +}; type Query = { code: string; }; -const logger = log('api').c('auth').c('invites').c('web'); - export const PATH = '/api/auth/invites/web'; export default fastifyPlugin( (server, _, done) => { - server.get<{ Querystring: Query }>( - PATH, - { preHandler: [userMiddleware, administratorMiddleware], ...secondlyRatelimit(10) }, - async (req, res) => { - const { code } = req.query; + server.get<{ Querystring: Query }>(PATH, secondlyRatelimit(10), async (req, res) => { + const { code } = req.query; - if (!code) return res.send({ invite: null }); - if (!config.invites.enabled) return res.notFound(); + if (!code) return res.send({ invite: null }); + if (!config.invites.enabled) return res.notFound(); - const invite = await prisma.invite.findFirst({ - where: { - OR: [{ id: code }, { code }], + const invite = await prisma.invite.findFirst({ + where: { + OR: [{ id: code }, { code }], + }, + select: { + code: true, + maxUses: true, + uses: true, + expiresAt: true, + inviter: { + select: { username: true }, }, - select: { - code: true, - maxUses: true, - uses: true, - expiresAt: true, - inviter: { - select: { username: true }, - }, - }, - }); + }, + }); - if ( - !invite || - (invite.expiresAt && new Date(invite.expiresAt) < new Date()) || - (invite.maxUses && invite.uses >= invite.maxUses) - ) { - return res.notFound(); - } + if ( + !invite || + (invite.expiresAt && new Date(invite.expiresAt) < new Date()) || + (invite.maxUses && invite.uses >= invite.maxUses) + ) { + return res.notFound(); + } - delete (invite as any).expiresAt; + delete (invite as any).expiresAt; - return res.send({ invite }); - }, - ); + return res.send({ invite }); + }); done(); },