1047 lines
34 KiB
JavaScript
1047 lines
34 KiB
JavaScript
const nocodbService = require('../services/nocodb');
|
||
const emailService = require('../services/email');
|
||
const crypto = require('crypto');
|
||
const { validateResponse } = require('../utils/validators');
|
||
|
||
/**
|
||
* Get all responses for a campaign
|
||
* Public endpoint - no authentication required
|
||
* @param {Object} req - Express request object
|
||
* @param {Object} res - Express response object
|
||
*/
|
||
async function getCampaignResponses(req, res) {
|
||
try {
|
||
const { slug } = req.params;
|
||
const { sort = 'recent', level = '', offset = 0, limit = 20, status = 'approved' } = req.query;
|
||
|
||
// Get campaign by slug first
|
||
const campaign = await nocodbService.getCampaignBySlug(slug);
|
||
if (!campaign) {
|
||
return res.status(404).json({ error: 'Campaign not found' });
|
||
}
|
||
|
||
// Build filter conditions
|
||
// NocoDB v2 API requires ~and operator between multiple conditions
|
||
let whereConditions = [
|
||
`(Campaign Slug,eq,${slug})~and(Status,eq,${status})`
|
||
];
|
||
|
||
if (level) {
|
||
whereConditions[0] += `~and(Representative Level,eq,${level})`;
|
||
}
|
||
|
||
// Build sort order
|
||
let sortOrder = '-CreatedAt'; // Default to most recent
|
||
if (sort === 'upvotes') {
|
||
sortOrder = '-Upvote Count';
|
||
} else if (sort === 'verified') {
|
||
sortOrder = '-Is Verified,-CreatedAt';
|
||
}
|
||
|
||
// Fetch responses
|
||
const responses = await nocodbService.getRepresentativeResponses({
|
||
where: whereConditions[0], // Use the first (and only) element
|
||
sort: sortOrder,
|
||
offset: parseInt(offset),
|
||
limit: parseInt(limit)
|
||
});
|
||
|
||
console.log(`Found ${responses.length} responses for campaign ${slug} with status ${status}`);
|
||
if (responses.length > 0) {
|
||
console.log('First response sample:', JSON.stringify(responses[0], null, 2));
|
||
}
|
||
|
||
// For each response, check if current user has upvoted
|
||
const userId = req.user?.id;
|
||
const userEmail = req.user?.email;
|
||
|
||
const responsesWithUpvoteStatus = await Promise.all(responses.map(async (response) => {
|
||
let hasUpvoted = false;
|
||
|
||
if (userId || userEmail) {
|
||
const upvotes = await nocodbService.getResponseUpvotes({
|
||
where: `(Response ID,eq,${response.id})`
|
||
});
|
||
|
||
hasUpvoted = upvotes.some(upvote =>
|
||
(userId && upvote.user_id === userId) ||
|
||
(userEmail && upvote.user_email === userEmail)
|
||
);
|
||
}
|
||
|
||
return {
|
||
...response,
|
||
hasUpvoted
|
||
};
|
||
}));
|
||
|
||
res.json({
|
||
success: true,
|
||
responses: responsesWithUpvoteStatus,
|
||
pagination: {
|
||
offset: parseInt(offset),
|
||
limit: parseInt(limit),
|
||
hasMore: responses.length === parseInt(limit)
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Error getting campaign responses:', error);
|
||
res.status(500).json({ error: 'Failed to get campaign responses' });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Submit a new response
|
||
* Optional authentication - allows anonymous submissions
|
||
* @param {Object} req - Express request object
|
||
* @param {Object} res - Express response object
|
||
*/
|
||
async function submitResponse(req, res) {
|
||
try {
|
||
const { slug } = req.params;
|
||
const responseData = req.body;
|
||
|
||
// Get campaign by slug first
|
||
const campaign = await nocodbService.getCampaignBySlug(slug);
|
||
if (!campaign) {
|
||
return res.status(404).json({ error: 'Campaign not found' });
|
||
}
|
||
|
||
// Validate response data
|
||
const validation = validateResponse(responseData);
|
||
if (!validation.valid) {
|
||
return res.status(400).json({ error: validation.error });
|
||
}
|
||
|
||
// Handle file upload if present
|
||
let screenshotUrl = null;
|
||
if (req.file) {
|
||
screenshotUrl = `/uploads/responses/${req.file.filename}`;
|
||
}
|
||
|
||
// DEBUG: Log verification-related fields
|
||
console.log('=== VERIFICATION DEBUG ===');
|
||
console.log('send_verification from form:', responseData.send_verification);
|
||
console.log('representative_email from form:', responseData.representative_email);
|
||
console.log('representative_name from form:', responseData.representative_name);
|
||
|
||
// Generate verification token if verification is requested and email is provided
|
||
let verificationToken = null;
|
||
let verificationSentAt = null;
|
||
|
||
// Handle send_verification - could be string, boolean, or array from form
|
||
let sendVerificationValue = responseData.send_verification;
|
||
if (Array.isArray(sendVerificationValue)) {
|
||
// If it's an array, check if any value indicates true
|
||
sendVerificationValue = sendVerificationValue.some(val => val === 'true' || val === true || val === 'on');
|
||
}
|
||
const sendVerification = sendVerificationValue === 'true' || sendVerificationValue === true || sendVerificationValue === 'on';
|
||
console.log('sendVerification evaluated to:', sendVerification);
|
||
|
||
// Handle representative_email - could be string or array from form
|
||
let representativeEmail = responseData.representative_email;
|
||
if (Array.isArray(representativeEmail)) {
|
||
representativeEmail = representativeEmail[0]; // Take first email if array
|
||
}
|
||
representativeEmail = representativeEmail || null;
|
||
console.log('representativeEmail after processing:', representativeEmail);
|
||
|
||
if (sendVerification && representativeEmail) {
|
||
// Generate a secure random token
|
||
verificationToken = crypto.randomBytes(32).toString('hex');
|
||
verificationSentAt = new Date().toISOString();
|
||
console.log('Generated verification token:', verificationToken.substring(0, 16) + '...');
|
||
console.log('Verification sent at:', verificationSentAt);
|
||
} else {
|
||
console.log('Skipping verification token generation. sendVerification:', sendVerification, 'representativeEmail:', representativeEmail);
|
||
}
|
||
|
||
// Normalize is_anonymous checkbox value
|
||
const isAnonymous = responseData.is_anonymous === true ||
|
||
responseData.is_anonymous === 'true' ||
|
||
responseData.is_anonymous === 'on';
|
||
|
||
// Prepare response data for NocoDB
|
||
const newResponse = {
|
||
campaign_id: campaign.ID || campaign.Id || campaign.id || campaign['Campaign ID'],
|
||
campaign_slug: slug,
|
||
representative_name: responseData.representative_name,
|
||
representative_title: responseData.representative_title || null,
|
||
representative_level: responseData.representative_level,
|
||
response_type: responseData.response_type,
|
||
response_text: responseData.response_text,
|
||
user_comment: responseData.user_comment || null,
|
||
screenshot_url: screenshotUrl,
|
||
submitted_by_name: responseData.submitted_by_name || null,
|
||
submitted_by_email: responseData.submitted_by_email || null,
|
||
submitted_by_user_id: req.user?.id || null,
|
||
is_anonymous: isAnonymous,
|
||
status: 'pending', // All submissions start as pending
|
||
is_verified: false,
|
||
representative_email: representativeEmail,
|
||
verification_token: verificationToken,
|
||
verification_sent_at: verificationSentAt,
|
||
verified_at: null,
|
||
verified_by: null,
|
||
upvote_count: 0,
|
||
submitted_ip: req.ip || req.connection.remoteAddress
|
||
};
|
||
|
||
console.log('Submitting response with campaign_id:', newResponse.campaign_id, 'from campaign:', campaign);
|
||
|
||
// Create response in database
|
||
const createdResponse = await nocodbService.createRepresentativeResponse(newResponse);
|
||
|
||
// Send verification email if requested
|
||
let verificationEmailSent = false;
|
||
if (sendVerification && representativeEmail && verificationToken) {
|
||
try {
|
||
const baseUrl = process.env.BASE_URL || `${req.protocol}://${req.get('host')}`;
|
||
const verificationUrl = `${baseUrl}/api/responses/${createdResponse.id}/verify/${verificationToken}`;
|
||
const reportUrl = `${baseUrl}/api/responses/${createdResponse.id}/report/${verificationToken}`;
|
||
|
||
const campaignTitle = campaign.Title || campaign.title || 'Unknown Campaign';
|
||
const submittedDate = new Date().toLocaleDateString('en-US', {
|
||
year: 'numeric',
|
||
month: 'long',
|
||
day: 'numeric'
|
||
});
|
||
|
||
await emailService.sendResponseVerification({
|
||
representativeEmail,
|
||
representativeName: responseData.representative_name,
|
||
campaignTitle,
|
||
responseType: responseData.response_type,
|
||
responseText: responseData.response_text,
|
||
submittedDate,
|
||
submitterName: responseData.is_anonymous ? 'Anonymous' : (responseData.submitted_by_name || 'A constituent'),
|
||
verificationUrl,
|
||
reportUrl
|
||
});
|
||
|
||
verificationEmailSent = true;
|
||
console.log('Verification email sent successfully to:', representativeEmail);
|
||
} catch (emailError) {
|
||
console.error('Failed to send verification email:', emailError);
|
||
// Don't fail the whole request if email fails
|
||
}
|
||
}
|
||
|
||
const responseMessage = verificationEmailSent
|
||
? 'Response submitted successfully. A verification email has been sent to the representative. Your response will be visible after moderation.'
|
||
: 'Response submitted successfully. It will be visible after moderation.';
|
||
|
||
res.status(201).json({
|
||
success: true,
|
||
message: responseMessage,
|
||
response: createdResponse,
|
||
verificationEmailSent
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Error submitting response:', error);
|
||
res.status(500).json({ error: 'Failed to submit response' });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Upvote a response
|
||
* Optional authentication - allows anonymous upvotes with IP tracking
|
||
* @param {Object} req - Express request object
|
||
* @param {Object} res - Express response object
|
||
*/
|
||
async function upvoteResponse(req, res) {
|
||
try {
|
||
const { id } = req.params;
|
||
|
||
// Get response
|
||
const response = await nocodbService.getRepresentativeResponseById(id);
|
||
if (!response) {
|
||
return res.status(404).json({ error: 'Response not found' });
|
||
}
|
||
|
||
const userId = req.user?.id;
|
||
const userEmail = req.user?.email;
|
||
const userIp = req.ip || req.connection.remoteAddress;
|
||
|
||
// Check if already upvoted
|
||
const existingUpvotes = await nocodbService.getResponseUpvotes({
|
||
where: `(Response ID,eq,${id})`
|
||
});
|
||
|
||
const alreadyUpvoted = existingUpvotes.some(upvote =>
|
||
(userId && upvote.user_id === userId) ||
|
||
(userEmail && upvote.user_email === userEmail) ||
|
||
(upvote.upvoted_ip === userIp)
|
||
);
|
||
|
||
if (alreadyUpvoted) {
|
||
return res.status(400).json({ error: 'You have already upvoted this response' });
|
||
}
|
||
|
||
// Create upvote
|
||
await nocodbService.createResponseUpvote({
|
||
response_id: id,
|
||
user_id: userId || null,
|
||
user_email: userEmail || null,
|
||
upvoted_ip: userIp
|
||
});
|
||
|
||
// Increment upvote count
|
||
const newCount = (response.upvote_count || 0) + 1;
|
||
await nocodbService.updateRepresentativeResponse(id, {
|
||
upvote_count: newCount
|
||
});
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Response upvoted successfully',
|
||
upvoteCount: newCount
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Error upvoting response:', error);
|
||
res.status(500).json({ error: 'Failed to upvote response' });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Remove upvote from a response
|
||
* Optional authentication
|
||
* @param {Object} req - Express request object
|
||
* @param {Object} res - Express response object
|
||
*/
|
||
async function removeUpvote(req, res) {
|
||
try {
|
||
const { id } = req.params;
|
||
|
||
// Get response
|
||
const response = await nocodbService.getRepresentativeResponseById(id);
|
||
if (!response) {
|
||
return res.status(404).json({ error: 'Response not found' });
|
||
}
|
||
|
||
const userId = req.user?.id;
|
||
const userEmail = req.user?.email;
|
||
const userIp = req.ip || req.connection.remoteAddress;
|
||
|
||
// Find upvote to remove
|
||
const upvotes = await nocodbService.getResponseUpvotes({
|
||
where: `(Response ID,eq,${id})`
|
||
});
|
||
|
||
const upvoteToRemove = upvotes.find(upvote =>
|
||
(userId && upvote.user_id === userId) ||
|
||
(userEmail && upvote.user_email === userEmail) ||
|
||
(upvote.upvoted_ip === userIp)
|
||
);
|
||
|
||
if (!upvoteToRemove) {
|
||
return res.status(400).json({ error: 'You have not upvoted this response' });
|
||
}
|
||
|
||
// Delete upvote
|
||
await nocodbService.deleteResponseUpvote(upvoteToRemove.id);
|
||
|
||
// Decrement upvote count
|
||
const newCount = Math.max((response.upvote_count || 0) - 1, 0);
|
||
await nocodbService.updateRepresentativeResponse(id, {
|
||
upvote_count: newCount
|
||
});
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Upvote removed successfully',
|
||
upvoteCount: newCount
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Error removing upvote:', error);
|
||
res.status(500).json({ error: 'Failed to remove upvote' });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get response statistics for a campaign
|
||
* Public endpoint
|
||
* @param {Object} req - Express request object
|
||
* @param {Object} res - Express response object
|
||
*/
|
||
async function getResponseStats(req, res) {
|
||
try {
|
||
const { slug } = req.params;
|
||
|
||
// Get campaign by slug first
|
||
const campaign = await nocodbService.getCampaignBySlug(slug);
|
||
if (!campaign) {
|
||
return res.status(404).json({ error: 'Campaign not found' });
|
||
}
|
||
|
||
// Get all approved responses for this campaign
|
||
const responses = await nocodbService.getRepresentativeResponses({
|
||
where: `(Campaign Slug,eq,${slug})~and(Status,eq,approved)`
|
||
});
|
||
|
||
// Calculate stats
|
||
const totalResponses = responses.length;
|
||
const verifiedResponses = responses.filter(r => r.is_verified).length;
|
||
const totalUpvotes = responses.reduce((sum, r) => sum + (r.upvote_count || 0), 0);
|
||
|
||
// Count by level
|
||
const byLevel = {
|
||
Federal: responses.filter(r => r.representative_level === 'Federal').length,
|
||
Provincial: responses.filter(r => r.representative_level === 'Provincial').length,
|
||
Municipal: responses.filter(r => r.representative_level === 'Municipal').length,
|
||
'School Board': responses.filter(r => r.representative_level === 'School Board').length
|
||
};
|
||
|
||
res.json({
|
||
success: true,
|
||
campaign: {
|
||
title: campaign['Campaign Title'] || campaign.title,
|
||
description: campaign['Description'] || campaign.description,
|
||
slug: campaign['Campaign Slug'] || campaign.slug,
|
||
cover_photo: campaign['Cover Photo'] || campaign.cover_photo
|
||
},
|
||
stats: {
|
||
totalResponses,
|
||
verifiedResponses,
|
||
totalUpvotes,
|
||
byLevel
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Error getting response stats:', error);
|
||
res.status(500).json({ error: 'Failed to get response statistics' });
|
||
}
|
||
}
|
||
|
||
// Admin and campaign owner functions
|
||
|
||
/**
|
||
* Get all responses (admin or campaign owners - includes pending/rejected)
|
||
* Campaign owners only see responses to their own campaigns
|
||
* @param {Object} req - Express request object
|
||
* @param {Object} res - Express response object
|
||
*/
|
||
async function getAdminResponses(req, res) {
|
||
try {
|
||
const { status = 'all', campaign_slug = '', offset = 0, limit = 50 } = req.query;
|
||
|
||
console.log(`User fetching responses with status filter: ${status}, campaign: ${campaign_slug}`);
|
||
|
||
const isAdmin = (req.user && req.user.isAdmin) || req.session?.isAdmin || false;
|
||
const sessionUserId = req.user?.id ?? req.session?.userId ?? null;
|
||
const sessionUserEmail = req.user?.email ?? req.session?.userEmail ?? null;
|
||
|
||
// If not admin, we need to filter by campaign ownership
|
||
let campaignsToFilter = [];
|
||
if (!isAdmin) {
|
||
// Get all campaigns owned by this user
|
||
const allCampaigns = await nocodbService.getAllCampaigns();
|
||
|
||
campaignsToFilter = allCampaigns.filter(campaign => {
|
||
const createdById = campaign['Created By User ID'] ?? campaign.created_by_user_id ?? null;
|
||
const createdByEmail = campaign['Created By User Email'] ?? campaign.created_by_user_email ?? null;
|
||
|
||
return (
|
||
(createdById != null && sessionUserId != null && String(createdById) === String(sessionUserId)) ||
|
||
(createdByEmail && sessionUserEmail && String(createdByEmail).toLowerCase() === String(sessionUserEmail).toLowerCase())
|
||
);
|
||
});
|
||
|
||
console.log(`User owns ${campaignsToFilter.length} campaigns out of ${allCampaigns.length} total`);
|
||
}
|
||
|
||
let whereConditions = [];
|
||
|
||
// Filter by status
|
||
if (status !== 'all') {
|
||
whereConditions.push(`(Status,eq,${status})`);
|
||
}
|
||
|
||
// Filter by campaign slug if provided
|
||
if (campaign_slug) {
|
||
const slugCondition = `(Campaign Slug,eq,${campaign_slug})`;
|
||
if (whereConditions.length > 0) {
|
||
whereConditions[0] += `~and${slugCondition}`;
|
||
} else {
|
||
whereConditions.push(slugCondition);
|
||
}
|
||
}
|
||
|
||
const responses = await nocodbService.getRepresentativeResponses({
|
||
where: whereConditions.length > 0 ? whereConditions[0] : undefined,
|
||
sort: '-CreatedAt',
|
||
offset: parseInt(offset),
|
||
limit: parseInt(limit)
|
||
});
|
||
|
||
console.log(`Found ${responses.length} total responses before filtering`);
|
||
|
||
// Filter responses by campaign ownership if not admin
|
||
let filteredResponses = responses;
|
||
if (!isAdmin && campaignsToFilter.length > 0) {
|
||
const ownedCampaignSlugs = campaignsToFilter.map(c => c['Campaign Slug'] || c.slug);
|
||
filteredResponses = responses.filter(r => ownedCampaignSlugs.includes(r.campaign_slug));
|
||
console.log(`After ownership filter: ${filteredResponses.length} responses`);
|
||
} else if (!isAdmin) {
|
||
// User owns no campaigns, return empty
|
||
filteredResponses = [];
|
||
}
|
||
|
||
if (filteredResponses.length > 0) {
|
||
console.log('Response statuses:', filteredResponses.map(r => `ID:${r.id} Status:${r.status}`).join(', '));
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
responses: filteredResponses,
|
||
pagination: {
|
||
offset: parseInt(offset),
|
||
limit: parseInt(limit),
|
||
hasMore: filteredResponses.length === parseInt(limit)
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Error getting admin responses:', error);
|
||
res.status(500).json({ error: 'Failed to get responses' });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Update response status (admin or campaign owner)
|
||
* @param {Object} req - Express request object
|
||
* @param {Object} res - Express response object
|
||
*/
|
||
async function updateResponseStatus(req, res) {
|
||
try {
|
||
const { id } = req.params;
|
||
const { status } = req.body;
|
||
|
||
console.log(`Updating response ${id} to status: ${status}`);
|
||
|
||
if (!['pending', 'approved', 'rejected'].includes(status)) {
|
||
return res.status(400).json({ error: 'Invalid status' });
|
||
}
|
||
|
||
// Get the response to check campaign ownership
|
||
const response = await nocodbService.getRepresentativeResponseById(id);
|
||
if (!response) {
|
||
return res.status(404).json({ error: 'Response not found' });
|
||
}
|
||
|
||
// Get campaign to check ownership
|
||
const campaignId = response.campaign_id;
|
||
const campaign = await nocodbService.getCampaignById(campaignId);
|
||
if (!campaign) {
|
||
return res.status(404).json({ error: 'Campaign not found' });
|
||
}
|
||
|
||
// Check if user is admin or campaign owner
|
||
const isAdmin = (req.user && req.user.isAdmin) || req.session?.isAdmin || false;
|
||
const sessionUserId = req.user?.id ?? req.session?.userId ?? null;
|
||
const sessionUserEmail = req.user?.email ?? req.session?.userEmail ?? null;
|
||
|
||
if (!isAdmin) {
|
||
const createdById = campaign['Created By User ID'] ?? campaign.created_by_user_id ?? null;
|
||
const createdByEmail = campaign['Created By User Email'] ?? campaign.created_by_user_email ?? null;
|
||
|
||
const ownsCampaign = (
|
||
(createdById != null && sessionUserId != null && String(createdById) === String(sessionUserId)) ||
|
||
(createdByEmail && sessionUserEmail && String(createdByEmail).toLowerCase() === String(sessionUserEmail).toLowerCase())
|
||
);
|
||
|
||
if (!ownsCampaign) {
|
||
return res.status(403).json({ error: 'You can only moderate responses to your own campaigns' });
|
||
}
|
||
}
|
||
|
||
const updated = await nocodbService.updateRepresentativeResponse(id, { status });
|
||
console.log('Updated response:', JSON.stringify(updated, null, 2));
|
||
|
||
res.json({
|
||
success: true,
|
||
message: `Response ${status} successfully`
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Error updating response status:', error);
|
||
res.status(500).json({ error: 'Failed to update response status' });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Update response (admin only)
|
||
* @param {Object} req - Express request object
|
||
* @param {Object} res - Express response object
|
||
*/
|
||
async function updateResponse(req, res) {
|
||
try {
|
||
const { id } = req.params;
|
||
const updates = req.body;
|
||
|
||
await nocodbService.updateRepresentativeResponse(id, updates);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Response updated successfully'
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Error updating response:', error);
|
||
res.status(500).json({ error: 'Failed to update response' });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Delete response (admin only)
|
||
* @param {Object} req - Express request object
|
||
* @param {Object} res - Express response object
|
||
*/
|
||
async function deleteResponse(req, res) {
|
||
try {
|
||
const { id } = req.params;
|
||
|
||
// Delete all upvotes for this response first
|
||
const upvotes = await nocodbService.getResponseUpvotes({
|
||
where: `(Response ID,eq,${id})`
|
||
});
|
||
|
||
for (const upvote of upvotes) {
|
||
await nocodbService.deleteResponseUpvote(upvote.id);
|
||
}
|
||
|
||
// Delete the response
|
||
await nocodbService.deleteRepresentativeResponse(id);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Response deleted successfully'
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Error deleting response:', error);
|
||
res.status(500).json({ error: 'Failed to delete response' });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Verify a response using verification token
|
||
* Public endpoint - no authentication required
|
||
* @param {Object} req - Express request object
|
||
* @param {Object} res - Express response object
|
||
*/
|
||
async function verifyResponse(req, res) {
|
||
try {
|
||
const { id, token } = req.params;
|
||
|
||
console.log('=== VERIFICATION ATTEMPT ===');
|
||
console.log('Response ID:', id);
|
||
console.log('Token from URL:', token);
|
||
|
||
// Get the response
|
||
const response = await nocodbService.getRepresentativeResponseById(id);
|
||
if (!response) {
|
||
console.log('Response not found for ID:', id);
|
||
return res.status(404).send(`
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>Response Not Found</title>
|
||
<style>
|
||
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
|
||
h1 { color: #e74c3c; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>❌ Response Not Found</h1>
|
||
<p>The response you're trying to verify could not be found.</p>
|
||
<p>It may have been deleted or the link may be incorrect.</p>
|
||
</body>
|
||
</html>
|
||
`);
|
||
}
|
||
|
||
console.log('Response found:', {
|
||
id: response.id,
|
||
verification_token: response.verification_token,
|
||
verification_token_type: typeof response.verification_token,
|
||
token_from_url: token,
|
||
token_from_url_type: typeof token,
|
||
tokens_match: response.verification_token === token
|
||
});
|
||
|
||
// Check if token matches
|
||
if (response.verification_token !== token) {
|
||
console.log('Token mismatch! Expected:', response.verification_token, 'Got:', token);
|
||
return res.status(403).send(`
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>Invalid Verification Token</title>
|
||
<style>
|
||
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
|
||
h1 { color: #e74c3c; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>❌ Invalid Verification Token</h1>
|
||
<p>The verification link is invalid or has expired.</p>
|
||
</body>
|
||
</html>
|
||
`);
|
||
}
|
||
|
||
// Check if already verified
|
||
if (response.verified_at) {
|
||
return res.send(`
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>Already Verified</title>
|
||
<style>
|
||
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
|
||
h1 { color: #3498db; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>ℹ️ Already Verified</h1>
|
||
<p>This response has already been verified on ${new Date(response.verified_at).toLocaleDateString()}.</p>
|
||
</body>
|
||
</html>
|
||
`);
|
||
}
|
||
|
||
// Update response to verified
|
||
const updatedData = {
|
||
is_verified: true,
|
||
verified_at: new Date().toISOString(),
|
||
verified_by: response.representative_email || 'Representative',
|
||
status: 'approved' // Auto-approve when verified by representative
|
||
};
|
||
|
||
await nocodbService.updateRepresentativeResponse(id, updatedData);
|
||
|
||
res.send(`
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>Response Verified</title>
|
||
<style>
|
||
body {
|
||
font-family: Arial, sans-serif;
|
||
text-align: center;
|
||
padding: 50px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
}
|
||
.container {
|
||
background: white;
|
||
color: #333;
|
||
padding: 40px;
|
||
border-radius: 10px;
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
}
|
||
h1 { color: #27ae60; margin-top: 0; }
|
||
.checkmark { font-size: 60px; }
|
||
a { color: #3498db; text-decoration: none; }
|
||
a:hover { text-decoration: underline; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="checkmark">✅</div>
|
||
<h1>Response Verified!</h1>
|
||
<p>Thank you for verifying this response.</p>
|
||
<p>The response has been marked as verified and will now appear with a verification badge on the Response Wall.</p>
|
||
<p style="margin-top: 30px; font-size: 14px; color: #7f8c8d;">
|
||
You can close this window now.
|
||
</p>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`);
|
||
|
||
} catch (error) {
|
||
console.error('Error verifying response:', error);
|
||
res.status(500).send(`
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>Verification Error</title>
|
||
<style>
|
||
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
|
||
h1 { color: #e74c3c; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>❌ Verification Error</h1>
|
||
<p>An error occurred while verifying the response.</p>
|
||
<p>Please try again later or contact support.</p>
|
||
</body>
|
||
</html>
|
||
`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Report a response as invalid using verification token
|
||
* Public endpoint - no authentication required
|
||
* @param {Object} req - Express request object
|
||
* @param {Object} res - Express response object
|
||
*/
|
||
async function reportResponse(req, res) {
|
||
try {
|
||
const { id, token } = req.params;
|
||
|
||
// Get the response
|
||
const response = await nocodbService.getRepresentativeResponseById(id);
|
||
if (!response) {
|
||
return res.status(404).send(`
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>Response Not Found</title>
|
||
<style>
|
||
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
|
||
h1 { color: #e74c3c; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>❌ Response Not Found</h1>
|
||
<p>The response you're trying to report could not be found.</p>
|
||
</body>
|
||
</html>
|
||
`);
|
||
}
|
||
|
||
// Check if token matches
|
||
if (response.verification_token !== token) {
|
||
return res.status(403).send(`
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>Invalid Token</title>
|
||
<style>
|
||
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
|
||
h1 { color: #e74c3c; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>❌ Invalid Token</h1>
|
||
<p>The report link is invalid or has expired.</p>
|
||
</body>
|
||
</html>
|
||
`);
|
||
}
|
||
|
||
// Update response status to rejected (disputed by representative)
|
||
const updatedData = {
|
||
status: 'rejected',
|
||
is_verified: false,
|
||
verified_at: null,
|
||
verified_by: `Disputed by ${response.representative_email || 'Representative'}`
|
||
};
|
||
|
||
await nocodbService.updateRepresentativeResponse(id, updatedData);
|
||
|
||
res.send(`
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>Response Reported</title>
|
||
<style>
|
||
body {
|
||
font-family: Arial, sans-serif;
|
||
text-align: center;
|
||
padding: 50px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
}
|
||
.container {
|
||
background: white;
|
||
color: #333;
|
||
padding: 40px;
|
||
border-radius: 10px;
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
}
|
||
h1 { color: #e74c3c; margin-top: 0; }
|
||
.icon { font-size: 60px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="icon">⚠️</div>
|
||
<h1>Response Reported</h1>
|
||
<p>Thank you for reporting this response.</p>
|
||
<p>The response has been marked as disputed and will be hidden from public view while we investigate.</p>
|
||
<p style="margin-top: 30px; font-size: 14px; color: #7f8c8d;">
|
||
You can close this window now.
|
||
</p>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`);
|
||
|
||
} catch (error) {
|
||
console.error('Error reporting response:', error);
|
||
res.status(500).send(`
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>Report Error</title>
|
||
<style>
|
||
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
|
||
h1 { color: #e74c3c; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>❌ Report Error</h1>
|
||
<p>An error occurred while reporting the response.</p>
|
||
<p>Please try again later or contact support.</p>
|
||
</body>
|
||
</html>
|
||
`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Resend verification email for a response
|
||
* Public endpoint - no authentication required
|
||
* @param {Object} req - Express request object
|
||
* @param {Object} res - Express response object
|
||
*/
|
||
async function resendVerification(req, res) {
|
||
try {
|
||
const { id } = req.params;
|
||
|
||
console.log('=== RESEND VERIFICATION REQUEST ===');
|
||
console.log('Response ID:', id);
|
||
|
||
// Get the response
|
||
const response = await nocodbService.getRepresentativeResponseById(id);
|
||
if (!response) {
|
||
console.log('Response not found for ID:', id);
|
||
return res.status(404).json({
|
||
success: false,
|
||
error: 'Response not found'
|
||
});
|
||
}
|
||
|
||
// Check if already verified
|
||
if (response.verified_at) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
error: 'This response has already been verified'
|
||
});
|
||
}
|
||
|
||
// Check if we have the necessary data
|
||
if (!response.representative_email) {
|
||
return res.status(400).json({
|
||
success: false,
|
||
error: 'No representative email on file for this response'
|
||
});
|
||
}
|
||
|
||
if (!response.verification_token) {
|
||
// Generate a new token if one doesn't exist
|
||
const crypto = require('crypto');
|
||
const newToken = crypto.randomBytes(32).toString('hex');
|
||
const verificationSentAt = new Date().toISOString();
|
||
|
||
await nocodbService.updateRepresentativeResponse(id, {
|
||
verification_token: newToken,
|
||
verification_sent_at: verificationSentAt
|
||
});
|
||
|
||
response.verification_token = newToken;
|
||
response.verification_sent_at = verificationSentAt;
|
||
}
|
||
|
||
// Get campaign details
|
||
const campaign = await nocodbService.getCampaignBySlug(response.campaign_slug);
|
||
if (!campaign) {
|
||
return res.status(404).json({
|
||
success: false,
|
||
error: 'Campaign not found'
|
||
});
|
||
}
|
||
|
||
// Send verification email
|
||
try {
|
||
const baseUrl = process.env.BASE_URL || `${req.protocol}://${req.get('host')}`;
|
||
const verificationUrl = `${baseUrl}/api/responses/${response.id}/verify/${response.verification_token}`;
|
||
const reportUrl = `${baseUrl}/api/responses/${response.id}/report/${response.verification_token}`;
|
||
|
||
const campaignTitle = campaign.Title || campaign.title || 'Unknown Campaign';
|
||
const submittedDate = new Date(response.created_at).toLocaleDateString('en-US', {
|
||
year: 'numeric',
|
||
month: 'long',
|
||
day: 'numeric'
|
||
});
|
||
|
||
await emailService.sendResponseVerification({
|
||
representativeEmail: response.representative_email,
|
||
representativeName: response.representative_name,
|
||
campaignTitle,
|
||
responseType: response.response_type,
|
||
responseText: response.response_text,
|
||
submittedDate,
|
||
submitterName: response.is_anonymous ? 'Anonymous' : (response.submitted_by_name || 'A constituent'),
|
||
verificationUrl,
|
||
reportUrl
|
||
});
|
||
|
||
// Update verification_sent_at timestamp
|
||
await nocodbService.updateRepresentativeResponse(id, {
|
||
verification_sent_at: new Date().toISOString()
|
||
});
|
||
|
||
console.log('Verification email resent successfully to:', response.representative_email);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Verification email sent successfully to the representative'
|
||
});
|
||
|
||
} catch (emailError) {
|
||
console.error('Failed to send verification email:', emailError);
|
||
res.status(500).json({
|
||
success: false,
|
||
error: 'Failed to send verification email. Please try again later.'
|
||
});
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error resending verification:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
error: 'An error occurred while processing your request'
|
||
});
|
||
}
|
||
}
|
||
|
||
module.exports = {
|
||
getCampaignResponses,
|
||
submitResponse,
|
||
upvoteResponse,
|
||
removeUpvote,
|
||
getResponseStats,
|
||
getAdminResponses,
|
||
updateResponseStatus,
|
||
updateResponse,
|
||
deleteResponse,
|
||
verifyResponse,
|
||
reportResponse,
|
||
resendVerification
|
||
};
|