require('dotenv').config(); const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const morgan = require('morgan'); const crypto = require('crypto'); const { DynamoDBClient } = require('@aws-sdk/client-dynamodb'); const { DynamoDBDocumentClient, GetCommand, PutCommand, DeleteCommand, ScanCommand } = require('@aws-sdk/lib-dynamodb'); const { body, param, validationResult, query } = require('express-validator'); const { spawn } = require('child_process'); const path = require('path'); const app = express(); const PORT = process.env.PORT || 3001; const TOKEN_SECRET = process.env.TOKEN_SECRET_KEY; // Middleware app.use(helmet()); //app.use(cors()); const corsOptions = { origin: [ 'https://config.email-bayarea.com', ], methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'], allowedHeaders: [ 'Content-Type', 'Accept', 'Authorization', 'x-hide-loading', ], credentials: false, // true nur wenn Cookies / Auth-Headers mit credentials genutzt werden }; app.use(cors(corsOptions)); app.options('*', cors(corsOptions)); app.use(express.json()); app.use(morgan('dev')); // AWS DynamoDB Configuration const client = new DynamoDBClient({ region: process.env.AWS_REGION || 'us-east-2', credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, }, }); const docClient = DynamoDBDocumentClient.from(client); const TABLE_NAME = process.env.DYNAMODB_TABLE || 'email-rules'; // Validation Middleware const validateEmailRule = [ body('email_address').isEmail().withMessage('Valid email address is required'), body('ooo_active').optional().isBoolean().withMessage('ooo_active must be a boolean'), body('ooo_message').optional().isString().withMessage('ooo_message must be a string'), body('ooo_content_type').optional().isIn(['text', 'html']).withMessage('ooo_content_type must be "text" or "html"'), body('forwards').optional().isArray().withMessage('forwards must be an array'), body('forwards.*').optional().isEmail().withMessage('All forward addresses must be valid emails'), ]; const validateEmail = [ param('email').isEmail().withMessage('Valid email address is required'), ]; // Error Handler Middleware const handleValidationErrors = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ error: 'Validation failed', details: errors.array(), }); } next(); }; // Token Validation Function const validateToken = (email, expires, signature) => { // Check expiry const now = Math.floor(Date.now() / 1000); if (now > parseInt(expires)) { return { valid: false, error: 'Token expired' }; } // Verify signature const data = `${email}|${expires}`; const expected = crypto .createHmac('sha256', TOKEN_SECRET) .update(data) .digest('hex'); if (signature !== expected) { return { valid: false, error: 'Invalid signature' }; } return { valid: true, email }; }; // Trigger Email Rules Synchronization const triggerSync = () => { const syncScriptPath = path.join(__dirname, '../sync/sync.js'); console.log('🔄 Triggering email rules synchronization...'); const syncProcess = spawn('sudo', ['node', syncScriptPath], { detached: true, stdio: 'ignore' }); syncProcess.unref(); // Allow the parent to exit independently console.log('✅ Sync triggered (running in background)'); }; // POST /api/auth/validate-token - Validate authentication token from Roundcube app.post('/api/auth/validate-token', [ body('email').isEmail().withMessage('Valid email is required'), body('expires').isNumeric().withMessage('Expires must be a number'), body('signature').notEmpty().withMessage('Signature is required'), ], handleValidationErrors, (req, res) => { try { const { email, expires, signature } = req.body; const result = validateToken(email, expires, signature); if (!result.valid) { return res.status(401).json({ error: 'Invalid token', message: result.error, }); } res.json({ success: true, email: result.email, }); } catch (error) { console.error('Token validation error:', error); res.status(500).json({ error: 'Token validation failed', message: error.message, }); } }); // Health Check app.get('/health', (req, res) => { res.json({ status: 'OK', timestamp: new Date().toISOString() }); }); // GET /api/rules - Get all rules app.get('/api/rules', async (req, res) => { try { const command = new ScanCommand({ TableName: TABLE_NAME, }); const response = await docClient.send(command); res.json({ rules: response.Items || [], count: response.Count || 0, }); } catch (error) { console.error('Error fetching rules:', error); res.status(500).json({ error: 'Failed to fetch rules', message: error.message, }); } }); // GET /api/rules/:email - Get rule for specific email app.get('/api/rules/:email', validateEmail, handleValidationErrors, async (req, res) => { try { const email = decodeURIComponent(req.params.email); const command = new GetCommand({ TableName: TABLE_NAME, Key: { email_address: email, }, }); const response = await docClient.send(command); if (!response.Item) { return res.status(404).json({ error: 'Rule not found', message: `No rule exists for email: ${email}`, }); } res.json(response.Item); } catch (error) { console.error('Error fetching rule:', error); res.status(500).json({ error: 'Failed to fetch rule', message: error.message, }); } }); // POST /api/rules - Create or update rule app.post('/api/rules', validateEmailRule, handleValidationErrors, async (req, res) => { try { const { email_address, ooo_active, ooo_message, ooo_content_type, forwards } = req.body; const item = { email_address, ooo_active: ooo_active || false, ooo_message: ooo_message || '', ooo_content_type: ooo_content_type || 'text', forwards: forwards || [], last_updated: new Date().toISOString(), }; const command = new PutCommand({ TableName: TABLE_NAME, Item: item, }); await docClient.send(command); // Trigger immediate synchronization triggerSync(); res.status(201).json({ message: 'Rule created/updated successfully', rule: item, }); } catch (error) { console.error('Error creating/updating rule:', error); res.status(500).json({ error: 'Failed to create/update rule', message: error.message, }); } }); // PUT /api/rules/:email - Update existing rule app.put('/api/rules/:email', validateEmail, validateEmailRule, handleValidationErrors, async (req, res) => { try { const email = decodeURIComponent(req.params.email); const { ooo_active, ooo_message, ooo_content_type, forwards } = req.body; // First check if rule exists const getCommand = new GetCommand({ TableName: TABLE_NAME, Key: { email_address: email }, }); const existingRule = await docClient.send(getCommand); if (!existingRule.Item) { return res.status(404).json({ error: 'Rule not found', message: `No rule exists for email: ${email}`, }); } // Merge with existing data const item = { ...existingRule.Item, ooo_active: ooo_active !== undefined ? ooo_active : existingRule.Item.ooo_active, ooo_message: ooo_message !== undefined ? ooo_message : existingRule.Item.ooo_message, ooo_content_type: ooo_content_type !== undefined ? ooo_content_type : existingRule.Item.ooo_content_type, forwards: forwards !== undefined ? forwards : existingRule.Item.forwards, last_updated: new Date().toISOString(), }; const putCommand = new PutCommand({ TableName: TABLE_NAME, Item: item, }); await docClient.send(putCommand); // Trigger immediate synchronization triggerSync(); res.json({ message: 'Rule updated successfully', rule: item, }); } catch (error) { console.error('Error updating rule:', error); res.status(500).json({ error: 'Failed to update rule', message: error.message, }); } }); // DELETE /api/rules/:email - Delete rule app.delete('/api/rules/:email', validateEmail, handleValidationErrors, async (req, res) => { try { const email = decodeURIComponent(req.params.email); // First check if rule exists const getCommand = new GetCommand({ TableName: TABLE_NAME, Key: { email_address: email }, }); const existingRule = await docClient.send(getCommand); if (!existingRule.Item) { return res.status(404).json({ error: 'Rule not found', message: `No rule exists for email: ${email}`, }); } const deleteCommand = new DeleteCommand({ TableName: TABLE_NAME, Key: { email_address: email, }, }); await docClient.send(deleteCommand); // Trigger immediate synchronization triggerSync(); res.json({ message: 'Rule deleted successfully', email_address: email, }); } catch (error) { console.error('Error deleting rule:', error); res.status(500).json({ error: 'Failed to delete rule', message: error.message, }); } }); // 404 Handler app.use((req, res) => { res.status(404).json({ error: 'Not Found', message: `Cannot ${req.method} ${req.path}`, }); }); // Global Error Handler app.use((err, req, res, next) => { console.error('Unhandled error:', err); res.status(500).json({ error: 'Internal Server Error', message: err.message, }); }); // Start Server app.listen(PORT, () => { console.log(`🚀 Email Config API running on port ${PORT}`); console.log(`📊 DynamoDB Table: ${TABLE_NAME}`); console.log(`🌍 Region: ${process.env.AWS_REGION || 'us-east-2'}`); });