initial
This commit is contained in:
11
backend/.env.example
Normal file
11
backend/.env.example
Normal file
@@ -0,0 +1,11 @@
|
||||
# Server Configuration
|
||||
PORT=3001
|
||||
NODE_ENV=development
|
||||
|
||||
# AWS Configuration
|
||||
AWS_REGION=us-east-2
|
||||
AWS_ACCESS_KEY_ID=your_access_key_here
|
||||
AWS_SECRET_ACCESS_KEY=your_secret_key_here
|
||||
|
||||
# DynamoDB Configuration
|
||||
DYNAMODB_TABLE=email-rules
|
||||
4
backend/.gitignore
vendored
Normal file
4
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
.env
|
||||
*.log
|
||||
.DS_Store
|
||||
2728
backend/package-lock.json
generated
Normal file
2728
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
backend/package.json
Normal file
27
backend/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "email-config-api",
|
||||
"version": "1.0.0",
|
||||
"description": "REST API for Email Configuration Management",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": ["email", "api", "dynamodb", "aws"],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"@aws-sdk/client-dynamodb": "^3.470.0",
|
||||
"@aws-sdk/lib-dynamodb": "^3.470.0",
|
||||
"express-validator": "^7.0.1",
|
||||
"helmet": "^7.1.0",
|
||||
"morgan": "^1.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2"
|
||||
}
|
||||
}
|
||||
345
backend/server.js
Normal file
345
backend/server.js
Normal file
@@ -0,0 +1,345 @@
|
||||
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());
|
||||
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'}`);
|
||||
});
|
||||
Reference in New Issue
Block a user