import 'dotenv/config'; import 'zone.js/node'; import { APP_BASE_HREF } from '@angular/common'; import { CommonEngine } from '@angular/ssr/node'; import express from 'express'; import { existsSync } from 'node:fs'; import { join } from 'node:path'; import AppServerModule from './src/main.server'; const STRAVA_API_BASE = 'https://www.strava.com/api/v3'; const FACEIT_API_BASE = 'https://open.faceit.com'; const TRACKER_API_BASE = 'https://public-api.tracker.gg'; function getRiotApiKey(path: string, gameTypeHeader: string | undefined): string { const lolKey = process.env['RIOT_API_KEY_LOL'] || ''; const tftKey = process.env['RIOT_API_KEY_TFT'] || ''; if (gameTypeHeader === 'tft' || path.includes('/tft/')) { return tftKey; } return lolKey; } // The Express app is exported so that it can be used by serverless Functions. export function app(): express.Express { const server = express(); const distFolder = join(process.cwd(), 'dist/goals-tracker/browser'); const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? join(distFolder, 'index.original.html') : join(distFolder, 'index.html'); const commonEngine = new CommonEngine(); server.set('view engine', 'html'); server.set('views', distFolder); server.use(express.json()); server.use(express.urlencoded({ extended: true })); // --- API Proxy routes (before static and catch-all) --- server.post('/api/auth/strava/token', async (req, res) => { const { code } = req.body || {}; const clientId = process.env['STRAVA_CLIENT_ID']; const clientSecret = process.env['STRAVA_CLIENT_SECRET']; if (!code || !clientId || !clientSecret) { res.status(400).json({ error: 'Missing code or Strava credentials' }); return; } try { const params = new URLSearchParams({ client_id: clientId, client_secret: clientSecret, code, grant_type: 'authorization_code', }); const response = await fetch(`${STRAVA_API_BASE}/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params.toString(), }); const data = await response.json(); res.status(response.status).json(data); } catch (err) { res.status(500).json({ error: 'Token exchange failed' }); } }); server.post('/api/auth/strava/refresh', async (req, res) => { const { refresh_token } = req.body || {}; const clientId = process.env['STRAVA_CLIENT_ID']; const clientSecret = process.env['STRAVA_CLIENT_SECRET']; if (!refresh_token || !clientId || !clientSecret) { res.status(400).json({ error: 'Missing refresh_token or Strava credentials' }); return; } try { const params = new URLSearchParams({ client_id: clientId, client_secret: clientSecret, refresh_token, grant_type: 'refresh_token', }); const response = await fetch(`${STRAVA_API_BASE}/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params.toString(), }); const data = await response.json(); res.status(response.status).json(data); } catch (err) { res.status(500).json({ error: 'Token refresh failed' }); } }); server.all(/^\/api\/proxy\/riot\//, async (req, res) => { const match = req.path.match(/^\/api\/proxy\/riot\/([^/]+)\/(.*)$/); const routing = match?.[1] ?? ''; const rest = (match?.[2] ?? '').replace(/^\//, ''); const gameType = req.headers['x-riot-game-type'] as string | undefined; const apiKey = getRiotApiKey(rest, gameType); if (!apiKey) { res.status(502).json({ error: 'Riot API key not configured' }); return; } const targetUrl = `https://${routing}.api.riotgames.com/${rest}`; const query = req.url.includes('?') ? req.url.substring(req.url.indexOf('?')) : ''; try { const headers: Record = { 'X-Riot-Token': apiKey.trim(), }; const fetchOpts: RequestInit = { method: req.method, headers, }; if (req.method !== 'GET' && req.body && Object.keys(req.body).length > 0) { headers['Content-Type'] = 'application/json'; fetchOpts.body = JSON.stringify(req.body); } const response = await fetch(targetUrl + query, fetchOpts); const text = await response.text(); res.status(response.status); const contentType = response.headers.get('content-type'); if (contentType?.includes('application/json')) { res.json(JSON.parse(text || '{}')); } else { res.send(text); } } catch (err) { res.status(502).json({ error: 'Riot proxy failed' }); } }); server.all(/^\/api\/proxy\/faceit/, async (req, res) => { const path = req.path.replace(/^\/api\/proxy\/faceit/, '') || '/'; const apiKey = process.env['FACEIT_API_KEY']; if (!apiKey) { res.status(502).json({ error: 'Faceit API key not configured' }); return; } const targetUrl = `${FACEIT_API_BASE}${path}`; const query = req.url.includes('?') ? req.url.substring(req.url.indexOf('?')) : ''; try { const headers: Record = { Authorization: `Bearer ${apiKey}`, }; const fetchOpts: RequestInit = { method: req.method, headers, }; if (req.method !== 'GET' && req.body && Object.keys(req.body).length > 0) { headers['Content-Type'] = 'application/json'; fetchOpts.body = JSON.stringify(req.body); } const response = await fetch(targetUrl + query, fetchOpts); const text = await response.text(); res.status(response.status); const contentType = response.headers.get('content-type'); if (contentType?.includes('application/json')) { res.json(JSON.parse(text || '{}')); } else { res.send(text); } } catch (err) { res.status(502).json({ error: 'Faceit proxy failed' }); } }); server.all(/^\/api\/proxy\/tracker/, async (req, res) => { const path = req.path.replace(/^\/api\/proxy\/tracker/, '') || '/'; const apiKey = process.env['TRACKER_GG_API_KEY']; if (!apiKey) { res.status(502).json({ error: 'Tracker.gg API key not configured' }); return; } const targetUrl = `${TRACKER_API_BASE}${path}`; const query = req.url.includes('?') ? req.url.substring(req.url.indexOf('?')) : ''; try { const headers: Record = { 'TRN-Api-Key': apiKey, }; const fetchOpts: RequestInit = { method: req.method, headers, }; if (req.method !== 'GET' && req.body && Object.keys(req.body).length > 0) { headers['Content-Type'] = 'application/json'; fetchOpts.body = JSON.stringify(req.body); } const response = await fetch(targetUrl + query, fetchOpts); const text = await response.text(); res.status(response.status); const contentType = response.headers.get('content-type'); if (contentType?.includes('application/json')) { res.json(JSON.parse(text || '{}')); } else { res.send(text); } } catch (err) { res.status(502).json({ error: 'Tracker.gg proxy failed' }); } }); // Serve static files from /browser server.get( '*.*', express.static(distFolder, { maxAge: '1y', }) ); // All regular routes use the Angular engine server.get('*', (req, res, next) => { const { protocol, originalUrl, baseUrl, headers } = req; commonEngine .render({ bootstrap: AppServerModule.bootstrap, providers: [ ...AppServerModule.providers, { provide: APP_BASE_HREF, useValue: baseUrl }, ] as unknown as import('@angular/core').StaticProvider[], documentFilePath: indexHtml, url: `${protocol}://${headers.host}${originalUrl}`, publicPath: distFolder, }) .then((html: string) => res.send(html)) .catch((err: unknown) => next(err)); }); return server; } function run(): void { const port = process.env['PORT'] || 4000; const server = app(); server.listen(port, () => { console.log(`Node Express server listening on http://localhost:${port}`); }); } declare const __non_webpack_require__: NodeRequire; const mainModule = __non_webpack_require__.main; const moduleFilename = (mainModule && mainModule.filename) || ''; if (moduleFilename === __filename || moduleFilename.includes('iisnode')) { run(); } export default AppServerModule.bootstrap;