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
115 lines
4.3 KiB
TypeScript
115 lines
4.3 KiB
TypeScript
import { Router } from 'express';
|
|
import { z } from 'zod';
|
|
import { socialAdminService } from './social-admin.service';
|
|
import { requireRole } from '../../middleware/rbac.middleware';
|
|
import { SOCIAL_ROLES } from '../../utils/roles';
|
|
import { logger } from '../../utils/logger';
|
|
|
|
const router = Router();
|
|
|
|
// Self-contained auth guard (defense-in-depth — parent also applies requireRole)
|
|
router.use(requireRole(...SOCIAL_ROLES));
|
|
|
|
/** GET /api/social/admin/stats — Dashboard overview */
|
|
router.get('/stats', async (_req, res) => {
|
|
try {
|
|
const stats = await socialAdminService.getDashboardStats();
|
|
res.json(stats);
|
|
} catch (err) {
|
|
logger.error('Social admin stats error:', err);
|
|
res.status(500).json({ error: 'Failed to fetch social stats' });
|
|
}
|
|
});
|
|
|
|
/** GET /api/social/admin/graph — Full social graph */
|
|
router.get('/graph', async (req, res) => {
|
|
try {
|
|
const role = req.query.role as string | undefined;
|
|
const search = req.query.search as string | undefined;
|
|
const data = await socialAdminService.getGraphData({ role, search });
|
|
res.json(data);
|
|
} catch (err) {
|
|
logger.error('Social admin graph error:', err);
|
|
res.status(500).json({ error: 'Failed to fetch social graph' });
|
|
}
|
|
});
|
|
|
|
/** GET /api/social/admin/graph/ego/:userId — Ego-centric subgraph */
|
|
router.get('/graph/ego/:userId', async (req, res) => {
|
|
try {
|
|
const userId = req.params.userId as string;
|
|
const maxHops = parseInt(req.query.maxHops as string) || 2;
|
|
const data = await socialAdminService.getEgoGraphData(userId, Math.min(maxHops, 3));
|
|
res.json(data);
|
|
} catch (err) {
|
|
logger.error('Social admin ego graph error:', err);
|
|
res.status(500).json({ error: 'Failed to fetch ego graph' });
|
|
}
|
|
});
|
|
|
|
/** GET /api/social/admin/moderation — Moderation overview */
|
|
router.get('/moderation', async (req, res) => {
|
|
try {
|
|
const page = parseInt(req.query.page as string) || 1;
|
|
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
|
const data = await socialAdminService.getModerationData(page, limit);
|
|
res.json(data);
|
|
} catch (err) {
|
|
logger.error('Social admin moderation error:', err);
|
|
res.status(500).json({ error: 'Failed to fetch moderation data' });
|
|
}
|
|
});
|
|
|
|
/** POST /api/social/admin/blocks/:id/remove — Force-remove a block */
|
|
router.post('/blocks/:id/remove', async (req, res) => {
|
|
try {
|
|
const blockId = parseInt(req.params.id as string);
|
|
if (isNaN(blockId)) return res.status(400).json({ error: 'Invalid block ID' });
|
|
await socialAdminService.removeBlock(blockId);
|
|
res.json({ success: true });
|
|
} catch (err: unknown) {
|
|
const statusCode = (err as { statusCode?: number }).statusCode ?? 500;
|
|
const message = err instanceof Error ? err.message : 'Failed to remove block';
|
|
res.status(statusCode).json({ error: message });
|
|
}
|
|
});
|
|
|
|
const achievementSchema = z.object({
|
|
userId: z.string().min(1).max(100),
|
|
achievementId: z.string().min(1).max(100),
|
|
});
|
|
|
|
/** POST /api/social/admin/achievements/grant — Grant achievement */
|
|
router.post('/achievements/grant', async (req, res) => {
|
|
try {
|
|
const parsed = achievementSchema.safeParse(req.body);
|
|
if (!parsed.success) {
|
|
return res.status(400).json({ error: 'Valid userId and achievementId are required' });
|
|
}
|
|
const result = await socialAdminService.grantAchievement(parsed.data.userId, parsed.data.achievementId);
|
|
res.json(result);
|
|
} catch (err: unknown) {
|
|
const statusCode = (err as { statusCode?: number }).statusCode ?? 500;
|
|
const message = err instanceof Error ? err.message : 'Failed to grant achievement';
|
|
res.status(statusCode).json({ error: message });
|
|
}
|
|
});
|
|
|
|
/** POST /api/social/admin/achievements/revoke — Revoke achievement */
|
|
router.post('/achievements/revoke', async (req, res) => {
|
|
try {
|
|
const parsed = achievementSchema.safeParse(req.body);
|
|
if (!parsed.success) {
|
|
return res.status(400).json({ error: 'Valid userId and achievementId are required' });
|
|
}
|
|
await socialAdminService.revokeAchievement(parsed.data.userId, parsed.data.achievementId);
|
|
res.json({ success: true });
|
|
} catch (err: unknown) {
|
|
const statusCode = (err as { statusCode?: number }).statusCode ?? 500;
|
|
const message = err instanceof Error ? err.message : 'Failed to revoke achievement';
|
|
res.status(statusCode).json({ error: message });
|
|
}
|
|
});
|
|
|
|
export { router as socialAdminRouter };
|