new system for creating campaigns from the main site dashboard

This commit is contained in:
admin 2025-10-15 10:32:18 -06:00
parent b71a6e4ff3
commit 06ecffaf4d
12 changed files with 956 additions and 3 deletions

View File

@ -1,5 +1,6 @@
const emailService = require('../services/email'); const emailService = require('../services/email');
const nocoDB = require('../services/nocodb'); const nocoDB = require('../services/nocodb');
const crypto = require('crypto');
class EmailsController { class EmailsController {
async sendEmail(req, res, next) { async sendEmail(req, res, next) {
@ -211,6 +212,206 @@ class EmailsController {
}); });
} }
} }
async initiateEmailToCampaign(req, res, next) {
try {
const { email, subject, message, postalCode, senderName } = req.body;
// Check if email verification is enabled
const verificationEnabled = process.env.EMAIL_VERIFICATION_ENABLED !== 'false';
if (!verificationEnabled) {
return res.status(400).json({
success: false,
error: 'Email verification is not enabled'
});
}
// Generate verification token
const token = crypto.randomBytes(32).toString('hex');
const expiryHours = parseInt(process.env.EMAIL_VERIFICATION_EXPIRY) || 24;
const expiresAt = new Date(Date.now() + expiryHours * 60 * 60 * 1000);
// Store token and campaign data
await nocoDB.createEmailVerification({
token,
email,
temp_campaign_data: JSON.stringify({
subject,
message,
postalCode,
senderName
}),
created_at: new Date().toISOString(),
expires_at: expiresAt.toISOString(),
used: false
});
// Send verification email
const appUrl = process.env.APP_URL || 'http://localhost:3333';
const verificationUrl = `${appUrl}/verify-email.html?token=${token}`;
await emailService.sendEmailVerification(email, verificationUrl, senderName || 'there');
res.json({
success: true,
message: 'Verification email sent. Please check your inbox.'
});
} catch (error) {
console.error('Email to campaign conversion error:', error);
res.status(500).json({
success: false,
error: 'Failed to initiate campaign conversion',
message: error.message
});
}
}
async verifyEmailToken(req, res, next) {
try {
const { token } = req.params;
// Find verification record
const verification = await nocoDB.getEmailVerificationByToken(token);
if (!verification) {
return res.status(404).json({
success: false,
error: 'Invalid or expired verification link'
});
}
// Check if already used
if (verification.used) {
return res.status(400).json({
success: false,
error: 'This verification link has already been used'
});
}
// Check if expired
const now = new Date();
const expiresAt = new Date(verification.expires_at || verification.expiresAt || verification['Expires At']);
if (now > expiresAt) {
return res.status(400).json({
success: false,
error: 'This verification link has expired'
});
}
// Mark as used
const verificationId = verification.id || verification.Id || verification.ID;
await nocoDB.updateEmailVerification(verificationId, { used: true });
// Parse campaign data
const campaignDataStr = verification.temp_campaign_data || verification.tempCampaignData || verification['Temp Campaign Data'];
const campaignData = JSON.parse(campaignDataStr);
// Check if user exists
const userEmail = verification.email || verification.Email;
const existingUser = await nocoDB.getUserByEmail(userEmail);
if (existingUser) {
// User exists, log them in automatically
req.session.authenticated = true;
req.session.userId = existingUser.ID || existingUser.Id || existingUser.id;
req.session.userEmail = existingUser.Email || existingUser.email;
req.session.userName = existingUser.Name || existingUser.name;
req.session.isAdmin = existingUser.Admin || existingUser.admin || false;
req.session.userType = existingUser['User Type'] || existingUser.UserType || existingUser.userType || 'user';
req.session.save((err) => {
if (err) {
console.error('Session save error:', err);
return res.status(500).json({
success: false,
error: 'Session error'
});
}
res.json({
success: true,
needsAccount: false,
campaignData: campaignData,
redirectTo: '/dashboard.html'
});
});
} else {
// User doesn't exist - create a new user account automatically
try {
// Generate a temporary password (user can change it later)
const tempPassword = crypto.randomBytes(16).toString('hex');
// Extract name from campaign data or use email prefix
const userName = campaignData.senderName || userEmail.split('@')[0];
// Create new user
const newUser = await nocoDB.createUser({
'Name': userName,
'Email': userEmail,
'Password': tempPassword,
'Admin': false,
'User Type': 'user'
});
const userId = newUser.ID || newUser.Id || newUser.id || newUser;
// Send login credentials email to the new user
try {
await emailService.sendLoginDetails({
Name: userName,
Email: userEmail,
Password: tempPassword,
admin: false
});
console.log('Welcome email with credentials sent to:', userEmail);
} catch (emailError) {
console.error('Failed to send welcome email:', emailError);
// Don't fail the whole process if email sending fails
}
// Log the new user in automatically
req.session.authenticated = true;
req.session.userId = userId;
req.session.userEmail = userEmail;
req.session.userName = userName;
req.session.isAdmin = false;
req.session.userType = 'user';
req.session.save((err) => {
if (err) {
console.error('Session save error:', err);
return res.status(500).json({
success: false,
error: 'Session error'
});
}
res.json({
success: true,
needsAccount: false,
isNewUser: true,
campaignData: campaignData,
redirectTo: '/dashboard.html',
message: 'Account created successfully! Check your email for login credentials.'
});
});
} catch (createError) {
console.error('Error creating user account:', createError);
return res.status(500).json({
success: false,
error: 'Failed to create user account',
message: createError.message
});
}
}
} catch (error) {
console.error('Email verification error:', error);
res.status(500).json({
success: false,
error: 'Failed to verify email',
message: error.message
});
}
}
} }
module.exports = new EmailsController(); module.exports = new EmailsController();

View File

@ -28,6 +28,9 @@ class UserDashboard {
this.setupEventListeners(); this.setupEventListeners();
this.loadUserCampaigns(); this.loadUserCampaigns();
this.loadAnalytics(); this.loadAnalytics();
// Check for pending campaign from email verification
this.checkPendingCampaign();
} }
setupUserInterface() { setupUserInterface() {
@ -405,6 +408,94 @@ class UserDashboard {
} }
} }
checkPendingCampaign() {
// Check for pending campaign data from email verification
const pendingCampaignData = sessionStorage.getItem('pendingCampaign');
if (pendingCampaignData) {
try {
const campaignData = JSON.parse(pendingCampaignData);
// Clear the session storage
sessionStorage.removeItem('pendingCampaign');
// Pre-populate the create campaign form
this.populateCampaignFormFromEmail(campaignData);
// Switch to create tab
this.switchTab('create');
// Show helpful message
this.showMessage(
'🎉 Your email content has been loaded! Please add a campaign title and description, then publish your campaign.',
'success',
10000
);
// Show additional info for new users
if (this.user && !this.user.lastLogin) {
setTimeout(() => {
this.showMessage(
'Tip: You can update your password anytime in the Account Settings tab.',
'info',
8000
);
}, 2000);
}
} catch (error) {
console.error('Error loading pending campaign:', error);
}
}
}
populateCampaignFormFromEmail(campaignData) {
// Pre-fill form fields with email data
const titleField = document.getElementById('create-title');
const descriptionField = document.getElementById('create-description');
const subjectField = document.getElementById('create-email-subject');
const bodyField = document.getElementById('create-email-body');
// Auto-generate title from subject
if (titleField && campaignData.subject) {
const suggestedTitle = campaignData.subject.length > 50
? campaignData.subject.substring(0, 47) + '...'
: campaignData.subject;
titleField.value = suggestedTitle;
}
// Add helpful description template
if (descriptionField && campaignData.postalCode) {
descriptionField.value = `Join others from ${campaignData.postalCode} and across the region in sending this important message to our representatives.`;
}
// Fill in email content
if (subjectField && campaignData.subject) {
subjectField.value = campaignData.subject;
}
if (bodyField && campaignData.message) {
bodyField.value = campaignData.message;
}
// Add a subtle highlight to show pre-filled fields
[titleField, descriptionField, subjectField, bodyField].forEach(field => {
if (field && field.value) {
field.style.backgroundColor = '#fffacd'; // Light yellow
// Remove highlight on focus
field.addEventListener('focus', function() {
this.style.backgroundColor = '';
}, { once: true });
}
});
// Scroll to top of form
const createTab = document.getElementById('create-tab');
if (createTab) {
createTab.scrollTop = 0;
}
}
async loadUserCampaigns() { async loadUserCampaigns() {
const loadingDiv = document.getElementById('campaigns-loading'); const loadingDiv = document.getElementById('campaigns-loading');
const listDiv = document.getElementById('campaigns-list'); const listDiv = document.getElementById('campaigns-list');

View File

@ -572,6 +572,9 @@ class EmailComposer {
if (result.success) { if (result.success) {
window.messageDisplay.show('Email sent successfully! Your representative will receive your message.', 'success', 7000); window.messageDisplay.show('Email sent successfully! Your representative will receive your message.', 'success', 7000);
// Show campaign conversion prompt if user is not authenticated
this.showCampaignConversionPrompt(this.currentEmailData);
// Close the inline composer after successful send // Close the inline composer after successful send
this.closeInlineComposer(); this.closeInlineComposer();
this.currentEmailData = null; this.currentEmailData = null;
@ -851,6 +854,10 @@ class EmailComposer {
if (result.success) { if (result.success) {
window.messageDisplay.show('Email sent successfully! Your representative will receive your message.', 'success'); window.messageDisplay.show('Email sent successfully! Your representative will receive your message.', 'success');
// Show campaign conversion prompt if user is not authenticated
this.showCampaignConversionPrompt(this.currentEmailData);
this.closePreviewModal(); this.closePreviewModal();
this.currentEmailData = null; this.currentEmailData = null;
} else { } else {
@ -927,6 +934,132 @@ Respectfully,
return templates[type] || templates.general; return templates[type] || templates.general;
} }
// Campaign conversion prompt
showCampaignConversionPrompt(emailData) {
// Only show if user is not authenticated
if (window.authManager && window.authManager.isAuthenticated) {
return;
}
// Create the conversion prompt
const promptHTML = `
<div class="campaign-conversion-prompt" style="
position: fixed;
bottom: 20px;
right: 20px;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
max-width: 400px;
z-index: 10000;
animation: slideIn 0.3s ease-out;
">
<button class="close-prompt" style="
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
">&times;</button>
<h3 style="margin-top: 0; color: #d73027;">🚀 Turn Your Message Into a Campaign!</h3>
<p style="margin: 15px 0; color: #666;">
Want others to join your cause? Create a campaign so more people can send similar messages.
</p>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button id="convert-to-campaign" class="btn btn-primary" style="flex: 1;">
Create Campaign
</button>
<button id="skip-conversion" class="btn btn-secondary" style="flex: 1;">
No Thanks
</button>
</div>
</div>
<style>
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
</style>
`;
// Insert the prompt
const promptDiv = document.createElement('div');
promptDiv.innerHTML = promptHTML;
document.body.appendChild(promptDiv);
// Attach event listeners
const convertBtn = promptDiv.querySelector('#convert-to-campaign');
const skipBtn = promptDiv.querySelector('#skip-conversion');
const closeBtn = promptDiv.querySelector('.close-prompt');
const removePrompt = () => {
promptDiv.remove();
};
convertBtn.addEventListener('click', () => {
this.initiateConversion(emailData);
removePrompt();
});
skipBtn.addEventListener('click', removePrompt);
closeBtn.addEventListener('click', removePrompt);
// Auto-remove after 30 seconds
setTimeout(removePrompt, 30000);
}
async initiateConversion(emailData) {
if (!emailData) {
window.messageDisplay.show('No email data available for conversion', 'error');
return;
}
try {
// Show loading message
window.messageDisplay.show('Sending verification email...', 'info');
const conversionData = {
email: emailData.senderEmail,
senderName: emailData.senderName,
subject: emailData.subject,
message: emailData.message,
postalCode: emailData.postalCode
};
const response = await window.apiClient.post('/emails/convert-to-campaign', conversionData);
if (response.success) {
window.messageDisplay.show(
'Verification email sent! Please check your inbox to complete campaign creation.',
'success',
10000
);
} else {
throw new Error(response.error || 'Failed to send verification email');
}
} catch (error) {
console.error('Campaign conversion error:', error);
window.messageDisplay.show(
`Failed to initiate campaign creation: ${error.message}`,
'error',
8000
);
}
}
} }
// Initialize when DOM is loaded // Initialize when DOM is loaded

View File

@ -0,0 +1,89 @@
// Email Verification Handler
document.addEventListener('DOMContentLoaded', async () => {
const statusDiv = document.getElementById('verification-status');
// Get token from URL query parameters
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
if (!token) {
showError('Invalid verification link - no token provided');
return;
}
try {
// Call API to verify token
const response = await fetch(`/api/verify-email/${token}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include' // Important for session management
});
const data = await response.json();
if (data.success) {
// Store campaign data in session storage for dashboard
if (data.campaignData) {
sessionStorage.setItem('pendingCampaign', JSON.stringify(data.campaignData));
}
// Show success and redirect
if (data.isNewUser) {
showSuccess(
'✅ Welcome! Your account has been created successfully. Check your email for login credentials. Your email content is ready - just add a campaign title and description!',
'/dashboard.html',
'Go to Dashboard',
4000
);
} else if (data.needsAccount) {
showSuccess(
'Email verified! Please create your account to continue.',
'/login.html',
'Go to Login',
3000
);
} else {
showSuccess(
'Email verified! Redirecting to campaign creation...',
'/dashboard.html',
'Go to Dashboard',
2000
);
}
} else {
showError(data.error || 'Verification failed');
}
} catch (error) {
console.error('Verification error:', error);
showError('An error occurred during verification. Please try again or contact support.');
}
});
function showSuccess(message, redirectUrl, buttonText, autoRedirectDelay = 3000) {
const statusDiv = document.getElementById('verification-status');
statusDiv.innerHTML = `
<div class="success-icon"></div>
<h2 style="color: #28a745;">Verification Successful!</h2>
<p class="message">${message}</p>
<a href="${redirectUrl}" class="btn">${buttonText}</a>
`;
// Auto-redirect after delay
setTimeout(() => {
window.location.href = redirectUrl;
}, autoRedirectDelay);
}
function showError(errorMessage) {
const statusDiv = document.getElementById('verification-status');
statusDiv.innerHTML = `
<div class="error-icon"></div>
<h2 style="color: #dc3545;">Verification Failed</h2>
<p class="message">${errorMessage}</p>
<a href="/" class="btn" style="background-color: #666;">Return to Home</a>
`;
}

View File

@ -0,0 +1,102 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email Verification - BNKops Influence</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="/css/styles.css">
<style>
body {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.verification-container {
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
max-width: 500px;
width: 90%;
text-align: center;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #d73027;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 20px auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.success-icon {
font-size: 64px;
color: #28a745;
margin: 20px 0;
}
.error-icon {
font-size: 64px;
color: #dc3545;
margin: 20px 0;
}
.message {
margin: 20px 0;
color: #666;
line-height: 1.6;
}
.btn {
display: inline-block;
margin-top: 20px;
padding: 12px 24px;
background-color: #d73027;
color: white;
text-decoration: none;
border-radius: 6px;
font-weight: bold;
transition: background-color 0.3s;
}
.btn:hover {
background-color: #c02822;
}
.logo {
color: #d73027;
font-size: 28px;
font-weight: bold;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="verification-container">
<div class="logo">BNKops Influence</div>
<div id="verification-status">
<div class="loading">
<div class="spinner"></div>
<p class="message">Verifying your email...</p>
</div>
</div>
</div>
</div>
<script src="/js/api-client.js"></script>
<script src="/js/verify-email.js"></script>
</body>
</html>

View File

@ -90,6 +90,26 @@ router.post(
emailsController.sendTestEmail emailsController.sendTestEmail
); );
// Email-to-campaign conversion endpoints
router.post(
'/emails/convert-to-campaign',
rateLimiter.general,
[
body('email').isEmail().withMessage('Valid email is required'),
body('subject').notEmpty().withMessage('Subject is required'),
body('message').notEmpty().withMessage('Message is required'),
body('postalCode').matches(/^[A-Za-z]\d[A-Za-z]\s?\d[A-Za-z]\d$/).withMessage('Invalid postal code format')
],
handleValidationErrors,
emailsController.initiateEmailToCampaign
);
router.get(
'/verify-email/:token',
rateLimiter.general,
emailsController.verifyEmailToken
);
router.get( router.get(
'/emails/logs', '/emails/logs',
requireAdmin, requireAdmin,

View File

@ -386,6 +386,29 @@ class EmailService {
throw error; throw error;
} }
} }
async sendEmailVerification(recipientEmail, verificationUrl, userName) {
try {
const templateVariables = {
USER_NAME: userName || 'there',
VERIFICATION_URL: verificationUrl
};
const emailOptions = {
to: recipientEmail,
from: {
email: process.env.SMTP_FROM_EMAIL,
name: process.env.SMTP_FROM_NAME
},
subject: 'Verify Your Email to Create Your Campaign'
};
return await this.sendTemplatedEmail('email-verification', templateVariables, emailOptions);
} catch (error) {
console.error('Failed to send email verification:', error);
throw error;
}
}
} }
module.exports = new EmailService(); module.exports = new EmailService();

View File

@ -28,7 +28,8 @@ class NocoDBService {
users: process.env.NOCODB_TABLE_USERS, users: process.env.NOCODB_TABLE_USERS,
calls: process.env.NOCODB_TABLE_CALLS, calls: process.env.NOCODB_TABLE_CALLS,
representativeResponses: process.env.NOCODB_TABLE_REPRESENTATIVE_RESPONSES, representativeResponses: process.env.NOCODB_TABLE_REPRESENTATIVE_RESPONSES,
responseUpvotes: process.env.NOCODB_TABLE_RESPONSE_UPVOTES responseUpvotes: process.env.NOCODB_TABLE_RESPONSE_UPVOTES,
emailVerifications: process.env.NOCODB_TABLE_EMAIL_VERIFICATIONS
}; };
// Validate that all table IDs are set // Validate that all table IDs are set
@ -877,6 +878,83 @@ class NocoDBService {
created_at: data.CreatedAt || data.created_at created_at: data.CreatedAt || data.created_at
}; };
} }
// Email verification methods
async createEmailVerification(verificationData) {
if (!this.tableIds.emailVerifications) {
throw new Error('Email verifications table not configured');
}
const data = {
'Token': verificationData.token,
'Email': verificationData.email,
'Temp Campaign Data': verificationData.temp_campaign_data,
'Created At': verificationData.created_at,
'Expires At': verificationData.expires_at,
'Used': verificationData.used || false
};
return await this.create(this.tableIds.emailVerifications, data);
}
async getEmailVerificationByToken(token) {
if (!this.tableIds.emailVerifications) {
throw new Error('Email verifications table not configured');
}
try {
const response = await this.getAll(this.tableIds.emailVerifications, {
where: `(Token,eq,${token})`,
limit: 1
});
return response.list?.[0] || null;
} catch (error) {
console.error('Error in getEmailVerificationByToken:', error.message);
throw error;
}
}
async updateEmailVerification(verificationId, updateData) {
if (!this.tableIds.emailVerifications) {
throw new Error('Email verifications table not configured');
}
const data = {};
if (updateData.used !== undefined) {
data['Used'] = updateData.used;
}
return await this.update(this.tableIds.emailVerifications, verificationId, data);
}
async deleteExpiredEmailVerifications() {
if (!this.tableIds.emailVerifications) {
throw new Error('Email verifications table not configured');
}
try {
const now = new Date().toISOString();
const response = await this.getAll(this.tableIds.emailVerifications, {
where: `(Expires At,lt,${now})`
});
if (response.list && response.list.length > 0) {
for (const verification of response.list) {
const id = verification.ID || verification.Id || verification.id;
if (id) {
await this.client.delete(`${this.getTableUrl(this.tableIds.emailVerifications)}/${id}`);
}
}
return { success: true, deletedCount: response.list.length };
}
return { success: true, deletedCount: 0 };
} catch (error) {
console.error('Error deleting expired email verifications:', error.message);
return { success: false, error: error.message };
}
}
} }
module.exports = new NocoDBService(); module.exports = new NocoDBService();

View File

@ -0,0 +1,113 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Verify Your Email to Create a Campaign</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.container {
background-color: #f9f9f9;
border-radius: 8px;
padding: 30px;
border: 1px solid #e0e0e0;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.logo {
color: #d73027;
font-size: 24px;
font-weight: bold;
}
.content {
background-color: white;
padding: 20px;
border-radius: 6px;
margin-bottom: 20px;
}
.cta-button {
display: inline-block;
background-color: #d73027;
color: white !important;
padding: 14px 28px;
text-decoration: none;
border-radius: 4px;
margin: 20px 0;
font-weight: bold;
text-align: center;
}
.footer {
text-align: center;
font-size: 12px;
color: #666;
margin-top: 30px;
}
.info {
color: #3498db;
font-size: 14px;
margin-top: 20px;
padding: 15px;
background-color: #e8f4f8;
border-radius: 4px;
}
.highlight {
background-color: #fff3cd;
padding: 15px;
border-radius: 4px;
margin: 15px 0;
border-left: 4px solid #ffc107;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">{{APP_NAME}}</div>
</div>
<div class="content">
<h2>🚀 Verify Your Email to Create a Campaign</h2>
<p>Hi {{USER_NAME}},</p>
<p>You're one step away from turning your message into a powerful campaign!</p>
<div class="highlight">
<strong>What happens next?</strong>
<ul style="margin: 10px 0;">
<li>Click the verification button below</li>
<li>Create your account (if you don't have one)</li>
<li>Your email content will be pre-filled</li>
<li>Add campaign details and publish!</li>
</ul>
</div>
<p style="text-align: center;">
<a href="{{VERIFICATION_URL}}" class="cta-button">Verify Email & Create Campaign</a>
</p>
<div class="info">
<strong>⏰ Important:</strong> This link expires in 24 hours.
</div>
<p style="margin-top: 20px; font-size: 14px; color: #666;">
If you didn't request this, you can safely ignore this email.
</p>
<p style="margin-top: 20px; font-size: 12px; color: #999;">
If the button doesn't work, copy and paste this link into your browser:<br>
<span style="word-break: break-all;">{{VERIFICATION_URL}}</span>
</p>
</div>
<div class="footer">
<p>{{APP_NAME}} - Empowering civic engagement</p>
<p style="margin-top: 10px;">{{TIMESTAMP}}</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,24 @@
{{APP_NAME}}
Verify Your Email to Create a Campaign
Hi {{USER_NAME}},
You're one step away from turning your message into a powerful campaign!
What happens next?
- Click the verification link below
- Create your account (if you don't have one)
- Your email content will be pre-filled
- Add campaign details and publish!
Verify your email by clicking this link:
{{VERIFICATION_URL}}
⏰ Important: This link expires in 24 hours.
If you didn't request this, you can safely ignore this email.
---
{{APP_NAME}} - Empowering civic engagement
{{TIMESTAMP}}

View File

@ -39,6 +39,11 @@ NODE_ENV=development
EMAIL_TEST_MODE=true EMAIL_TEST_MODE=true
TEST_EMAIL_RECIPIENT=your-test-email@domain.com TEST_EMAIL_RECIPIENT=your-test-email@domain.com
# Email Verification Configuration
# For email-to-campaign conversion feature
EMAIL_VERIFICATION_ENABLED=true
EMAIL_VERIFICATION_EXPIRY=24
# NocoDB Table IDs # NocoDB Table IDs
# These will be auto-generated when you run build-nocodb.sh # These will be auto-generated when you run build-nocodb.sh
# DO NOT modify these manually - they are set by the setup script # DO NOT modify these manually - they are set by the setup script
@ -48,6 +53,10 @@ NOCODB_TABLE_POSTAL_CODES=
NOCODB_TABLE_CAMPAIGN_EMAILS= NOCODB_TABLE_CAMPAIGN_EMAILS=
NOCODB_TABLE_CAMPAIGNS= NOCODB_TABLE_CAMPAIGNS=
NOCODB_TABLE_USERS= NOCODB_TABLE_USERS=
NOCODB_TABLE_CALLS=
NOCODB_TABLE_REPRESENTATIVE_RESPONSES=
NOCODB_TABLE_RESPONSE_UPVOTES=
NOCODB_TABLE_EMAIL_VERIFICATIONS=
# Optional: Development Mode Settings # Optional: Development Mode Settings
# Uncomment and modify these for local development with MailHog # Uncomment and modify these for local development with MailHog

View File

@ -1615,6 +1615,65 @@ create_users_table() {
create_table "$base_id" "influence_users" "$table_data" "User authentication and management" create_table "$base_id" "influence_users" "$table_data" "User authentication and management"
} }
# Function to create the email verifications table
create_email_verifications_table() {
local base_id=$1
local table_data='{
"table_name": "influence_email_verifications",
"title": "Influence Email Verifications",
"columns": [
{
"column_name": "id",
"title": "ID",
"uidt": "ID",
"pk": true,
"ai": true,
"rqd": true
},
{
"column_name": "token",
"title": "Token",
"uidt": "SingleLineText",
"rqd": true
},
{
"column_name": "email",
"title": "Email",
"uidt": "Email",
"rqd": true
},
{
"column_name": "temp_campaign_data",
"title": "Temp Campaign Data",
"uidt": "LongText",
"rqd": false
},
{
"column_name": "created_at",
"title": "Created At",
"uidt": "DateTime",
"rqd": false
},
{
"column_name": "expires_at",
"title": "Expires At",
"uidt": "DateTime",
"rqd": true
},
{
"column_name": "used",
"title": "Used",
"uidt": "Checkbox",
"rqd": false,
"cdf": "false"
}
]
}'
create_table "$base_id" "influence_email_verifications" "$table_data" "Email verification tokens for campaign conversion"
}
# Function to create a new base # Function to create a new base
create_base() { create_base() {
local base_data='{ local base_data='{
@ -1654,6 +1713,7 @@ update_env_with_table_ids() {
local call_logs_table_id=$8 local call_logs_table_id=$8
local representative_responses_table_id=$9 local representative_responses_table_id=$9
local response_upvotes_table_id=${10} local response_upvotes_table_id=${10}
local email_verifications_table_id=${11}
print_status "Updating .env file with NocoDB project and table IDs..." print_status "Updating .env file with NocoDB project and table IDs..."
@ -1691,6 +1751,7 @@ update_env_with_table_ids() {
update_env_var "NOCODB_TABLE_CALLS" "$call_logs_table_id" update_env_var "NOCODB_TABLE_CALLS" "$call_logs_table_id"
update_env_var "NOCODB_TABLE_REPRESENTATIVE_RESPONSES" "$representative_responses_table_id" update_env_var "NOCODB_TABLE_REPRESENTATIVE_RESPONSES" "$representative_responses_table_id"
update_env_var "NOCODB_TABLE_RESPONSE_UPVOTES" "$response_upvotes_table_id" update_env_var "NOCODB_TABLE_RESPONSE_UPVOTES" "$response_upvotes_table_id"
update_env_var "NOCODB_TABLE_EMAIL_VERIFICATIONS" "$email_verifications_table_id"
print_success "Successfully updated .env file with all table IDs" print_success "Successfully updated .env file with all table IDs"
@ -1828,8 +1889,15 @@ main() {
exit 1 exit 1
fi fi
# Create email verifications table
EMAIL_VERIFICATIONS_TABLE_ID=$(create_email_verifications_table "$BASE_ID")
if [[ $? -ne 0 ]]; then
print_error "Failed to create email verifications table"
exit 1
fi
# Validate all table IDs were created successfully # Validate all table IDs were created successfully
if ! validate_table_ids "$REPRESENTATIVES_TABLE_ID" "$EMAIL_LOGS_TABLE_ID" "$POSTAL_CODES_TABLE_ID" "$CAMPAIGNS_TABLE_ID" "$CAMPAIGN_EMAILS_TABLE_ID" "$USERS_TABLE_ID" "$CALL_LOGS_TABLE_ID" "$REPRESENTATIVE_RESPONSES_TABLE_ID" "$RESPONSE_UPVOTES_TABLE_ID"; then if ! validate_table_ids "$REPRESENTATIVES_TABLE_ID" "$EMAIL_LOGS_TABLE_ID" "$POSTAL_CODES_TABLE_ID" "$CAMPAIGNS_TABLE_ID" "$CAMPAIGN_EMAILS_TABLE_ID" "$USERS_TABLE_ID" "$CALL_LOGS_TABLE_ID" "$REPRESENTATIVE_RESPONSES_TABLE_ID" "$RESPONSE_UPVOTES_TABLE_ID" "$EMAIL_VERIFICATIONS_TABLE_ID"; then
print_error "One or more table IDs are invalid" print_error "One or more table IDs are invalid"
exit 1 exit 1
fi fi
@ -1853,6 +1921,7 @@ main() {
table_mapping["influence_call_logs"]="$CALL_LOGS_TABLE_ID" table_mapping["influence_call_logs"]="$CALL_LOGS_TABLE_ID"
table_mapping["influence_representative_responses"]="$REPRESENTATIVE_RESPONSES_TABLE_ID" table_mapping["influence_representative_responses"]="$REPRESENTATIVE_RESPONSES_TABLE_ID"
table_mapping["influence_response_upvotes"]="$RESPONSE_UPVOTES_TABLE_ID" table_mapping["influence_response_upvotes"]="$RESPONSE_UPVOTES_TABLE_ID"
table_mapping["influence_email_verifications"]="$EMAIL_VERIFICATIONS_TABLE_ID"
# Get source table information # Get source table information
local source_tables_response local source_tables_response
@ -1907,6 +1976,7 @@ main() {
print_status " - influence_call_logs (ID: $CALL_LOGS_TABLE_ID)" print_status " - influence_call_logs (ID: $CALL_LOGS_TABLE_ID)"
print_status " - influence_representative_responses (ID: $REPRESENTATIVE_RESPONSES_TABLE_ID)" print_status " - influence_representative_responses (ID: $REPRESENTATIVE_RESPONSES_TABLE_ID)"
print_status " - influence_response_upvotes (ID: $RESPONSE_UPVOTES_TABLE_ID)" print_status " - influence_response_upvotes (ID: $RESPONSE_UPVOTES_TABLE_ID)"
print_status " - influence_email_verifications (ID: $EMAIL_VERIFICATIONS_TABLE_ID)"
# Automatically update .env file with new project ID # Automatically update .env file with new project ID
print_status "" print_status ""
@ -1929,7 +1999,7 @@ main() {
fi fi
# Update .env file with table IDs # Update .env file with table IDs
update_env_with_table_ids "$BASE_ID" "$REPRESENTATIVES_TABLE_ID" "$EMAIL_LOGS_TABLE_ID" "$POSTAL_CODES_TABLE_ID" "$CAMPAIGNS_TABLE_ID" "$CAMPAIGN_EMAILS_TABLE_ID" "$USERS_TABLE_ID" "$CALL_LOGS_TABLE_ID" "$REPRESENTATIVE_RESPONSES_TABLE_ID" "$RESPONSE_UPVOTES_TABLE_ID" update_env_with_table_ids "$BASE_ID" "$REPRESENTATIVES_TABLE_ID" "$EMAIL_LOGS_TABLE_ID" "$POSTAL_CODES_TABLE_ID" "$CAMPAIGNS_TABLE_ID" "$CAMPAIGN_EMAILS_TABLE_ID" "$USERS_TABLE_ID" "$CALL_LOGS_TABLE_ID" "$REPRESENTATIVE_RESPONSES_TABLE_ID" "$RESPONSE_UPVOTES_TABLE_ID" "$EMAIL_VERIFICATIONS_TABLE_ID"
print_status "" print_status ""
print_status "============================================================" print_status "============================================================"