2024-06-26 13:59:53 +02:00

152 lines
5.1 KiB
TypeScript

import { Router } from 'express';
import type { Request, Response } from 'express';
import { handleErrorAsync } from '../../src/helpers.ts';
import SQL from 'sql-template-strings';
import type { UserRow } from './user.ts';
const configs = {
discordToken: process.env.DISCORD_TOKEN!,
discordClientId: process.env.DISCORD_CLIENT_ID!,
discordClientSecret: process.env.DISCORD_CLIENT_SECRET!,
discordRedirectUri: process.env.DISCORD_REDIRECT_URI!,
};
const store: Map<string, TokenSet> = new Map();
const BASE_DISCORD_URI = 'https://discord.com/api/v10';
const OAUTH_URL = Object.assign(new URL('/oauth2/authorize', BASE_DISCORD_URI), {
search: new URLSearchParams({
client_id: configs.discordClientId,
redirect_uri: configs.discordRedirectUri,
response_type: 'code',
scope: [
'role_connections.write',
'identify',
'email',
].join(' '),
prompt: 'consent',
}).toString(),
}).toString();
interface TokenSet {
access_token: string;
refresh_token: string;
expires_in: number;
expires_at?: number;
}
const buildDiscordRequest = (grantType: string, miscConfig: Record<string, string>): Record<string, string> => ({
client_id: configs.discordClientId,
client_secret: configs.discordClientSecret,
grant_type: grantType,
...miscConfig,
});
const getOAuthTokens = async (accessCode: string): Promise<TokenSet> => {
return await fetch(`${BASE_DISCORD_URI}/oauth2/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(
buildDiscordRequest('authorization_code', {
code: accessCode,
redirect_uri: configs.discordRedirectUri,
}),
),
}).then((res) => res.json());
};
const getAccessToken = async (userId: string) => {
const tokenSet = store.get(userId);
if (tokenSet === undefined) {
return undefined;
}
if (Date.now() > tokenSet.expires_at!) {
const token: TokenSet = await fetch(`${BASE_DISCORD_URI}/oauth2/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(
buildDiscordRequest('refresh_token', {
refresh_token: tokenSet.refresh_token,
}),
),
}).then((res) => res.json());
token.expires_at = Date.now() + tokenSet.expires_in * 1e3;
store.set(userId, token);
return token.access_token;
}
return tokenSet.access_token;
};
const getUserData = async (token: TokenSet) => {
const data = await fetch(`${BASE_DISCORD_URI}/users/@me`, {
headers: { Authorization: `Bearer ${token.access_token}` },
}).then((res) => res.json());
return {
uid: data.id,
email: data.email,
username: data.username,
};
};
const updateMetadata = async (req: Request, res: Response, data: { uid: string, email: string }) => {
const user = await req.db.get<Pick<UserRow, 'username' | 'roles'>>(
SQL`
SELECT users.username, users.roles FROM social_lookup
LEFT JOIN users on social_lookup.userId = users.id
WHERE social_lookup.provider = 'discord' AND social_lookup.identifier = ${data.uid};`,
);
if (!user) {
const error = {
code: 404,
error: 'User not found',
hint: [
'We are unable to locate your account using the information provided to us by Discord.',
'Please insure you have COMPLETELY followed all of the steps shown in the verification channel.',
'If you are still having an issue after confirming the above,',
'please contact the Modmail bot for further assistance.',
].join(' '),
};
return res.status(error.code).json(error);
}
await fetch(
`${BASE_DISCORD_URI}/users/@me/applications/${configs.discordClientId}/role-connection`,
{
method: 'PUT',
headers: {
Authorization: `Bearer ${await getAccessToken(data.uid)}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
platform_name: 'Pronouns.Page',
platform_username: user.username,
metadata: {
isteam: user.roles !== '',
isuser: true,
},
}),
},
);
res.send('200 OK');
};
const router = Router();
router.get('/discord/linked-role', async (_req: Request, res: Response) => {
res.redirect(OAUTH_URL);
});
router.get('/discord/oauth2', handleErrorAsync(async (req, res) => {
const { code } = req.query;
if (!code) {
return res.status(400).send('missing state');
}
const token = await getOAuthTokens(code as string);
const userData = await getUserData(token);
token.expires_at = Date.now() + token.expires_in * 1000;
store.set(userData.uid, token);
await updateMetadata(req, res, userData);
}));
export default router;