-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathapp.ts
More file actions
180 lines (156 loc) · 6.5 KB
/
Copy pathapp.ts
File metadata and controls
180 lines (156 loc) · 6.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
/**
* Express Application Setup (Modular Architecture)
* Initializes the Express app with middleware and module-based routes
*/
import express, { type Express } from 'express';
import cors from 'cors';
import compression from 'compression';
import path from 'path';
import { fileURLToPath } from 'url';
import { getEnv } from './shared/config/env.js';
import { createLogger } from './shared/logs/logger.js';
import { GitHubClient } from './shared/utils/github-client.js';
// Module route creators
import { createStatsRouter } from './modules/stats/index.js';
import { createLanguagesRouter } from './modules/languages/index.js';
import { createGraphsRouter } from './modules/graphs/index.js';
import { createBadgesRouter } from './modules/badges/index.js';
import { createIconsRouter } from './modules/icons/index.js';
import { createHealthRouter } from './modules/health/index.js';
import { createUsersRouter } from './modules/users/index.js';
// Shared middleware
import { errorHandler, trackRequest } from './shared/middlewares/index.js';
import { securityMiddleware, rateLimiter, strictRateLimiter } from './shared/middlewares/performance.middleware.js';
import type { ResponseCache } from './shared/utils/response-cache.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const publicDir = path.join(__dirname, '..', 'public');
const logger = createLogger({ module: 'app' });
/**
* Create and configure Express application
*/
export function createApp(): Express {
const app = express();
const env = getEnv();
// Behind Cloudflare → nginx (one hop). Needed so req.ip is the real client
// IP for per-IP rate limiting and downstream visitor-dedup work; without
// this, everyone shares the nginx-loopback bucket.
app.set('trust proxy', 1);
// ⚡️ PERFORMANCE: Enable gzip compression for responses
app.use(compression({
level: 6,
threshold: 1024,
}));
// 🔒 SECURITY: Helmet-based headers (see performance.middleware for the
// rationale on CSP/COEP/CORP tuning for cross-origin badge embedding).
app.use(securityMiddleware);
// 🚦 RATE LIMIT: Global 1000 req / 15 min per IP. Endpoint-specific limits
// for GitHub-hitting routes are applied in `initializeRoutes`.
app.use(rateLimiter);
// CORS Configuration.
//
// In production, only our own origins may send credentialed requests.
// In dev, we accept any origin but drop `credentials` — the combination
// of `Access-Control-Allow-Origin: *` + `credentials: true` is invalid
// per spec and browsers refuse it anyway, but leaving `credentials: true`
// there previously masked the misconfig and encouraged relying on it.
app.use(cors(
env.APP_ENV === 'production'
? {
origin: ['https://stats.pphat.top', 'https://pphat.top'],
methods: ['GET', 'POST'],
credentials: true,
}
: {
origin: '*',
methods: ['GET', 'POST'],
credentials: false,
},
));
// Body Parsing Middleware. All API routes are GET so a 10 MB budget was
// pure attack surface (L3). Kept minimal for any future POST endpoints.
app.use(express.json({ limit: '100kb' }));
app.use(express.urlencoded({ extended: true, limit: '100kb' }));
// Static File Serving. Single mount at `/` (I1). External callers that
// used the `/public/...` prefix should update to `/...`; drop the alias
// after grep confirms nothing external still depends on it.
app.use(express.static(publicDir));
// Request Logging Middleware (Development only)
if (env.DEBUG) {
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.debug(`${req.method} ${req.path}`, {
method: req.method,
path: req.path,
status: res.statusCode,
duration: `${duration}ms`,
});
});
next();
});
}
logger.info('Express middleware configured');
return app;
}
/**
* Initialize application routes using modular structure
*/
export function initializeRoutes(
app: Express,
githubClient: GitHubClient,
cache: ResponseCache<any>,
cacheDuration: number,
cacheService?: any
): void {
const logger = createLogger({ module: 'routes' });
// Root route
app.get('/', (req, res) => {
res.json({
name: 'GitHub Stats API',
version: '2.0.0',
description: 'Modern GitHub statistics and badge generation service',
documentation: '/api-docs',
endpoints: {
stats: '/stats',
languages: '/languages',
graphs: '/graph',
badges: '/badges',
icons: '/icons',
health: '/health',
users: '/users'
}
});
});
// Mount module routes. `trackRequest` logs every card request (including
// programmatic/bot user-agents) into `stats_requests` for the admin dashboard.
// `strictRateLimiter` is layered on /stats and /badges because both may
// fan out to the GitHub API on cache miss — the global rateLimiter alone
// would let a hot spot burn through the API quota.
app.use('/stats', strictRateLimiter, trackRequest, createStatsRouter(githubClient, cache, cacheDuration));
app.use('/languages', trackRequest, createLanguagesRouter(githubClient, cache, cacheDuration));
app.use('/graph', trackRequest, createGraphsRouter(githubClient, cache, cacheDuration));
app.use('/badges', strictRateLimiter, trackRequest, createBadgesRouter(githubClient, cache, cacheDuration));
app.use('/icons', createIconsRouter());
app.use('/health', createHealthRouter(cacheService));
app.use('/users', createUsersRouter());
logger.info('Module routes registered');
}
/**
* Setup error handlers for the application
*/
export function setupErrorHandlers(app: Express): void {
const logger = createLogger({ module: 'error-handler' });
// 404 Handler
app.use((req, res) => {
res.status(404).json({
error: 'Not Found',
message: `Route ${req.method} ${req.path} not found`,
documentation: '/api-docs',
});
});
// Global Error Handler
app.use(errorHandler(logger));
logger.info('Error handlers configured');
}