Addresses data exposure, access control, input validation, infrastructure hardening, and supply chain security issues identified during audit. Key changes: - Strip internal fields from public campaign/profile/comment endpoints - Restrict docs routes to CONTENT_ROLES, provisioning to SUPER_ADMIN - Add SSE connection limits, social middleware fail-closed behavior - Bind all non-nginx ports to 127.0.0.1, pin container image versions - Add CSP header, conditional HSTS, token redaction in nginx logs - Validate nav URLs, calendar schemas, video tracking batch events - Reject default admin password placeholder, add SSRF protocol checks - Exclude .env from Code Server, enforce RC admin password in compose - Add Zod validation for achievement grant/revoke, webhook secret header - Fix path traversal prefix attack, add calendar token expiry Bunker Admin
71 lines
2.2 KiB
TypeScript
71 lines
2.2 KiB
TypeScript
import { Router } from 'express';
|
|
import { checkSocialEnabled } from './social.middleware';
|
|
import { requireRole } from '../../middleware/rbac.middleware';
|
|
import { SOCIAL_ROLES } from '../../utils/roles';
|
|
import { sseService } from './sse.service';
|
|
import { presenceService } from './presence.service';
|
|
|
|
const MAX_SSE_CONNECTIONS_PER_USER = 5;
|
|
|
|
const router = Router();
|
|
|
|
router.use(checkSocialEnabled);
|
|
|
|
/** GET /api/social/sse — establish SSE connection */
|
|
router.get('/', (req, res) => {
|
|
const userId = req.user!.id;
|
|
|
|
// Enforce per-user connection limit to prevent resource exhaustion
|
|
const existingCount = sseService.getConnectionCountForUser?.(userId) ?? 0;
|
|
if (existingCount >= MAX_SSE_CONNECTIONS_PER_USER) {
|
|
res.status(429).json({ error: { message: 'Too many SSE connections', code: 'TOO_MANY_CONNECTIONS' } });
|
|
return;
|
|
}
|
|
|
|
// Set SSE headers
|
|
res.writeHead(200, {
|
|
'Content-Type': 'text/event-stream',
|
|
'Cache-Control': 'no-cache',
|
|
'Connection': 'keep-alive',
|
|
'X-Accel-Buffering': 'no', // Disable nginx buffering
|
|
});
|
|
|
|
// Send initial connection event
|
|
res.write(`event: connected\ndata: ${JSON.stringify({ userId })}\n\n`);
|
|
|
|
// Register client
|
|
const connectionId = sseService.addClient(userId, res);
|
|
|
|
// Set user as online
|
|
presenceService.setOnline(userId).catch(() => {});
|
|
|
|
// Handle client disconnect
|
|
req.on('close', () => {
|
|
sseService.removeClient(connectionId);
|
|
// Only set offline if no more connections for this user
|
|
if (!sseService.isConnected(userId)) {
|
|
presenceService.setOffline(userId).catch(() => {});
|
|
}
|
|
});
|
|
});
|
|
|
|
/** GET /api/social/sse/online-friends — get currently online friends */
|
|
router.get('/online-friends', async (req, res, next) => {
|
|
try {
|
|
const friends = await presenceService.getOnlineFriends(req.user!.id);
|
|
res.json({ friends });
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
/** GET /api/social/sse/status — SSE service status (admin only) */
|
|
router.get('/status', requireRole(...SOCIAL_ROLES), (_req, res) => {
|
|
res.json({
|
|
connections: sseService.getConnectionCount(),
|
|
connectedUsers: sseService.getConnectedUserIds().length,
|
|
});
|
|
});
|
|
|
|
export const sseRouter = router;
|