133 lines
3.6 KiB
TypeScript
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;
|
|
});
|
|
}
|