Files
erika/lib/auth/discord-user.ts
T

133 lines
3.6 KiB
TypeScript

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<AdapterAccount>;
};
function accountUpdate(account: Partial<AdapterAccount> | 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<typeof users.$inferSelect> {
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;
});
}