266 lines
8.4 KiB
TypeScript
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;
|