import { Router, Request, Response } from 'express'; import { questionnaireOps, activityOps, voteOps, commentOps, participantOps, Comment, Questionnaire } from '../database.js'; // Helper to check if user can participate (write access) function canParticipate(questionnaire: Questionnaire, req: Request): { canWrite: boolean; participantName: string | null } { // Check for participant token first const token = req.cookies.participantToken; if (token) { const participant = participantOps.findByToken(token); if (participant && participant.questionnaire_id === questionnaire.id) { return { canWrite: true, participantName: participant.name }; } } // For public questionnaires, use visitor name if (!questionnaire.is_private) { const visitorName = req.cookies.visitorName; return { canWrite: !!visitorName, participantName: visitorName || null }; } // Private questionnaire without valid token return { canWrite: false, participantName: null }; } const router = Router(); // Access questionnaire via participant token router.get('/token/:token', (req: Request, res: Response) => { const participant = participantOps.findByToken(req.params.token); if (!participant) { res.status(404).json({ error: 'Ongeldige toegangslink' }); return; } const questionnaire = questionnaireOps.findById(participant.questionnaire_id); if (!questionnaire) { res.status(404).json({ error: 'Vragenlijst niet gevonden' }); return; } // Set participant token cookie res.cookie('participantToken', participant.token, { maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days httpOnly: true, sameSite: 'lax', }); const activities = activityOps.getByQuestionnaire(questionnaire.id); let userVotes: Record = {}; const votes = voteOps.getByQuestionnaireAndVoter(questionnaire.id, participant.name); votes.forEach(v => { userVotes[v.activity_id] = v.vote_type; }); res.json({ questionnaire, activities, visitorName: participant.name, userVotes, isParticipant: true, canWrite: true, }); }); // Get questionnaire by slug (public - may be read-only for private questionnaires) router.get('/:slug', (req: Request, res: Response) => { const questionnaire = questionnaireOps.findBySlug(req.params.slug); if (!questionnaire) { res.status(404).json({ error: 'Vragenlijst niet gevonden' }); return; } const activities = activityOps.getByQuestionnaire(questionnaire.id); const { canWrite, participantName } = canParticipate(questionnaire, req); let userVotes: Record = {}; if (participantName) { const votes = voteOps.getByQuestionnaireAndVoter(questionnaire.id, participantName); votes.forEach(v => { userVotes[v.activity_id] = v.vote_type; }); } res.json({ questionnaire, activities, visitorName: participantName || '', userVotes, isPrivate: !!questionnaire.is_private, canWrite, }); }); // Set visitor name (only for public questionnaires) router.post('/:slug/set-name', (req: Request, res: Response) => { const { name } = req.body; const questionnaire = questionnaireOps.findBySlug(req.params.slug); if (!questionnaire) { res.status(404).json({ error: 'Vragenlijst niet gevonden' }); return; } if (questionnaire.is_private) { res.status(403).json({ error: 'Voor deze vragenlijst is een uitnodigingslink nodig' }); return; } if (!name?.trim()) { res.status(400).json({ error: 'Naam is verplicht' }); return; } res.cookie('visitorName', name.trim(), { maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days httpOnly: true, sameSite: 'lax', }); res.json({ success: true, name: name.trim() }); }); // Clear visitor name router.post('/:slug/clear-name', (req: Request, res: Response) => { res.clearCookie('visitorName'); res.clearCookie('participantToken'); res.json({ success: true }); }); // Add activity router.post('/:slug/activities', (req: Request, res: Response) => { const { name, description } = req.body; const questionnaire = questionnaireOps.findBySlug(req.params.slug); if (!questionnaire) { res.status(404).json({ error: 'Vragenlijst niet gevonden' }); return; } const { canWrite, participantName } = canParticipate(questionnaire, req); if (!canWrite || !participantName) { if (questionnaire.is_private) { res.status(403).json({ error: 'Je hebt een uitnodigingslink nodig om deel te nemen' }); } else { res.status(401).json({ error: 'Voer eerst je naam in' }); } return; } if (!name?.trim()) { res.status(400).json({ error: 'Naam activiteit is verplicht' }); return; } const activityDescription = description?.trim() || null; const activityId = activityOps.create(questionnaire.id, name.trim(), participantName, activityDescription); const activities = activityOps.getByQuestionnaire(questionnaire.id); const activity = activities.find(a => a.id === activityId); res.json({ success: true, activity }); }); // Update activity (only by the user who added it) router.put('/:slug/activities/:activityId', (req: Request, res: Response) => { const { name, description } = req.body; const questionnaire = questionnaireOps.findBySlug(req.params.slug); if (!questionnaire) { res.status(404).json({ error: 'Vragenlijst niet gevonden' }); return; } const { canWrite, participantName } = canParticipate(questionnaire, req); if (!canWrite || !participantName) { if (questionnaire.is_private) { res.status(403).json({ error: 'Je hebt een uitnodigingslink nodig om te bewerken' }); } else { res.status(401).json({ error: 'Voer eerst je naam in' }); } return; } const activity = activityOps.findById(parseInt(req.params.activityId)); if (!activity || activity.questionnaire_id !== questionnaire.id) { res.status(404).json({ error: 'Activiteit niet gevonden' }); return; } // Check if the user is the one who added the activity if (activity.added_by !== participantName) { res.status(403).json({ error: 'Je kunt alleen je eigen activiteiten bewerken' }); return; } if (!name?.trim()) { res.status(400).json({ error: 'Naam activiteit is verplicht' }); return; } activityOps.update(activity.id, name.trim(), description?.trim() || null); const activities = activityOps.getByQuestionnaire(questionnaire.id); const updatedActivity = activities.find(a => a.id === activity.id); res.json({ success: true, activity: updatedActivity }); }); // Vote on activity router.post('/:slug/activities/:activityId/vote', (req: Request, res: Response) => { const { voteType } = req.body; const questionnaire = questionnaireOps.findBySlug(req.params.slug); if (!questionnaire) { res.status(404).json({ error: 'Vragenlijst niet gevonden' }); return; } const { canWrite, participantName } = canParticipate(questionnaire, req); if (!canWrite || !participantName) { if (questionnaire.is_private) { res.status(403).json({ error: 'Je hebt een uitnodigingslink nodig om te stemmen' }); } else { res.status(401).json({ error: 'Voer eerst je naam in' }); } return; } const activity = activityOps.findById(parseInt(req.params.activityId)); if (!activity || activity.questionnaire_id !== questionnaire.id) { res.status(404).json({ error: 'Activiteit niet gevonden' }); return; } if (voteType !== 1 && voteType !== -1) { res.status(400).json({ error: 'Ongeldig stemtype' }); return; } const result = voteOps.upsert(activity.id, participantName, voteType); const activities = activityOps.getByQuestionnaire(questionnaire.id); const updatedActivity = activities.find(a => a.id === activity.id); res.json({ success: true, action: result.action, currentVote: result.voteType, activity: updatedActivity, }); }); // Get activities router.get('/:slug/activities', (req: Request, res: Response) => { const questionnaire = questionnaireOps.findBySlug(req.params.slug); if (!questionnaire) { res.status(404).json({ error: 'Vragenlijst niet gevonden' }); return; } const activities = activityOps.getByQuestionnaire(questionnaire.id); const visitorName = req.cookies.visitorName || ''; let userVotes: Record = {}; if (visitorName) { const votes = voteOps.getByQuestionnaireAndVoter(questionnaire.id, visitorName); votes.forEach(v => { userVotes[v.activity_id] = v.vote_type; }); } res.json({ activities, userVotes }); }); // Get comments for an activity router.get('/:slug/activities/:activityId/comments', (req: Request, res: Response) => { const questionnaire = questionnaireOps.findBySlug(req.params.slug); if (!questionnaire) { res.status(404).json({ error: 'Vragenlijst niet gevonden' }); return; } const activity = activityOps.findById(parseInt(req.params.activityId)); if (!activity || activity.questionnaire_id !== questionnaire.id) { res.status(404).json({ error: 'Activiteit niet gevonden' }); return; } const comments = commentOps.getByActivity(activity.id); // Build nested comment tree const commentMap: Record = {}; const rootComments: Comment[] = []; comments.forEach(comment => { comment.replies = []; commentMap[comment.id] = comment; }); comments.forEach(comment => { if (comment.parent_id) { if (commentMap[comment.parent_id]) { commentMap[comment.parent_id].replies!.push(comment); } } else { rootComments.push(comment); } }); res.json({ comments: rootComments, activity }); }); // Add a comment router.post('/:slug/activities/:activityId/comments', (req: Request, res: Response) => { const { content, parentId } = req.body; const questionnaire = questionnaireOps.findBySlug(req.params.slug); if (!questionnaire) { res.status(404).json({ error: 'Vragenlijst niet gevonden' }); return; } const { canWrite, participantName } = canParticipate(questionnaire, req); if (!canWrite || !participantName) { if (questionnaire.is_private) { res.status(403).json({ error: 'Je hebt een uitnodigingslink nodig om te reageren' }); } else { res.status(401).json({ error: 'Voer eerst je naam in' }); } return; } const activity = activityOps.findById(parseInt(req.params.activityId)); if (!activity || activity.questionnaire_id !== questionnaire.id) { res.status(404).json({ error: 'Activiteit niet gevonden' }); return; } if (!content?.trim()) { res.status(400).json({ error: 'Reactie inhoud is verplicht' }); return; } if (parentId) { const parentComment = commentOps.getById(parentId); if (!parentComment || parentComment.activity_id !== activity.id) { res.status(400).json({ error: 'Ongeldige reactie om op te reageren' }); return; } } const commentId = commentOps.create(activity.id, participantName, content.trim(), parentId || null); const comment = commentOps.getById(commentId); res.json({ success: true, comment: { ...comment, replies: [] } }); }); export default router;