import { LoginRateLimitedError, type AuthRepository } from "@minecraft-account-manager/auth"; import { and, desc, eq, gt, isNull, lt, sql } from "drizzle-orm"; import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; import * as schema from "./schema"; import { loginCodes, sessions, users } from "./schema"; type Database = PostgresJsDatabase; export function createAuthRepository(db: Database): AuthRepository { return { async saveLoginCode(code) { await db.transaction(async (tx) => { await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${code.discordUserId}))`); const [latestCode] = await tx .select({ createdAt: loginCodes.createdAt }) .from(loginCodes) .where(eq(loginCodes.discordUserId, code.discordUserId)) .orderBy(desc(loginCodes.createdAt)) .limit(1); if (latestCode && latestCode.createdAt > new Date(code.createdAt.getTime() - 30_000)) { throw new LoginRateLimitedError(); } await tx.delete(loginCodes).where(lt(loginCodes.expiresAt, code.createdAt)); await tx .update(loginCodes) .set({ consumedAt: code.createdAt }) .where( and( eq(loginCodes.discordUserId, code.discordUserId), isNull(loginCodes.consumedAt), ), ); await tx.insert(loginCodes).values({ tokenHash: code.tokenHash, discordUserId: code.discordUserId, discordUsername: code.discordUsername, discordGlobalName: code.discordGlobalName, expiresAt: code.expiresAt, createdAt: code.createdAt, }); }); }, async exchangeLoginCode(input) { return db.transaction(async (tx) => { const [loginCode] = await tx .update(loginCodes) .set({ consumedAt: input.now }) .where( and( eq(loginCodes.tokenHash, input.loginCodeHash), isNull(loginCodes.consumedAt), gt(loginCodes.expiresAt, input.now), ), ) .returning(); if (!loginCode) { return null; } const [existingUser] = await tx .select({ id: users.id }) .from(users) .where(eq(users.discordUserId, loginCode.discordUserId)) .limit(1); const [user] = await tx .insert(users) .values({ discordUserId: loginCode.discordUserId, discordUsername: loginCode.discordUsername, discordGlobalName: loginCode.discordGlobalName, }) .onConflictDoUpdate({ target: users.discordUserId, set: { discordUsername: loginCode.discordUsername, discordGlobalName: loginCode.discordGlobalName, updatedAt: input.now, }, }) .returning({ id: users.id, discordUserId: users.discordUserId, discordUsername: users.discordUsername, firstName: users.firstName, }); if (!user) { throw new Error("Failed to create or update the Discord user"); } await tx.insert(sessions).values({ userId: user.id, tokenHash: input.sessionTokenHash, expiresAt: input.sessionExpiresAt, lastSeenAt: input.now, createdAt: input.now, }); return { user, isNewUser: !existingUser }; }); }, }; } export async function findUserBySessionToken(db: Database, tokenHash: string, now = new Date()) { const [user] = await db .select({ id: users.id, discordUserId: users.discordUserId, discordUsername: users.discordUsername, discordGlobalName: users.discordGlobalName, firstName: users.firstName, onboardingCompletedAt: users.onboardingCompletedAt, sessionExpiresAt: sessions.expiresAt, }) .from(sessions) .innerJoin(users, eq(sessions.userId, users.id)) .where( and( eq(sessions.tokenHash, tokenHash), isNull(sessions.revokedAt), gt(sessions.expiresAt, now), ), ) .limit(1); return user ?? null; }