import { Router, Request, Response, NextFunction } from 'express'; import { strawPollsService } from './polls.service'; import { listStrawPollsSchema, submitStrawPollVoteSchema, submitStrawPollCommentSchema, challengeVoteSchema, } from './polls.schemas'; import { validate } from '../../middleware/validate'; import { authenticate, optionalAuth } from '../../middleware/auth.middleware'; import { strawPollVoteRateLimit, strawPollCommentRateLimit } from './polls.rate-limits'; import { pollSseService } from './polls-sse.service'; const publicRouter = Router(); // List active public polls publicRouter.get('/public', validate(listStrawPollsSchema, 'query'), async (req: Request, res: Response, next: NextFunction) => { try { const result = await strawPollsService.findAllPublic(req.query as any); res.json(result); } catch (err) { next(err); } }); // Get poll by slug (public) publicRouter.get('/public/:slug', optionalAuth, async (req: Request, res: Response, next: NextFunction) => { try { const slug = req.params.slug as string; const voterToken = req.query.voterToken as string | undefined; const clientIp = req.ip || req.socket.remoteAddress || ''; const poll = await strawPollsService.findBySlugPublic(slug, req.user?.id, voterToken, clientIp); if (!poll) return res.status(404).json({ error: 'Poll not found' }); // For AFTER_VOTE visibility, strip results if not voted if ('resultVisibility' in poll && poll.resultVisibility === 'AFTER_VOTE' && !poll.hasVoted) { const stripped = { ...poll, options: (poll as any).options?.map((o: any) => ({ ...o, voteCount: undefined })), totalVotes: undefined, showResults: false, }; return res.json(stripped); } res.json(poll); } catch (err) { next(err); } }); // Submit vote publicRouter.post('/public/:slug/vote', optionalAuth, strawPollVoteRateLimit, validate(submitStrawPollVoteSchema), async (req: Request, res: Response, next: NextFunction) => { try { const slug = req.params.slug as string; const clientIp = req.ip || req.socket.remoteAddress || ''; const result = await strawPollsService.submitVote(slug, req.body, req.user?.id, clientIp); res.json(result); } catch (err) { if (err instanceof Error && (err.message.includes('not found') || err.message.includes('not active') || err.message.includes('Invalid option'))) { return res.status(400).json({ error: err.message }); } if (err instanceof Error && err.message.includes('required')) { return res.status(401).json({ error: err.message }); } next(err); } }); // Submit comment publicRouter.post('/public/:slug/comment', optionalAuth, strawPollCommentRateLimit, validate(submitStrawPollCommentSchema), async (req: Request, res: Response, next: NextFunction) => { try { const slug = req.params.slug as string; const comment = await strawPollsService.addComment(slug, req.body, req.user?.id); res.status(201).json(comment); } catch (err) { if (err instanceof Error && err.message.includes('disabled')) { return res.status(400).json({ error: err.message }); } next(err); } }); // SSE stream for live results publicRouter.get('/public/:slug/live', (req: Request, res: Response) => { const slug = req.params.slug as string; res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', // Disable nginx buffering }); res.write(': connected\n\n'); const connectionId = pollSseService.addClient(slug, res); if (!connectionId) { res.write('event: error\ndata: {"message":"Too many connections"}\n\n'); res.end(); return; } req.on('close', () => { pollSseService.removeClient(connectionId); }); }); // Challenge a friend (requires auth) publicRouter.post('/public/:slug/challenge', authenticate, validate(challengeVoteSchema), async (req: Request, res: Response, next: NextFunction) => { try { const slug = req.params.slug as string; // Look up poll ID from slug const { prisma } = await import('../../config/database'); const poll = await prisma.strawPoll.findUnique({ where: { slug }, select: { id: true } }); if (!poll) return res.status(404).json({ error: 'Poll not found' }); const challenge = await strawPollsService.challengeFriend(poll.id, req.user!.id, req.body.challengedUserId); res.status(201).json(challenge); } catch (err) { if (err instanceof Error && err.message.includes('not found')) { return res.status(404).json({ error: err.message }); } next(err); } }); export { publicRouter as strawPollPublicRouter };