import cookieParser from 'cookie-parser'; import express from 'express'; import fetch from 'node-fetch'; import morgan from 'morgan'; import Sentry from '@sentry/node'; import bancho from './bancho.js'; import db from './database.js'; import {sync_discord_info} from './discord_updates.js'; import {get_or_create_user, get_user} from './user.js'; import Config from './util/config.js'; import {render_error, gen_url} from './util/helpers.js'; import {register_routes as register_api_routes} from './website_api.js'; import {register_routes as register_static_routes} from './website_ssr.js'; const auth_page = gen_url(0, '/auth'); async function listen() { const app = express(); if (Config.ENABLE_SENTRY) { app.use(Sentry.Handlers.requestHandler()); } app.use(morgan(':status :method :url :response-time ms (:remote-addr)')); app.enable('trust proxy'); app.set('trust proxy', () => true); app.use(cookieParser(Config.cookie_secret)); await register_api_routes(app); app.get('/', async (req, http_res) => { http_res.redirect('/lobbies/'); }); // Convenience redirect so we only have to generate the oauth URL here. app.get('/osu_login', (req, http_res) => { http_res.redirect(`https://osu.ppy.sh/oauth/authorize?client_id=${Config.osu_v2api_client_id}&response_type=code&state=login&scope=identify&redirect_uri=${auth_page}`); }); // Convenience URL that redirects you to your profile or the login page app.get('/me/', (req, http_res) => { if (req.signedCookies.user) { return http_res.redirect(`${req.protocol}://${req.hostname}/u/${req.signedCookies.user}`); } else { http_res.cookie('login_to', `${req.protocol}://${req.hostname}/me/`, { domain: Config.domain_name, maxAge: 30000, httpOnly: false, secure: Config.IS_PRODUCTION, signed: false, sameSite: 'Strict', }); return http_res.redirect('/osu_login'); } }); app.get('/auth_migrate', (req, http_res) => { try { const info = db.prepare(`SELECT osu_id FROM token WHERE token = ?`).get(req.query.token); http_res.cookie('user', info.osu_id, { domain: Config.domain_name, maxAge: 34560000000, httpOnly: false, secure: Config.IS_PRODUCTION, signed: true, sameSite: 'Strict', }); if (req.query.last_match_id) { http_res.cookie('last_match', req.query.last_match_id, { domain: Config.domain_name, maxAge: 34560000000, httpOnly: false, secure: Config.IS_PRODUCTION, signed: true, sameSite: 'Strict', }); } } catch { // If token was invalid, just ignore } // Nginx will serve index.html on 404 // Frontend will redirect to the correct page using the login_to cookie return http_res.status(404).end(); }); app.get('/auth', async (req, http_res) => { let res; if (!req.query.code) { return http_res.status(403).end(render_error('No auth code provided.')); } const fetchOauthTokens = async (req) => { // Get oauth tokens from osu!api try { res = await fetch('https://osu.ppy.sh/oauth/token', { method: 'post', body: JSON.stringify({ client_id: Config.osu_v2api_client_id, client_secret: Config.osu_v2api_client_secret, code: req.query.code, grant_type: 'authorization_code', redirect_uri: auth_page, }), headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, }); } catch (err) { console.error(res.status, await res.text()); http_res.status(503).end(render_error('Internal server error, try again later.')); return null; } if (!res.ok) { console.error(res.status, await res.text()); http_res.status(403).end(render_error('Invalid auth code.')); return null; } // Get osu user id from the received oauth tokens return await res.json(); }; const fetchUserProfile = async (req, access_token) => { try { res = await fetch('https://osu.ppy.sh/api/v2/me/osu', { method: 'get', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': `Bearer ${access_token}`, }, }); } catch (err) { http_res.status(503).send(render_error('Internal server error, try again later.')); console.error(res.status, await res.text()); http_res.end(); return null; } if (!res.ok) { http_res.status(503).send(render_error('osu!web sent us bogus tokens. Sorry, idk what to do now')); http_res.end(); return null; } return await res.json(); }; if (req.query.state === 'login') { const tokens = await fetchOauthTokens(req); if (tokens === null) return; const user_profile = await fetchUserProfile(req, tokens.access_token); if (user_profile === null) return; // Initialize the user in the database if needed try { await get_or_create_user(user_profile.id); } catch (err) { return http_res.end(render_error(`Couldn't fetch profile... Are you banned?`)); } http_res.cookie('user', user_profile.id, { domain: Config.domain_name, maxAge: 34560000000, httpOnly: false, secure: Config.IS_PRODUCTION, signed: true, sameSite: 'Strict', }); return http_res.end(render_error('Logged in successfully!')); } else { // Get discord user id from ephemeral token const ephemeral_token = req.query.state; const discord_user_id = db.prepare(`SELECT discord_id FROM token WHERE token = ?`).get(ephemeral_token); if (!discord_user_id) { return http_res.status(403).end(render_error('Invalid Discord token. Please click the "Link account" button once again.')); } const tokens = await fetchOauthTokens(req); if (tokens === null) return; const user_profile = await fetchUserProfile(req, tokens.access_token); if (user_profile == null) return; // Initialize the user in the database if needed let user; try { user = await get_or_create_user(user_profile.id); } catch (err) { return http_res.end(render_error(`Couldn't fetch profile... Are you banned?`)); } // Link accounts! Finally. db.prepare(`UPDATE user SET discord_user_id = ? WHERE user_id = ?`).run(discord_user_id.discord_id, user_profile.id); db.prepare(`DELETE FROM token WHERE token = ?`).run(ephemeral_token); user.discord_user_id = discord_user_id.discord_id; await sync_discord_info(user, 'Account link'); return http_res.end(render_error('Account has been linked!')); } }); app.post('/get-invite/:banchoId', async (req, http_res) => { if (!req.signedCookies.user) { http_res.status(403).json({error: 'You need to be authenticated to get an invite.'}); return; } let inviting_lobby = null; for (const lobby of bancho.joined_lobbies) { if (lobby.invite_id == req.params.banchoId) { inviting_lobby = lobby; break; } } if (!inviting_lobby) { http_res.status(404).json({error: 'Could not find the lobby. Maybe it has been closed?'}); return; } try { const user = await get_user(req.signedCookies.user); await bancho.privmsg(user.username, `${user.username}, here's your invite: [http://osump://${inviting_lobby.invite_id}/ ${inviting_lobby.name}]`); http_res.status(200).json({success: true}); } catch (err) { http_res.status(503).json({error: 'Failed to send invite', details: err.message}); } }); // Legacy URLs app.get('/leaderboard/:ruleset/*', (req, res, next) => { if (req.params.ruleset.indexOf('page-') != -1) { return next(); } const new_path = req.path.replace(`/${req.params.ruleset}/`, '/'); return res.redirect(`${req.protocol}://${req.params.ruleset}.${Config.domain_name}${new_path}`); }); app.get('/u/:userId/:ruleset/*', (req, res, next) => { if (req.params.ruleset.indexOf('page-') != -1) { return next(); } const new_path = req.path.replace(`/${req.params.ruleset}/`, '/'); return res.redirect(`${req.protocol}://${req.params.ruleset}.${Config.domain_name}${new_path}`); }); await register_static_routes(app); if (Config.ENABLE_SENTRY) { app.use(Sentry.Handlers.errorHandler()); } app.listen(3001, () => { console.log(`Listening on :${3001}. Access via ${gen_url(0, '/')}`); }); } export {listen};