Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 40 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@sendgrid/mail": "^8.1.6",
"@sentry/node": "^10.19.0",
"@types/multer": "^2.0.0",
"@types/yauzl": "^2.10.3",
"adm-zip": "^0.5.16",
"apple-signin-auth": "^2.0.0",
"bcryptjs": "^3.0.2",
Expand All @@ -38,7 +39,8 @@
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"uuid": "^9.0.0",
"winston": "^3.10.0"
"winston": "^3.10.0",
"yauzl": "^3.2.0"
},
"devDependencies": {
"@types/adm-zip": "^0.5.7",
Expand Down
33 changes: 18 additions & 15 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ const getLogLevel = (): string => {
}
return config.isProduction() ? 'info' : 'debug';
};
import {
securityHeaders,
corsConfig,
requestSizeLimit,
import {
securityHeaders,
corsConfig,
requestSizeLimit,
securityLogger,
apiRateLimit,
progressiveSlowDown
progressiveSlowDown
} from './middleware/security';
import { requestLogger } from './middleware/requestLogger';

Expand Down Expand Up @@ -107,7 +107,7 @@ app.get('/', (req: Request, res: Response) => {
// For comprehensive metrics, use /monitoring/health
app.get('/health', async (req: Request, res: Response) => {
const startTime = Date.now();

try {
await postgresService.query('SELECT 1');
const dbLatency = Date.now() - startTime;
Expand Down Expand Up @@ -145,15 +145,18 @@ app.get('/health', async (req: Request, res: Response) => {
}
});

import { encode } from 'html-entities';

// OAuth callback handler
app.get('/auth/callback', (req: Request, res: Response) => {
const code = req.query.code;
if (code) {
if (code && typeof code === 'string') {
const sanitizedCode = encode(code);
res.send(`
<html>
<body>
<h2>✅ Authorization Successful!</h2>
<p>Authorization code: <code>${code}</code></p>
<p>Authorization code: <code>${sanitizedCode}</code></p>
<p>Copy this code and paste it into your terminal where the script is waiting.</p>
</body>
</html>
Expand Down Expand Up @@ -189,10 +192,10 @@ async function startServer() {
try {
// Initialize database tables
await postgresService.initializeTables();

// Initialize Redis cache (optional, won't fail if unavailable)
await redisConfig.initialize();

// Start the server
const server = app.listen(PORT, () => {
logger.info('Server started', {
Expand All @@ -208,18 +211,18 @@ async function startServer() {
// Graceful shutdown handling
const gracefulShutdown = async (signal: string) => {
logger.info(`${signal} received, starting graceful shutdown...`);

server.close(async () => {
logger.info('HTTP server closed');

// Close database connections
await postgresService.close();
logger.info('Database connections closed');

// Close Redis connection
await redisConfig.close();
logger.info('Redis connection closed');

logger.info('Graceful shutdown complete');
process.exit(0);
});
Expand All @@ -233,7 +236,7 @@ async function startServer() {

process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

} catch (error) {
logger.error('Failed to start server', {
error: error instanceof Error ? error.message : String(error),
Expand Down
42 changes: 42 additions & 0 deletions src/middleware/concurrencyLimiter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Request, Response, NextFunction } from 'express';

const MAX_CONCURRENT_REQUESTS = 100;
let activeRequests = 0;
const requestQueue: Array<() => void> = [];

/**
* Simple concurrency limiter for Express routes.
* Queues requests when the maximum number of active handlers is reached.
*/
export function concurrencyLimiter(req: Request, res: Response, next: NextFunction): void {
const start = () => {
activeRequests++;
let released = false;

const release = () => {
if (released) return;
released = true;
activeRequests = Math.max(0, activeRequests - 1);
const nextInQueue = requestQueue.shift();
if (nextInQueue) {
nextInQueue();
}
};

res.on('finish', release);
res.on('close', release);

// If the request was already aborted while waiting, release immediately
if (req.destroyed || req.aborted) {
return release();
}

next();
};

if (activeRequests < MAX_CONCURRENT_REQUESTS) {
start();
} else {
requestQueue.push(start);
}
}
1 change: 1 addition & 0 deletions src/models/Token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface TokenPayload {
type: 'access' | 'refresh';
iat?: number; // issued at
exp?: number; // expiration
jti?: string; // JWT ID for revocation
}

export interface AuthTokens {
Expand Down
9 changes: 6 additions & 3 deletions src/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,12 +346,15 @@ router.post('/logout', asyncHandler(async (req: Request, res: Response) => {
// In production, you might want to maintain a blacklist of invalidated tokens
// For now, client should simply discard the tokens

// TODO: Implement token blacklist/revocation in Phase 2
// For now, just return success
// Implement token blacklist/revocation
if (refreshToken) {
const authService = container.get<AuthService>('authService');
await authService.revokeRefreshToken(refreshToken);
}

res.status(200).json({
success: true,
message: 'Logged out successfully. Please discard your tokens.'
message: 'Logged out successfully.'
});
}));

Expand Down
Loading
Loading