gluestick/src/auth/discord.ts

170 lines
4.1 KiB
TypeScript

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<DiscordUserResponse> {
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<boolean> {
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<string> {
const me = await this.getMe();
return me.username;
}
async getUsername(): Promise<string> {
const me = await this.getMe();
return me.username + "#" + me.discriminator;
}
async getId(): Promise<string> {
const me = await this.getMe();
return me.id;
}
async getAvatar(): Promise<string | null> {
const me = await this.getMe();
return me.avatar !== null
? `https://cdn.discordapp.com/avatars/${me.id}/${me.avatar}.png`
: null;
}
async getEmail(): Promise<string | null> {
const me = await this.getMe();
return me.email;
}
async getState(): Promise<AuthProviderState> {
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<DiscordAccessTokenResponse | null> {
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<DiscordAccessTokenResponse | null> {
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<number> {
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;
}
}