feat: add share schema and Discord account reconciliation
This commit is contained in:
@@ -5,10 +5,33 @@ 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,
|
||||
@@ -25,11 +48,46 @@ export const authOptions: NextAuthOptions = {
|
||||
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 }) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
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;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user