import { Router, Request, Response, NextFunction } from 'express'; import { responsesService } from './responses.service'; import { submitResponseSchema, listPublicResponsesSchema, listAdminResponsesSchema, updateResponseStatusSchema, } from './responses.schemas'; import { validate } from '../../../middleware/validate'; import { authenticate } from '../../../middleware/auth.middleware'; import { optionalAuth } from '../../../middleware/auth.middleware'; import { requireRole } from '../../../middleware/rbac.middleware'; import { responseRateLimit } from '../../../middleware/rate-limit'; import { INFLUENCE_ROLES } from '../../../utils/roles'; // --- Campaign-scoped public routes (mount at /api/campaigns) --- const campaignPublicRouter = Router(); // GET /api/campaigns/:slug/responses campaignPublicRouter.get( '/:slug/responses', validate(listPublicResponsesSchema, 'query'), async (req: Request, res: Response, next: NextFunction) => { try { const slug = req.params.slug as string; const result = await responsesService.listApproved(slug, req.query as any); res.json(result); } catch (err) { next(err); } } ); // GET /api/campaigns/:slug/response-stats campaignPublicRouter.get( '/:slug/response-stats', async (req: Request, res: Response, next: NextFunction) => { try { const slug = req.params.slug as string; const stats = await responsesService.getStats(slug); res.json(stats); } catch (err) { next(err); } } ); // POST /api/campaigns/:slug/responses campaignPublicRouter.post( '/:slug/responses', responseRateLimit, validate(submitResponseSchema), async (req: Request, res: Response, next: NextFunction) => { try { const slug = req.params.slug as string; const senderIp = req.ip || req.socket.remoteAddress; const result = await responsesService.submitResponse(slug, req.body, senderIp); res.status(201).json(result); } catch (err) { next(err); } } ); // --- Response-scoped public routes (mount at /api/responses) --- const responsesPublicRouter = Router(); // POST /api/responses/:id/upvote responsesPublicRouter.post( '/:id/upvote', optionalAuth, async (req: Request, res: Response, next: NextFunction) => { try { const id = req.params.id as string; const userIp = req.ip || req.socket.remoteAddress; const userId = req.user?.id; const result = await responsesService.upvote(id, userIp, userId); res.json(result); } catch (err) { next(err); } } ); // DELETE /api/responses/:id/upvote responsesPublicRouter.delete( '/:id/upvote', optionalAuth, async (req: Request, res: Response, next: NextFunction) => { try { const id = req.params.id as string; const userIp = req.ip || req.socket.remoteAddress; const userId = req.user?.id; const result = await responsesService.removeUpvote(id, userIp, userId); res.json(result); } catch (err) { next(err); } } ); // GET /api/responses/:id/verify/:token — returns HTML page responsesPublicRouter.get( '/:id/verify/:token', async (req: Request, res: Response, next: NextFunction) => { try { const id = req.params.id as string; const token = req.params.token as string; const result = await responsesService.verify(id, token); const html = result.success ? buildResultPage('Response Verified', `Thank you for verifying this response for the "${result.campaignTitle}" campaign. The response has been approved and will now appear on the public response wall.`, '#16a34a') : buildResultPage('Verification Failed', result.reason || 'Unable to verify this response.', '#dc2626'); res.type('html').send(html); } catch (err) { next(err); } } ); // GET /api/responses/:id/report/:token — returns HTML page responsesPublicRouter.get( '/:id/report/:token', async (req: Request, res: Response, next: NextFunction) => { try { const id = req.params.id as string; const token = req.params.token as string; const result = await responsesService.report(id, token); const html = result.success ? buildResultPage('Response Reported', `This response for the "${result.campaignTitle}" campaign has been flagged as invalid and removed from the public response wall. Thank you for letting us know.`, '#dc2626') : buildResultPage('Report Failed', result.reason || 'Unable to process this report.', '#dc2626'); res.type('html').send(html); } catch (err) { next(err); } } ); // --- Admin routes (mount at /api/responses) --- const responsesAdminRouter = Router(); responsesAdminRouter.use(authenticate); responsesAdminRouter.use(requireRole(...INFLUENCE_ROLES)); // GET /api/responses responsesAdminRouter.get( '/', validate(listAdminResponsesSchema, 'query'), async (req: Request, res: Response, next: NextFunction) => { try { const result = await responsesService.findAll(req.query as any); res.json(result); } catch (err) { next(err); } } ); // PATCH /api/responses/:id/status responsesAdminRouter.patch( '/:id/status', validate(updateResponseStatusSchema), async (req: Request, res: Response, next: NextFunction) => { try { const id = req.params.id as string; const result = await responsesService.updateStatus(id, req.body); res.json(result); } catch (err) { next(err); } } ); // POST /api/responses/:id/resend-verification responsesAdminRouter.post( '/:id/resend-verification', async (req: Request, res: Response, next: NextFunction) => { try { const id = req.params.id as string; const result = await responsesService.resendVerification(id); res.json(result); } catch (err) { next(err); } } ); // DELETE /api/responses/:id responsesAdminRouter.delete( '/:id', async (req: Request, res: Response, next: NextFunction) => { try { const id = req.params.id as string; await responsesService.deleteResponse(id); res.status(204).send(); } catch (err) { next(err); } } ); export { campaignPublicRouter as responseCampaignPublicRouter, responsesPublicRouter, responsesAdminRouter }; // --- HTML page builder for verify/report endpoints --- function escapeHtml(unsafe: string): string { return unsafe .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function buildResultPage(title: string, message: string, accentColor: string): string { const escapedTitle = escapeHtml(title); const escapedMessage = escapeHtml(message); return `
${escapedMessage}