Files
questionnaire/server/routes/questionnaire.ts

367 lines
11 KiB
TypeScript

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<number, number> = {};
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<number, number> = {};
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<number, number> = {};
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<number, Comment> = {};
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;