Verfied response system for electeds

This commit is contained in:
2025-10-16 12:12:54 -06:00
parent 84e2283976
commit ba3b685a8d
17 changed files with 2163 additions and 89 deletions

View File

@@ -1,4 +1,6 @@
const nocodbService = require('../services/nocodb');
const emailService = require('../services/email');
const crypto = require('crypto');
const { validateResponse } = require('../utils/validators');
/**
@@ -118,6 +120,48 @@ async function submitResponse(req, res) {
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'],
@@ -132,9 +176,14 @@ async function submitResponse(req, res) {
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: responseData.is_anonymous || false,
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
};
@@ -144,10 +193,50 @@ async function submitResponse(req, res) {
// 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: 'Response submitted successfully. It will be visible after moderation.',
response: createdResponse
message: responseMessage,
response: createdResponse,
verificationEmailSent
});
} catch (error) {
@@ -534,6 +623,290 @@ async function deleteResponse(req, res) {
}
}
/**
* 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>
`);
}
}
module.exports = {
getCampaignResponses,
submitResponse,
@@ -543,5 +916,7 @@ module.exports = {
getAdminResponses,
updateResponseStatus,
updateResponse,
deleteResponse
deleteResponse,
verifyResponse,
reportResponse
};

View File

@@ -303,6 +303,47 @@
color: #7f8c8d;
}
/* Postal Lookup Styles */
.postal-lookup-container {
display: flex;
gap: 0.5rem;
}
.postal-lookup-container input {
flex: 1;
}
.postal-lookup-container .btn {
white-space: nowrap;
padding: 0.75rem 1rem;
}
#rep-select {
width: 100%;
padding: 0.5rem;
border: 2px solid #3498db;
border-radius: 4px;
font-size: 0.95rem;
background: white;
cursor: pointer;
}
#rep-select option {
padding: 0.5rem;
cursor: pointer;
}
#rep-select option:hover {
background: #f0f8ff;
}
#rep-select-group {
background: #f8f9fa;
padding: 1rem;
border-radius: 4px;
border: 1px solid #e1e8ed;
}
.form-actions {
display: flex;
gap: 1rem;
@@ -313,6 +354,25 @@
flex: 1;
}
/* Checkbox styling */
.form-group input[type="checkbox"] {
width: auto;
margin-right: 0.5rem;
cursor: pointer;
}
.form-group label:has(input[type="checkbox"]) {
display: flex;
align-items: center;
font-weight: normal;
cursor: pointer;
}
.form-group input[type="checkbox"]:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.loading {
text-align: center;
padding: 2rem;

View File

@@ -5,6 +5,7 @@ let currentOffset = 0;
let currentSort = 'recent';
let currentLevel = '';
const LIMIT = 20;
let loadedRepresentatives = [];
// Initialize
document.addEventListener('DOMContentLoaded', () => {
@@ -73,9 +74,185 @@ document.addEventListener('DOMContentLoaded', () => {
form.addEventListener('submit', handleSubmitResponse);
}
// Postal code lookup button
const lookupBtn = document.getElementById('lookup-rep-btn');
if (lookupBtn) {
lookupBtn.addEventListener('click', handlePostalLookup);
}
// Postal code input formatting
const postalInput = document.getElementById('modal-postal-code');
if (postalInput) {
postalInput.addEventListener('input', formatPostalCodeInput);
postalInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
handlePostalLookup();
}
});
}
// Representative selection
const repSelect = document.getElementById('rep-select');
if (repSelect) {
repSelect.addEventListener('change', handleRepresentativeSelect);
}
console.log('Response Wall: Initialization complete');
});
// Postal Code Lookup Functions
function formatPostalCodeInput(e) {
let value = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, '');
// Format as A1A 1A1
if (value.length > 3) {
value = value.slice(0, 3) + ' ' + value.slice(3, 6);
}
e.target.value = value;
}
function validatePostalCode(postalCode) {
const cleaned = postalCode.replace(/\s/g, '');
// Check format: Letter-Number-Letter Number-Letter-Number
const regex = /^[A-Z]\d[A-Z]\d[A-Z]\d$/;
if (!regex.test(cleaned)) {
return { valid: false, message: 'Please enter a valid postal code format (A1A 1A1)' };
}
// Check if it's an Alberta postal code (starts with T)
if (!cleaned.startsWith('T')) {
return { valid: false, message: 'This tool is designed for Alberta postal codes only (starting with T)' };
}
return { valid: true };
}
async function handlePostalLookup() {
const postalInput = document.getElementById('modal-postal-code');
const postalCode = postalInput.value.trim();
if (!postalCode) {
showError('Please enter a postal code');
return;
}
const validation = validatePostalCode(postalCode);
if (!validation.valid) {
showError(validation.message);
return;
}
const lookupBtn = document.getElementById('lookup-rep-btn');
lookupBtn.disabled = true;
lookupBtn.textContent = '🔄 Searching...';
try {
const response = await window.apiClient.getRepresentativesByPostalCode(postalCode);
const data = response.data || response;
loadedRepresentatives = data.representatives || [];
if (loadedRepresentatives.length === 0) {
showError('No representatives found for this postal code');
document.getElementById('rep-select-group').style.display = 'none';
} else {
displayRepresentativeOptions(loadedRepresentatives);
showSuccess(`Found ${loadedRepresentatives.length} representatives`);
}
} catch (error) {
console.error('Postal lookup failed:', error);
showError('Failed to lookup representatives: ' + error.message);
} finally {
lookupBtn.disabled = false;
lookupBtn.textContent = '🔍 Search';
}
}
function displayRepresentativeOptions(representatives) {
const repSelect = document.getElementById('rep-select');
const repSelectGroup = document.getElementById('rep-select-group');
// Clear existing options
repSelect.innerHTML = '';
// Add representatives as options
representatives.forEach((rep, index) => {
const option = document.createElement('option');
option.value = index;
// Format display text
let displayText = rep.name;
if (rep.district_name) {
displayText += ` - ${rep.district_name}`;
}
if (rep.party_name) {
displayText += ` (${rep.party_name})`;
}
displayText += ` [${rep.elected_office || 'Representative'}]`;
option.textContent = displayText;
repSelect.appendChild(option);
});
// Show the select group
repSelectGroup.style.display = 'block';
}
function handleRepresentativeSelect(e) {
const selectedIndex = e.target.value;
if (selectedIndex === '') return;
const rep = loadedRepresentatives[selectedIndex];
if (!rep) return;
// Auto-fill form fields
document.getElementById('representative-name').value = rep.name || '';
document.getElementById('representative-title').value = rep.elected_office || '';
// Set government level based on elected office
const level = determineGovernmentLevel(rep.elected_office);
document.getElementById('representative-level').value = level;
// Store email for verification option
if (rep.email) {
// Handle email being either string or array
const emailValue = Array.isArray(rep.email) ? rep.email[0] : rep.email;
document.getElementById('representative-email').value = emailValue;
// Enable verification checkbox if we have an email
const verificationCheckbox = document.getElementById('send-verification');
verificationCheckbox.disabled = false;
} else {
document.getElementById('representative-email').value = '';
// Disable verification checkbox if no email
const verificationCheckbox = document.getElementById('send-verification');
verificationCheckbox.disabled = true;
verificationCheckbox.checked = false;
}
showSuccess('Representative details filled. Please complete the rest of the form.');
}
function determineGovernmentLevel(electedOffice) {
if (!electedOffice) return '';
const office = electedOffice.toLowerCase();
if (office.includes('mp') || office.includes('member of parliament')) {
return 'Federal';
} else if (office.includes('mla') || office.includes('member of the legislative assembly')) {
return 'Provincial';
} else if (office.includes('councillor') || office.includes('councilor') || office.includes('mayor')) {
return 'Municipal';
} else if (office.includes('trustee') || office.includes('school board')) {
return 'School Board';
}
return '';
}
// Load response statistics
async function loadResponseStats() {
try {
@@ -294,6 +471,19 @@ function openSubmitModal() {
function closeSubmitModal() {
document.getElementById('submit-modal').style.display = 'none';
document.getElementById('submit-response-form').reset();
// Reset postal code lookup
document.getElementById('rep-select-group').style.display = 'none';
document.getElementById('rep-select').innerHTML = '';
loadedRepresentatives = [];
// Reset hidden fields
document.getElementById('representative-email').value = '';
// Reset verification checkbox
const verificationCheckbox = document.getElementById('send-verification');
verificationCheckbox.disabled = false;
verificationCheckbox.checked = false;
}
// Handle response submission
@@ -301,6 +491,15 @@ async function handleSubmitResponse(e) {
e.preventDefault();
const formData = new FormData(e.target);
// Note: Both send_verification checkbox and representative_email hidden field
// are already included in FormData from the form
// send_verification will be 'on' if checked, undefined if not checked
// representative_email will be populated by handleRepresentativeSelect()
// Get verification status for UI feedback
const sendVerification = document.getElementById('send-verification').checked;
const repEmail = document.getElementById('representative-email').value;
try {
const response = await fetch(`/api/campaigns/${currentCampaignSlug}/responses`, {
@@ -311,7 +510,11 @@ async function handleSubmitResponse(e) {
const data = await response.json();
if (data.success) {
showSuccess(data.message || 'Response submitted successfully! It will appear after moderation.');
let message = data.message || 'Response submitted successfully! It will appear after moderation.';
if (sendVerification && repEmail) {
message += ' A verification email has been sent to the representative.';
}
showSuccess(message);
closeSubmitModal();
// Don't reload responses since submission is pending approval
} else {

View File

@@ -83,9 +83,30 @@
<span class="close" id="modal-close-btn">&times;</span>
<h2>Share a Representative Response</h2>
<form id="submit-response-form" enctype="multipart/form-data">
<!-- Postal Code Lookup -->
<div class="form-group">
<label for="modal-postal-code">Find Your Representative by Postal Code</label>
<div class="postal-lookup-container">
<input type="text" id="modal-postal-code" placeholder="Enter postal code (e.g., T5K 2J1)" maxlength="7">
<button type="button" class="btn btn-secondary" id="lookup-rep-btn">🔍 Search</button>
</div>
<small>Search for representatives by postal code to auto-fill details</small>
</div>
<!-- Representatives Selection (Hidden by default) -->
<div class="form-group" id="rep-select-group" style="display: none;">
<label for="rep-select">Select Representative *</label>
<select id="rep-select" size="5">
<!-- Options will be populated by JavaScript -->
</select>
<small>Click on a representative to auto-fill the form</small>
</div>
<!-- Manual Entry Fields -->
<div class="form-group">
<label for="representative-name">Representative Name *</label>
<input type="text" id="representative-name" name="representative_name" required>
<small>Or enter manually if not found above</small>
</div>
<div class="form-group">
@@ -104,6 +125,9 @@
</select>
</div>
<!-- Hidden field to store representative email for verification -->
<input type="hidden" id="representative-email" name="representative_email">
<div class="form-group">
<label for="response-type">Response Type *</label>
<select id="response-type" name="response_type" required>
@@ -150,6 +174,14 @@
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="send-verification" name="send_verification">
Send verification request to representative
</label>
<small>This will email the representative to verify this response is authentic</small>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" id="cancel-submit-btn">Cancel</button>
<button type="submit" class="btn btn-primary">Submit Response</button>
@@ -158,6 +190,7 @@
</div>
</div>
<script src="/js/api-client.js"></script>
<script src="/js/response-wall.js"></script>
</body>
</html>

View File

@@ -245,6 +245,10 @@ router.post(
router.post('/responses/:id/upvote', optionalAuth, rateLimiter.general, responsesController.upvoteResponse);
router.delete('/responses/:id/upvote', optionalAuth, rateLimiter.general, responsesController.removeUpvote);
// Response Verification Routes (public - no auth required)
router.get('/responses/:id/verify/:token', responsesController.verifyResponse);
router.get('/responses/:id/report/:token', responsesController.reportResponse);
// Admin and Campaign Owner Response Management Routes
router.get('/admin/responses', requireNonTemp, rateLimiter.general, responsesController.getAdminResponses);
router.patch('/admin/responses/:id/status', requireNonTemp, rateLimiter.general,

View File

@@ -409,6 +409,62 @@ class EmailService {
throw error;
}
}
/**
* Send response verification email to representative
* @param {Object} options - Email options
* @param {string} options.representativeEmail - Representative's email address
* @param {string} options.representativeName - Representative's name
* @param {string} options.campaignTitle - Campaign title
* @param {string} options.responseType - Type of response (Email, Letter, etc.)
* @param {string} options.responseText - The actual response text
* @param {string} options.submittedDate - Date the response was submitted
* @param {string} options.submitterName - Name of person who submitted
* @param {string} options.verificationUrl - URL to verify the response
* @param {string} options.reportUrl - URL to report as invalid
*/
async sendResponseVerification(options) {
try {
const {
representativeEmail,
representativeName,
campaignTitle,
responseType,
responseText,
submittedDate,
submitterName,
verificationUrl,
reportUrl
} = options;
const templateVariables = {
REPRESENTATIVE_NAME: representativeName,
CAMPAIGN_TITLE: campaignTitle,
RESPONSE_TYPE: responseType,
RESPONSE_TEXT: responseText,
SUBMITTED_DATE: submittedDate,
SUBMITTER_NAME: submitterName || 'Anonymous',
VERIFICATION_URL: verificationUrl,
REPORT_URL: reportUrl,
APP_NAME: process.env.APP_NAME || 'BNKops Influence',
TIMESTAMP: new Date().toLocaleString()
};
const emailOptions = {
to: representativeEmail,
from: {
email: process.env.SMTP_FROM_EMAIL,
name: process.env.SMTP_FROM_NAME
},
subject: `Response Verification Request - ${campaignTitle}`
};
return await this.sendTemplatedEmail('response-verification', templateVariables, emailOptions);
} catch (error) {
console.error('Failed to send response verification email:', error);
throw error;
}
}
}
module.exports = new EmailService();

View File

@@ -758,6 +758,11 @@ class NocoDBService {
'Is Anonymous': responseData.is_anonymous,
'Status': responseData.status,
'Is Verified': responseData.is_verified,
'Representative Email': responseData.representative_email,
'Verification Token': responseData.verification_token,
'Verification Sent At': responseData.verification_sent_at,
'Verified At': responseData.verified_at,
'Verified By': responseData.verified_by,
'Upvote Count': responseData.upvote_count,
'Submitted IP': responseData.submitted_ip
};
@@ -780,6 +785,11 @@ class NocoDBService {
if (updates.upvote_count !== undefined) data['Upvote Count'] = updates.upvote_count;
if (updates.response_text !== undefined) data['Response Text'] = updates.response_text;
if (updates.user_comment !== undefined) data['User Comment'] = updates.user_comment;
if (updates.representative_email !== undefined) data['Representative Email'] = updates.representative_email;
if (updates.verification_token !== undefined) data['Verification Token'] = updates.verification_token;
if (updates.verification_sent_at !== undefined) data['Verification Sent At'] = updates.verification_sent_at;
if (updates.verified_at !== undefined) data['Verified At'] = updates.verified_at;
if (updates.verified_by !== undefined) data['Verified By'] = updates.verified_by;
console.log(`Updating response ${responseId} with data:`, JSON.stringify(data, null, 2));
@@ -858,6 +868,11 @@ class NocoDBService {
is_anonymous: data['Is Anonymous'] || data.is_anonymous || false,
status: data['Status'] || data.status,
is_verified: data['Is Verified'] || data.is_verified || false,
representative_email: data['Representative Email'] || data.representative_email,
verification_token: data['Verification Token'] || data.verification_token,
verification_sent_at: data['Verification Sent At'] || data.verification_sent_at,
verified_at: data['Verified At'] || data.verified_at,
verified_by: data['Verified By'] || data.verified_by,
upvote_count: data['Upvote Count'] || data.upvote_count || 0,
submitted_ip: data['Submitted IP'] || data.submitted_ip,
created_at: data.CreatedAt || data.created_at,

View File

@@ -0,0 +1,155 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Verify Response Submission</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background-color: #ffffff;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header {
text-align: center;
padding-bottom: 20px;
border-bottom: 2px solid #3498db;
margin-bottom: 30px;
}
.header h1 {
color: #2c3e50;
margin: 0;
font-size: 24px;
}
.content {
margin-bottom: 30px;
}
.info-box {
background-color: #f8f9fa;
border-left: 4px solid #3498db;
padding: 15px;
margin: 20px 0;
}
.info-box strong {
display: block;
color: #2c3e50;
margin-bottom: 5px;
}
.button-container {
text-align: center;
margin: 30px 0;
}
.button {
display: inline-block;
padding: 12px 30px;
margin: 10px;
text-decoration: none;
border-radius: 5px;
font-weight: bold;
font-size: 16px;
}
.verify-button {
background-color: #27ae60;
color: #ffffff;
}
.verify-button:hover {
background-color: #229954;
}
.report-button {
background-color: #e74c3c;
color: #ffffff;
}
.report-button:hover {
background-color: #c0392b;
}
.response-preview {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 15px;
margin: 20px 0;
white-space: pre-wrap;
font-size: 14px;
}
.footer {
text-align: center;
padding-top: 20px;
border-top: 1px solid #dee2e6;
margin-top: 30px;
font-size: 12px;
color: #7f8c8d;
}
.warning {
background-color: #fff3cd;
border: 1px solid #ffc107;
border-radius: 4px;
padding: 15px;
margin: 20px 0;
color: #856404;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📧 Response Verification Request</h1>
</div>
<div class="content">
<p>Dear {{REPRESENTATIVE_NAME}},</p>
<p>A constituent has submitted a response they claim to have received from you through the <strong>{{APP_NAME}}</strong> platform.</p>
<div class="info-box">
<strong>Campaign:</strong> {{CAMPAIGN_TITLE}}
<br>
<strong>Response Type:</strong> {{RESPONSE_TYPE}}
<br>
<strong>Submitted:</strong> {{SUBMITTED_DATE}}
<br>
<strong>Submitted By:</strong> {{SUBMITTER_NAME}}
</div>
<div class="response-preview">
<strong>Response Content:</strong><br>
{{RESPONSE_TEXT}}
</div>
<div class="warning">
<strong>⚠️ Action Required</strong><br>
Please verify whether this response is authentic by clicking one of the buttons below.
</div>
<div class="button-container">
<a href="{{VERIFICATION_URL}}" class="button verify-button">
✓ Verify This Response
</a>
<a href="{{REPORT_URL}}" class="button report-button">
✗ Report as Invalid
</a>
</div>
<p><strong>Why verify?</strong> Verification helps maintain transparency and accountability in constituent communications. Verified responses appear with a special badge on the Response Wall.</p>
<p><strong>What happens if I report?</strong> Reported responses will be marked as disputed and may be hidden from public view while we investigate.</p>
</div>
<div class="footer">
<p>This email was sent by {{APP_NAME}}<br>
You received this because a constituent submitted a response attributed to you.<br>
Verification links expire in 30 days.</p>
<p><strong>Timestamp:</strong> {{TIMESTAMP}}</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,42 @@
RESPONSE VERIFICATION REQUEST
==============================
Dear {{REPRESENTATIVE_NAME}},
A constituent has submitted a response they claim to have received from you through the {{APP_NAME}} platform.
SUBMISSION DETAILS:
-------------------
Campaign: {{CAMPAIGN_TITLE}}
Response Type: {{RESPONSE_TYPE}}
Submitted: {{SUBMITTED_DATE}}
Submitted By: {{SUBMITTER_NAME}}
RESPONSE CONTENT:
-----------------
{{RESPONSE_TEXT}}
ACTION REQUIRED:
---------------
Please verify whether this response is authentic by clicking one of the links below.
VERIFY THIS RESPONSE:
{{VERIFICATION_URL}}
REPORT AS INVALID:
{{REPORT_URL}}
WHY VERIFY?
-----------
Verification helps maintain transparency and accountability in constituent communications. Verified responses appear with a special badge on the Response Wall.
WHAT HAPPENS IF I REPORT?
--------------------------
Reported responses will be marked as disputed and may be hidden from public view while we investigate.
---
This email was sent by {{APP_NAME}}
You received this because a constituent submitted a response attributed to you.
Verification links expire in 30 days.
Timestamp: {{TIMESTAMP}}