import "server-only"; import type { AdapterAccount } from "next-auth/adapters"; import { and, eq } from "drizzle-orm"; import { db } from "@/db"; import { accounts, users } from "@/db/schema"; export type DiscordIdentity = { discordId: string; displayName: string; avatarUrl: string | null; email?: string | null; account?: Partial; }; function accountUpdate(account: Partial | undefined) { if (!account) return {}; return { ...(account.type !== undefined ? { type: account.type } : {}), ...(account.refresh_token !== undefined ? { refresh_token: account.refresh_token } : {}), ...(account.access_token !== undefined ? { access_token: account.access_token } : {}), ...(account.expires_at !== undefined ? { expires_at: account.expires_at } : {}), ...(account.token_type !== undefined ? { token_type: account.token_type } : {}), ...(account.scope !== undefined ? { scope: account.scope } : {}), ...(account.id_token !== undefined ? { id_token: account.id_token } : {}), ...(account.session_state !== undefined ? { session_state: account.session_state as string | null } : {}), }; } export async function ensureDiscordUser( identity: DiscordIdentity ): Promise { const { discordId, displayName, avatarUrl, email, account } = identity; return db.transaction(async (tx) => { const [linked] = await tx .select({ userId: accounts.userId }) .from(accounts) .where( and( eq(accounts.provider, "discord"), eq(accounts.providerAccountId, discordId) ) ) .limit(1); let userId = linked?.userId; if (!userId) { const [user] = await tx .insert(users) .values({ discordId, name: displayName, image: avatarUrl, ...(email !== undefined ? { email } : {}), }) .onConflictDoUpdate({ target: users.discordId, set: { name: displayName, image: avatarUrl, ...(email !== undefined ? { email } : {}), }, }) .returning({ id: users.id }); userId = user.id; await tx .insert(accounts) .values({ userId, provider: "discord", providerAccountId: discordId, type: account?.type ?? "oauth", ...accountUpdate(account), }) .onConflictDoNothing({ target: [accounts.provider, accounts.providerAccountId], }); const [winner] = await tx .select({ userId: accounts.userId }) .from(accounts) .where( and( eq(accounts.provider, "discord"), eq(accounts.providerAccountId, discordId) ) ) .limit(1); if (!winner) throw new Error("Failed to link Discord account"); userId = winner.userId; } const [user] = await tx .update(users) .set({ discordId, name: displayName, image: avatarUrl, ...(email !== undefined ? { email } : {}), }) .where(eq(users.id, userId)) .returning(); const nextAccount = accountUpdate(account); if (Object.keys(nextAccount).length > 0) { await tx .update(accounts) .set(nextAccount) .where( and( eq(accounts.provider, "discord"), eq(accounts.providerAccountId, discordId) ) ); } if (!user) throw new Error("Failed to reconcile Discord user"); return user; }); }