Files
tracker/server.ts
T
2026-02-13 19:58:25 +01:00

266 lines
8.4 KiB
TypeScript

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<string, string> = {
'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<string, string> = {
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<string, string> = {
'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;