Docker, API proxy, Drone CI, .env
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user