import type { Response } from 'express'; import type { IncomingHttpHeaders } from 'http'; import * as log from '../lib/log'; import { Cookie, parse as parseCookie } from 'tough-cookie'; import uid from 'uid-safe'; import { Client, RESTPostOAuth2AccessTokenResult, RESTPostOAuth2AccessTokenURLEncodedData, RESTPostOAuth2RefreshTokenURLEncodedData, Routes } from 'discord.js'; import got from 'got'; import { Session, db } from '../lib/db'; export const DISCORD_ENDPOINT = 'https://discord.com/api/v10'; const UID_BYTE_LENGTH = 18; const UID_STRING_LENGTH = 24; // why? const COOKIE_KEY = 'PHPSESSID'; const COOKIE_EXPIRES = 1_000 * 60 * 60 * 24 * 365; export async function getSessionString(cookieStr: string) { const cookies = cookieStr.split('; ').map(s => parseCookie(s)!).filter(c => c !== null); const sessionCookie = cookies.find(c => c.key === COOKIE_KEY); if (!sessionCookie || sessionCookie.value.length !== UID_STRING_LENGTH) { return await uid(UID_BYTE_LENGTH); } else { return sessionCookie.value; } } export function updateCookie(res: Response, sessionId: string) { const cookie = new Cookie({ key: COOKIE_KEY, value: sessionId, expires: new Date(Date.now() + COOKIE_EXPIRES), sameSite: 'strict' }); res.setHeader('Set-Cookie', cookie.toString()); } export async function getToken(bot: Client, code: string) { try { return await got.post(DISCORD_ENDPOINT + Routes.oauth2TokenExchange(), { form: { client_id: bot.config.clientId, client_secret: bot.config.clientSecret, code, grant_type: 'authorization_code', redirect_uri: bot.config.siteURL, } satisfies RESTPostOAuth2AccessTokenURLEncodedData // if you're looking to change this then you are blissfully unaware of the past // and have learnt 0 lessons }).json() as RESTPostOAuth2AccessTokenResult; } catch(err) { log.error(err); return; } } async function refreshToken(bot: Client, sessionId: string, refreshToken: string) { let resp; try { resp = await got.post(DISCORD_ENDPOINT + Routes.oauth2TokenExchange(), { form: { client_id: bot.config.clientId, client_secret: bot.config.clientSecret, grant_type: 'refresh_token', refresh_token: refreshToken, } satisfies RESTPostOAuth2RefreshTokenURLEncodedData }).json() as RESTPostOAuth2AccessTokenResult; } catch(err) { log.error(err); return; } const sessionData = { tokenType: resp.token_type, accessToken: resp.access_token, refreshToken: resp.refresh_token, expiresAt: Date.now() + resp.expires_in * 1000, }; return (await db('sessions') .where('id', sessionId) .update(sessionData) .returning('*'))[0]; } export async function getSession(bot: Client, headers: IncomingHttpHeaders) { const cookie = headers['cookie']; if (!cookie) return; const sessionStr = await getSessionString(cookie); const session = await db('sessions') .where('id', sessionStr) .first(); if (!session) return; if (Date.now() < session.expiresAt) return session; const newSession = refreshToken(bot, session.id, session.refreshToken); return newSession; } export async function setSession(sessionId: string, sessionData: Omit) { const session = await db('sessions') .where('id', sessionId) .first(); if (session) { await db('sessions') .where('id', sessionId) .update(sessionData); } else { await db('sessions') .insert({id: sessionId, ...sessionData}) .returning('*'); } }