new system for creating campaigns from the main site dashboard
This commit is contained in:
parent
b71a6e4ff3
commit
06ecffaf4d
@ -1,5 +1,6 @@
|
||||
const emailService = require('../services/email');
|
||||
const nocoDB = require('../services/nocodb');
|
||||
const crypto = require('crypto');
|
||||
|
||||
class EmailsController {
|
||||
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();
|
||||
@ -28,6 +28,9 @@ class UserDashboard {
|
||||
this.setupEventListeners();
|
||||
this.loadUserCampaigns();
|
||||
this.loadAnalytics();
|
||||
|
||||
// Check for pending campaign from email verification
|
||||
this.checkPendingCampaign();
|
||||
}
|
||||
|
||||
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() {
|
||||
const loadingDiv = document.getElementById('campaigns-loading');
|
||||
const listDiv = document.getElementById('campaigns-list');
|
||||
|
||||
@ -572,6 +572,9 @@ class EmailComposer {
|
||||
if (result.success) {
|
||||
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
|
||||
this.closeInlineComposer();
|
||||
this.currentEmailData = null;
|
||||
@ -851,6 +854,10 @@ class EmailComposer {
|
||||
|
||||
if (result.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.currentEmailData = null;
|
||||
} else {
|
||||
@ -927,6 +934,132 @@ Respectfully,
|
||||
|
||||
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;
|
||||
">×</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
|
||||
|
||||
89
influence/app/public/js/verify-email.js
Normal file
89
influence/app/public/js/verify-email.js
Normal 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>
|
||||
`;
|
||||
}
|
||||
102
influence/app/public/verify-email.html
Normal file
102
influence/app/public/verify-email.html
Normal 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>
|
||||
@ -90,6 +90,26 @@ router.post(
|
||||
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(
|
||||
'/emails/logs',
|
||||
requireAdmin,
|
||||
|
||||
@ -386,6 +386,29 @@ class EmailService {
|
||||
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();
|
||||
@ -28,7 +28,8 @@ class NocoDBService {
|
||||
users: process.env.NOCODB_TABLE_USERS,
|
||||
calls: process.env.NOCODB_TABLE_CALLS,
|
||||
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
|
||||
@ -877,6 +878,83 @@ class NocoDBService {
|
||||
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();
|
||||
|
||||
113
influence/app/templates/email/email-verification.html
Normal file
113
influence/app/templates/email/email-verification.html
Normal 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>
|
||||
24
influence/app/templates/email/email-verification.txt
Normal file
24
influence/app/templates/email/email-verification.txt
Normal 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}}
|
||||
@ -39,6 +39,11 @@ NODE_ENV=development
|
||||
EMAIL_TEST_MODE=true
|
||||
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
|
||||
# These will be auto-generated when you run build-nocodb.sh
|
||||
# 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_CAMPAIGNS=
|
||||
NOCODB_TABLE_USERS=
|
||||
NOCODB_TABLE_CALLS=
|
||||
NOCODB_TABLE_REPRESENTATIVE_RESPONSES=
|
||||
NOCODB_TABLE_RESPONSE_UPVOTES=
|
||||
NOCODB_TABLE_EMAIL_VERIFICATIONS=
|
||||
|
||||
# Optional: Development Mode Settings
|
||||
# Uncomment and modify these for local development with MailHog
|
||||
|
||||
@ -1615,6 +1615,65 @@ create_users_table() {
|
||||
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
|
||||
create_base() {
|
||||
local base_data='{
|
||||
@ -1654,6 +1713,7 @@ update_env_with_table_ids() {
|
||||
local call_logs_table_id=$8
|
||||
local representative_responses_table_id=$9
|
||||
local response_upvotes_table_id=${10}
|
||||
local email_verifications_table_id=${11}
|
||||
|
||||
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_REPRESENTATIVE_RESPONSES" "$representative_responses_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"
|
||||
|
||||
@ -1828,8 +1889,15 @@ main() {
|
||||
exit 1
|
||||
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
|
||||
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"
|
||||
exit 1
|
||||
fi
|
||||
@ -1853,6 +1921,7 @@ main() {
|
||||
table_mapping["influence_call_logs"]="$CALL_LOGS_TABLE_ID"
|
||||
table_mapping["influence_representative_responses"]="$REPRESENTATIVE_RESPONSES_TABLE_ID"
|
||||
table_mapping["influence_response_upvotes"]="$RESPONSE_UPVOTES_TABLE_ID"
|
||||
table_mapping["influence_email_verifications"]="$EMAIL_VERIFICATIONS_TABLE_ID"
|
||||
|
||||
# Get source table information
|
||||
local source_tables_response
|
||||
@ -1907,6 +1976,7 @@ main() {
|
||||
print_status " - influence_call_logs (ID: $CALL_LOGS_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_email_verifications (ID: $EMAIL_VERIFICATIONS_TABLE_ID)"
|
||||
|
||||
# Automatically update .env file with new project ID
|
||||
print_status ""
|
||||
@ -1929,7 +1999,7 @@ main() {
|
||||
fi
|
||||
|
||||
# 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 "============================================================"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user