import { AuthProvider, AuthProviderState } from "./AuthProvider"; import prisma from "@/prisma"; export type DiscordAccessTokenResponse = { access_token: string; token_type: string; expires_in: number; refresh_token: string; scope: string; }; type DiscordUserResponse = { id: string; avatar: string | null; username: string; email: string | null; discriminator: string; }; type DiscordGuildResponse = { id: string; }; export class DiscordAuthProvider extends AuthProvider { private async getMe(): Promise { const req = await fetch("https://discord.com/api/users/@me", { headers: { Authorization: `Bearer ${this.accessToken}` } }); const res: DiscordUserResponse = await req.json(); return res; } async isPermitted(): Promise { const req = await fetch("https://discord.com/api/users/@me/guilds", { headers: { Authorization: `Bearer ${this.accessToken}` } }); const res: DiscordGuildResponse[] = await req.json(); const guilds = res.map((guild) => guild.id); const allowedGuilds = process.env.DISCORD_ALLOWED_GUILDS?.split(",") ?? []; let allowed = false; for (const guild of allowedGuilds) { if (guilds.includes(guild)) allowed = true; } return allowed; } async getDisplayName(): Promise { const me = await this.getMe(); return me.username; } async getUsername(): Promise { const me = await this.getMe(); return me.username + "#" + me.discriminator; } async getId(): Promise { const me = await this.getMe(); return me.id; } async getAvatar(): Promise { const me = await this.getMe(); return me.avatar !== null ? `https://cdn.discordapp.com/avatars/${me.id}/${me.avatar}.png` : null; } async getEmail(): Promise { const me = await this.getMe(); return me.email; } async getState(): Promise { const username = await this.getUsername(); const id = await this.getId(); return { name: "Discord", connected: true, id, username }; } static get redirectUri(): string { return `${process.env.BASE_DOMAIN}oauth/discord/redirect`; } static async getToken( code: string ): Promise { const form = new URLSearchParams(); form.append("client_id", process.env.DISCORD_CLIENT_ID); form.append("client_secret", process.env.DISCORD_CLIENT_SECRET); form.append("grant_type", "authorization_code"); form.append("code", code); form.append("redirect_uri", this.redirectUri); const tokenResponse = await fetch("https://discord.com/api/oauth2/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: form.toString() }); if (!tokenResponse.ok) return null; return await tokenResponse.json(); } static async refreshToken( refreshToken: string ): Promise { const req = await fetch("https://discord.com/api/oauth2/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ client_id: process.env.DISCORD_CLIENT_ID, client_secret: process.env.DISCORD_CLIENT_SECRET, grant_type: "refresh_token", refresh_token: refreshToken }).toString() }); if (!req.ok) return null; return await req.json(); } static async update( id: string, accessToken: string, refreshToken: string, expiresAt: Date, userId?: number ): Promise { const a = await prisma.discordAuth.upsert({ where: { id }, create: { id, accessToken, refreshToken, expiresAt, user: userId != null ? { connect: { id: userId } } : { create: { username: null } }, invalid: false }, update: { accessToken, refreshToken, expiresAt, invalid: false } }); return a.userId; } }