diff --git a/src/web.ts b/src/web.ts index 66f3a7d..9acca65 100644 --- a/src/web.ts +++ b/src/web.ts @@ -11,6 +11,7 @@ import { IncomingHttpHeaders } from 'http'; 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; @@ -18,13 +19,87 @@ async function getSessionString(cookieStr: string) { const cookies = cookieStr.split('; ').map(s => parse(s)!).filter(c => c !== null); const sessionCookie = cookies.find(c => c.key === COOKIE_KEY); - if (!sessionCookie) { + if (!sessionCookie || sessionCookie.value.length !== UID_STRING_LENGTH) { return await uid(UID_BYTE_LENGTH); } else { return sessionCookie.value; } } +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('*'); + } +} + +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]; +} + +function updateCookie(res: express.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()); +} + async function getSession(bot: Client, headers: IncomingHttpHeaders) { const cookie = headers['cookie']; if (!cookie) return; @@ -39,32 +114,7 @@ async function getSession(bot: Client, headers: IncomingHttpHeaders) { if (Date.now() < session.expiresAt) return session; - 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: session.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', sessionStr) - .update(sessionData) - .returning('*'))[0]; + const newSession = refreshToken(bot, session.id, session.refreshToken); } export async function getUser(session: Session | undefined) { @@ -130,48 +180,19 @@ export async function startServer(bot: Client, port: number) { if (code) { try { - const resp = 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; + const resp = await getToken(bot, code); + if (!resp) return res.status(400).send('Invalid code provided'); const sessionId = await getSessionString(decodeURIComponent(req.headers.cookie || '')); - const session = await db('sessions') - .where('id', sessionId) - .first(); - - const sessionData = { + setSession(sessionId, { tokenType: resp.token_type, accessToken: resp.access_token, refreshToken: resp.refresh_token, expiresAt: Date.now() + resp.expires_in * 1000, - }; - - if (session) { - await db('sessions') - .where('id', sessionId) - .update(sessionData); - } else { - await db('sessions') - .insert({id: sessionId, ...sessionData} satisfies Session) - .returning('*'); - } - - const cookie = new Cookie({ - key: COOKIE_KEY, - value: sessionId, - expires: new Date(Date.now() + COOKIE_EXPIRES), - sameSite: 'strict' }); - res.setHeader('Set-Cookie', cookie.toString()); + + updateCookie(res, sessionId); return res.redirect('/profile'); } catch (err) {