Files
erika/lib/auth/auth-options.ts
T

121 lines
3.9 KiB
TypeScript

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