import "server-only"; import type { NextAuthOptions } from "next-auth"; import DiscordProvider from "next-auth/providers/discord"; import { DrizzleAdapter } from "@auth/drizzle-adapter"; import { db } from "@/db"; import { accounts, sessions, users, verificationTokens } from "@/db/schema"; import type { SessionWithDiscord } from "@/lib/auth/auth"; import { ensureDiscordUser } from "@/lib/auth/discord-user"; import { addDiscordGuildMember } from "@/lib/discord/discord"; const discordIdCache = new Map(); type DiscordOAuthProfile = { id: string; username: string; global_name?: string | null; discriminator?: string; avatar?: string | null; email?: string | null; }; function discordAvatarUrl(profile: DiscordOAuthProfile) { if (profile.avatar) { const format = profile.avatar.startsWith("a_") ? "gif" : "png"; return `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}?size=128`; } const discriminator = Number(profile.discriminator ?? 0); const index = discriminator > 0 ? discriminator % 5 : Number((BigInt(profile.id) >> BigInt(22)) % BigInt(6)); return `https://cdn.discordapp.com/embed/avatars/${index}.png`; } export const authOptions: NextAuthOptions = { adapter: DrizzleAdapter(db, { usersTable: users, accountsTable: accounts, sessionsTable: sessions, verificationTokensTable: verificationTokens, }), providers: [ DiscordProvider({ clientId: process.env.DISCORD_CLIENT_ID!, clientSecret: process.env.DISCORD_CLIENT_SECRET!, authorization: { params: { scope: "identify email guilds.join", }, }, profile(profile: DiscordOAuthProfile) { return { id: profile.id, name: profile.global_name?.trim() || profile.username, email: profile.email, image: discordAvatarUrl(profile), }; }, }), ], callbacks: { async signIn({ account, profile, user }) { if (account?.provider === "discord" && account.providerAccountId && account.access_token) { const discordProfile = profile as DiscordOAuthProfile | undefined; const displayName = discordProfile?.global_name?.trim() || discordProfile?.username || user.name || "Discord user"; const avatarUrl = discordProfile ? discordAvatarUrl(discordProfile) : user.image ?? null; const reconciled = await ensureDiscordUser({ discordId: account.providerAccountId, displayName, avatarUrl, email: discordProfile?.email ?? user.email ?? null, account: { type: "oauth", refresh_token: account.refresh_token, access_token: account.access_token, expires_at: account.expires_at, token_type: account.token_type, scope: account.scope, id_token: account.id_token, session_state: account.session_state, }, }); discordIdCache.set(reconciled.id, account.providerAccountId); await addDiscordGuildMember(account.providerAccountId, account.access_token); } return true; }, async session({ session, user }) { if (session.user) { (session as SessionWithDiscord).user!.id = user.id; let discordId = discordIdCache.get(user.id); if (!discordId) { const account = await db.query.accounts.findFirst({ where: (record, { eq, and }) => and(eq(record.userId, user.id), eq(record.provider, "discord")), columns: { providerAccountId: true }, }); if (account) { discordId = account.providerAccountId; discordIdCache.set(user.id, discordId); } } if (discordId) { (session as SessionWithDiscord).user!.discordId = discordId; } } return session; }, }, secret: process.env.NEXTAUTH_SECRET, };