bunker-admin c192c04c79 Security audit: fix 25 findings across API, nginx, and Docker
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
2026-03-09 14:13:37 -06:00

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;