const express = require('express'); const router = express.Router(); const { body, param, validationResult } = require('express-validator'); const representativesController = require('../controllers/representatives'); const emailsController = require('../controllers/emails'); const campaignsController = require('../controllers/campaigns'); const responsesController = require('../controllers/responses'); const rateLimiter = require('../utils/rate-limiter'); const { requireAdmin, requireAuth, requireNonTemp, optionalAuth } = require('../middleware/auth'); const upload = require('../middleware/upload'); // Import user routes const userRoutes = require('./users'); // Validation middleware const handleValidationErrors = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } next(); }; // Test endpoints router.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); router.get('/test-represent', representativesController.testConnection); router.get('/test-smtp', requireAdmin, emailsController.testSMTPConnection); // Representatives endpoints router.get( '/representatives/by-postal/:postalCode', param('postalCode').matches(/^[A-Za-z]\d[A-Za-z]\s?\d[A-Za-z]\d$/).withMessage('Invalid postal code format'), handleValidationErrors, rateLimiter.general, representativesController.getByPostalCode ); router.post( '/representatives/refresh-postal/:postalCode', param('postalCode').matches(/^[A-Za-z]\d[A-Za-z]\s?\d[A-Za-z]\d$/).withMessage('Invalid postal code format'), handleValidationErrors, representativesController.refreshPostalCode ); // Email endpoints router.post( '/emails/preview', rateLimiter.general, [ body('recipientEmail').isEmail().withMessage('Valid recipient email is required'), body('senderName').notEmpty().withMessage('Sender name is required'), body('senderEmail').isEmail().withMessage('Valid sender 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.previewEmail ); router.post( '/emails/send', rateLimiter.email, // General hourly rate limit rateLimiter.perRecipientEmailLimiter, // Per-recipient 5-minute rate limit [ body('recipientEmail').isEmail().withMessage('Valid email is required'), body('senderName').notEmpty().withMessage('Sender name is required'), body('senderEmail').isEmail().withMessage('Valid sender 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.sendEmail ); // Email testing endpoints router.post( '/emails/test', requireAdmin, rateLimiter.general, [ body('subject').notEmpty().withMessage('Subject is required'), body('message').notEmpty().withMessage('Message is required') ], handleValidationErrors, emailsController.sendTestEmail ); router.get( '/emails/logs', requireAdmin, rateLimiter.general, emailsController.getEmailLogs ); // Campaign endpoints (Admin) - Protected router.get('/admin/campaigns', requireAdmin, rateLimiter.general, campaignsController.getAllCampaigns); router.get('/admin/campaigns/:id', requireAdmin, rateLimiter.general, campaignsController.getCampaignById); router.post( '/admin/campaigns', requireAdmin, campaignsController.upload.single('cover_photo'), rateLimiter.general, [ body('title').notEmpty().withMessage('Campaign title is required'), body('email_subject').notEmpty().withMessage('Email subject is required'), body('email_body').notEmpty().withMessage('Email body is required') ], handleValidationErrors, campaignsController.createCampaign ); router.put('/admin/campaigns/:id', requireAdmin, campaignsController.upload.single('cover_photo'), rateLimiter.general, campaignsController.updateCampaign); router.delete('/admin/campaigns/:id', requireAdmin, rateLimiter.general, campaignsController.deleteCampaign); router.get('/admin/campaigns/:id/analytics', requireAdmin, rateLimiter.general, campaignsController.getCampaignAnalytics); // Campaign endpoints (Authenticated users) router.get('/campaigns', requireAuth, rateLimiter.general, campaignsController.getAllCampaigns); router.post( '/campaigns', requireNonTemp, campaignsController.upload.single('cover_photo'), rateLimiter.general, [ body('title').notEmpty().withMessage('Campaign title is required'), body('email_subject').notEmpty().withMessage('Email subject is required'), body('email_body').notEmpty().withMessage('Email body is required') ], handleValidationErrors, campaignsController.createCampaign ); router.put( '/campaigns/:id', requireNonTemp, campaignsController.upload.single('cover_photo'), rateLimiter.general, campaignsController.updateCampaign ); router.get('/campaigns/:id/analytics', requireAuth, rateLimiter.general, campaignsController.getCampaignAnalytics); // Campaign endpoints (Public) router.get('/public/campaigns', rateLimiter.general, campaignsController.getPublicCampaigns); router.get('/campaigns/:slug', rateLimiter.general, campaignsController.getCampaignBySlug); router.get('/campaigns/:slug/representatives/:postalCode', rateLimiter.representAPI, campaignsController.getRepresentativesForCampaign); router.post( '/campaigns/:slug/track-user', rateLimiter.general, [ body('postalCode').matches(/^[A-Za-z]\d[A-Za-z]\s?\d[A-Za-z]\d$/).withMessage('Invalid postal code format') ], handleValidationErrors, campaignsController.trackUserInfo ); router.post( '/campaigns/:slug/send-email', rateLimiter.email, // General hourly rate limit rateLimiter.perRecipientEmailLimiter, // Per-recipient 5-minute rate limit [ body('recipientEmail').isEmail().withMessage('Valid recipient email is required'), body('postalCode').matches(/^[A-Za-z]\d[A-Za-z]\s?\d[A-Za-z]\d$/).withMessage('Invalid postal code format'), body('emailMethod').isIn(['smtp', 'mailto']).withMessage('Email method must be smtp or mailto') ], handleValidationErrors, campaignsController.sendCampaignEmail ); // Campaign call tracking endpoint router.post( '/campaigns/:slug/track-call', rateLimiter.general, [ body('representativeName').notEmpty().withMessage('Representative name is required'), body('phoneNumber').notEmpty().withMessage('Phone number is required') ], handleValidationErrors, campaignsController.trackCampaignCall ); // General call tracking endpoint (non-campaign) router.post( '/track-call', rateLimiter.general, [ body('representativeName').notEmpty().withMessage('Representative name is required'), body('phoneNumber').notEmpty().withMessage('Phone number is required') ], handleValidationErrors, representativesController.trackCall ); // User management routes (admin only) router.use('/admin/users', userRoutes); // Response Wall Routes router.get('/campaigns/:slug/responses', rateLimiter.general, responsesController.getCampaignResponses); router.get('/campaigns/:slug/response-stats', rateLimiter.general, responsesController.getResponseStats); router.post( '/campaigns/:slug/responses', optionalAuth, upload.single('screenshot'), rateLimiter.general, [ body('representative_name').notEmpty().withMessage('Representative name is required'), body('representative_level').isIn(['Federal', 'Provincial', 'Municipal', 'School Board']).withMessage('Invalid representative level'), body('response_type').isIn(['Email', 'Letter', 'Phone Call', 'Meeting', 'Social Media', 'Other']).withMessage('Invalid response type'), body('response_text').notEmpty().withMessage('Response text is required') ], handleValidationErrors, responsesController.submitResponse ); router.post('/responses/:id/upvote', optionalAuth, rateLimiter.general, responsesController.upvoteResponse); router.delete('/responses/:id/upvote', optionalAuth, rateLimiter.general, responsesController.removeUpvote); // 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, [body('status').isIn(['pending', 'approved', 'rejected']).withMessage('Invalid status')], handleValidationErrors, responsesController.updateResponseStatus ); router.patch('/admin/responses/:id', requireAdmin, rateLimiter.general, responsesController.updateResponse); router.delete('/admin/responses/:id', requireAdmin, rateLimiter.general, responsesController.deleteResponse); // Debug endpoint to check raw NocoDB data router.get('/debug/responses', requireAdmin, async (req, res) => { try { const nocodbService = require('../services/nocodb'); // Get raw data without normalization const rawResult = await nocodbService.getAll(nocodbService.tableIds.representativeResponses, {}); res.json({ success: true, count: rawResult.list?.length || 0, rawResponses: rawResult.list || [], normalized: await nocodbService.getRepresentativeResponses({}) }); } catch (error) { res.status(500).json({ error: error.message, stack: error.stack }); } }); module.exports = router;