import { and, eq, gt, lt, lte, or, sql } from 'drizzle-orm'; import { invites, users } from '../../drizzle/schema'; import { db } from '../lib/db'; export const INVITE_TOKEN_HEADER = 'x-invite-token'; export const INVITE_LIFETIME_MS = 7 * 24 * 60 * 60 * 1000; // still has uses left and has not run out const usable = () => and(lt(invites.uses, invites.maxUses), gt(invites.expiresAt, new Date())); /** * Takes one use of an invite. It is a single statement, so two people * registering with the same single use link at once cannot both get in. */ export const claimInvite = async (token: string): Promise => { const claimed = await db.update(invites) .set({ uses: sql`${invites.uses} + 1` }) .where(and(eq(invites.token, token), usable())) .returning({ id: invites.id }); return claimed.length > 0; }; /** Gives a use back when the registration it was taken for did not go through. */ export const releaseInvite = async (token: string) => { await db.update(invites) .set({ uses: sql`${invites.uses} - 1` }) .where(and(eq(invites.token, token), gt(invites.uses, 0))); }; export const findUsableInvite = async (token: string) => { const rows = await db .select({ expiresAt: invites.expiresAt, maxUses: invites.maxUses, uses: invites.uses, inviterId: users.id, inviterName: users.name, inviterImage: users.image, }) .from(invites) .innerJoin(users, eq(users.id, invites.createdBy)) .where(and(eq(invites.token, token), usable())) .limit(1); return rows[0] ?? null; }; /** Used up and expired invites are of no use to anyone, so they are dropped. */ export const deleteDeadInvites = async () => { await db.delete(invites).where(or(lte(invites.expiresAt, new Date()), sql`${invites.uses} >= ${invites.maxUses}`)); };