fix: invites not working

This commit is contained in:
diced
2025-09-06 16:29:24 -07:00
parent 3240e19710
commit bfae105e5f
3 changed files with 72 additions and 54 deletions

View File

@@ -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<any>(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<Response['/api/auth/invites/web']>(
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 <LoadingOverlay visible />;
}
return (
<Center h='100vh'>
{config.website.loginBackground && (
@@ -183,8 +199,13 @@ export function Component() {
{invite && (
<Text ta='center' size='sm' c='dimmed'>
Youve been invited to join <b>{config?.website?.title ?? 'Zipline'}</b> by{' '}
<b>{invite.inviter?.username}</b>
Youve been invited to join <b>{config?.website?.title ?? 'Zipline'}</b>
{invite.inviter && (
<>
{' '}
by <b>{invite.inviter.username}</b>
</>
)}
</Text>
)}

View File

@@ -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;

View File

@@ -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();
},