website.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import cookieParser from 'cookie-parser';
  2. import express from 'express';
  3. import fetch from 'node-fetch';
  4. import morgan from 'morgan';
  5. import Sentry from '@sentry/node';
  6. import bancho from './bancho.js';
  7. import db from './database.js';
  8. import {sync_discord_info} from './discord_updates.js';
  9. import {get_or_create_user, get_user} from './user.js';
  10. import Config from './util/config.js';
  11. import {render_error, gen_url} from './util/helpers.js';
  12. import {register_routes as register_api_routes} from './website_api.js';
  13. import {register_routes as register_static_routes} from './website_ssr.js';
  14. const auth_page = gen_url(0, '/auth');
  15. async function listen() {
  16. const app = express();
  17. if (Config.ENABLE_SENTRY) {
  18. app.use(Sentry.Handlers.requestHandler());
  19. }
  20. app.use(morgan(':status :method :url :response-time ms (:remote-addr)'));
  21. app.enable('trust proxy');
  22. app.set('trust proxy', () => true);
  23. app.use(cookieParser(Config.cookie_secret));
  24. await register_api_routes(app);
  25. app.get('/', async (req, http_res) => {
  26. http_res.redirect('/lobbies/');
  27. });
  28. // Convenience redirect so we only have to generate the oauth URL here.
  29. app.get('/osu_login', (req, http_res) => {
  30. 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}`);
  31. });
  32. // Convenience URL that redirects you to your profile or the login page
  33. app.get('/me/', (req, http_res) => {
  34. if (req.signedCookies.user) {
  35. return http_res.redirect(`${req.protocol}://${req.hostname}/u/${req.signedCookies.user}`);
  36. } else {
  37. http_res.cookie('login_to', `${req.protocol}://${req.hostname}/me/`, {
  38. domain: Config.domain_name,
  39. maxAge: 30000,
  40. httpOnly: false,
  41. secure: Config.IS_PRODUCTION,
  42. signed: false,
  43. sameSite: 'Strict',
  44. });
  45. return http_res.redirect('/osu_login');
  46. }
  47. });
  48. app.get('/auth_migrate', (req, http_res) => {
  49. try {
  50. const info = db.prepare(`SELECT osu_id FROM token WHERE token = ?`).get(req.query.token);
  51. http_res.cookie('user', info.osu_id, {
  52. domain: Config.domain_name,
  53. maxAge: 34560000000,
  54. httpOnly: false,
  55. secure: Config.IS_PRODUCTION,
  56. signed: true,
  57. sameSite: 'Strict',
  58. });
  59. if (req.query.last_match_id) {
  60. http_res.cookie('last_match', req.query.last_match_id, {
  61. domain: Config.domain_name,
  62. maxAge: 34560000000,
  63. httpOnly: false,
  64. secure: Config.IS_PRODUCTION,
  65. signed: true,
  66. sameSite: 'Strict',
  67. });
  68. }
  69. } catch {
  70. // If token was invalid, just ignore
  71. }
  72. // Nginx will serve index.html on 404
  73. // Frontend will redirect to the correct page using the login_to cookie
  74. return http_res.status(404).end();
  75. });
  76. app.get('/auth', async (req, http_res) => {
  77. let res;
  78. if (!req.query.code) {
  79. return http_res.status(403).end(render_error('No auth code provided.'));
  80. }
  81. const fetchOauthTokens = async (req) => {
  82. // Get oauth tokens from osu!api
  83. try {
  84. res = await fetch('https://osu.ppy.sh/oauth/token', {
  85. method: 'post',
  86. body: JSON.stringify({
  87. client_id: Config.osu_v2api_client_id,
  88. client_secret: Config.osu_v2api_client_secret,
  89. code: req.query.code,
  90. grant_type: 'authorization_code',
  91. redirect_uri: auth_page,
  92. }),
  93. headers: {
  94. 'Accept': 'application/json',
  95. 'Content-Type': 'application/json',
  96. },
  97. });
  98. } catch (err) {
  99. console.error(res.status, await res.text());
  100. http_res.status(503).end(render_error('Internal server error, try again later.'));
  101. return null;
  102. }
  103. if (!res.ok) {
  104. console.error(res.status, await res.text());
  105. http_res.status(403).end(render_error('Invalid auth code.'));
  106. return null;
  107. }
  108. // Get osu user id from the received oauth tokens
  109. return await res.json();
  110. };
  111. const fetchUserProfile = async (req, access_token) => {
  112. try {
  113. res = await fetch('https://osu.ppy.sh/api/v2/me/osu', {
  114. method: 'get',
  115. headers: {
  116. 'Accept': 'application/json',
  117. 'Content-Type': 'application/json',
  118. 'Authorization': `Bearer ${access_token}`,
  119. },
  120. });
  121. } catch (err) {
  122. http_res.status(503).send(render_error('Internal server error, try again later.'));
  123. console.error(res.status, await res.text());
  124. http_res.end();
  125. return null;
  126. }
  127. if (!res.ok) {
  128. http_res.status(503).send(render_error('osu!web sent us bogus tokens. Sorry, idk what to do now'));
  129. http_res.end();
  130. return null;
  131. }
  132. return await res.json();
  133. };
  134. if (req.query.state === 'login') {
  135. const tokens = await fetchOauthTokens(req);
  136. if (tokens === null) return;
  137. const user_profile = await fetchUserProfile(req, tokens.access_token);
  138. if (user_profile === null) return;
  139. // Initialize the user in the database if needed
  140. try {
  141. await get_or_create_user(user_profile.id);
  142. } catch (err) {
  143. return http_res.end(render_error(`Couldn't fetch profile... Are you banned?`));
  144. }
  145. http_res.cookie('user', user_profile.id, {
  146. domain: Config.domain_name,
  147. maxAge: 34560000000,
  148. httpOnly: false,
  149. secure: Config.IS_PRODUCTION,
  150. signed: true,
  151. sameSite: 'Strict',
  152. });
  153. return http_res.end(render_error('Logged in successfully!'));
  154. } else {
  155. // Get discord user id from ephemeral token
  156. const ephemeral_token = req.query.state;
  157. const discord_user_id = db.prepare(`SELECT discord_id FROM token WHERE token = ?`).get(ephemeral_token);
  158. if (!discord_user_id) {
  159. return http_res.status(403).end(render_error('Invalid Discord token. Please click the "Link account" button once again.'));
  160. }
  161. const tokens = await fetchOauthTokens(req);
  162. if (tokens === null) return;
  163. const user_profile = await fetchUserProfile(req, tokens.access_token);
  164. if (user_profile == null) return;
  165. // Initialize the user in the database if needed
  166. let user;
  167. try {
  168. user = await get_or_create_user(user_profile.id);
  169. } catch (err) {
  170. return http_res.end(render_error(`Couldn't fetch profile... Are you banned?`));
  171. }
  172. // Link accounts! Finally.
  173. db.prepare(`UPDATE user SET discord_user_id = ? WHERE user_id = ?`).run(discord_user_id.discord_id, user_profile.id);
  174. db.prepare(`DELETE FROM token WHERE token = ?`).run(ephemeral_token);
  175. user.discord_user_id = discord_user_id.discord_id;
  176. await sync_discord_info(user, 'Account link');
  177. return http_res.end(render_error('Account has been linked!'));
  178. }
  179. });
  180. app.post('/get-invite/:banchoId', async (req, http_res) => {
  181. if (!req.signedCookies.user) {
  182. http_res.status(403).json({error: 'You need to be authenticated to get an invite.'});
  183. return;
  184. }
  185. let inviting_lobby = null;
  186. for (const lobby of bancho.joined_lobbies) {
  187. if (lobby.invite_id == req.params.banchoId) {
  188. inviting_lobby = lobby;
  189. break;
  190. }
  191. }
  192. if (!inviting_lobby) {
  193. http_res.status(404).json({error: 'Could not find the lobby. Maybe it has been closed?'});
  194. return;
  195. }
  196. try {
  197. const user = await get_user(req.signedCookies.user);
  198. await bancho.privmsg(user.username, `${user.username}, here's your invite: [http://osump://${inviting_lobby.invite_id}/ ${inviting_lobby.name}]`);
  199. http_res.status(200).json({success: true});
  200. } catch (err) {
  201. http_res.status(503).json({error: 'Failed to send invite', details: err.message});
  202. }
  203. });
  204. // Legacy URLs
  205. app.get('/leaderboard/:ruleset/*', (req, res, next) => {
  206. if (req.params.ruleset.indexOf('page-') != -1) {
  207. return next();
  208. }
  209. const new_path = req.path.replace(`/${req.params.ruleset}/`, '/');
  210. return res.redirect(`${req.protocol}://${req.params.ruleset}.${Config.domain_name}${new_path}`);
  211. });
  212. app.get('/u/:userId/:ruleset/*', (req, res, next) => {
  213. if (req.params.ruleset.indexOf('page-') != -1) {
  214. return next();
  215. }
  216. const new_path = req.path.replace(`/${req.params.ruleset}/`, '/');
  217. return res.redirect(`${req.protocol}://${req.params.ruleset}.${Config.domain_name}${new_path}`);
  218. });
  219. await register_static_routes(app);
  220. if (Config.ENABLE_SENTRY) {
  221. app.use(Sentry.Handlers.errorHandler());
  222. }
  223. app.listen(3001, () => {
  224. console.log(`Listening on :${3001}. Access via ${gen_url(0, '/')}`);
  225. });
  226. }
  227. export {listen};