feat: add Express server foundation with middleware

Configuration:
- app.config.js: Centralized configuration (ports, MongoDB, JWT, features)
- Feature flags for AI curation, media triage, case submissions

Middleware:
- auth.middleware.js: JWT authentication, role-based access control
- validation.middleware.js: Input validation, sanitization, ObjectId checks
- error.middleware.js: Global error handling, async wrapper, 404 handler

Express Server (src/server.js):
- Security: Helmet, CORS, rate limiting
- Request logging with Winston
- Health check endpoint
- MongoDB connection with graceful shutdown
- Static file serving
- Temporary homepage showing development status

Features:
- Production-ready error handling
- MongoDB duplicate key detection
- JWT token validation
- XSS protection via sanitization
- Rate limiting (100 req / 15min per IP)
- Graceful shutdown (SIGTERM/SIGINT)

Status: Server foundation complete, ready for API routes
Port: 9000
Database: tractatus_dev (MongoDB 27017)
This commit is contained in:
TheFlow 2025-10-06 23:56:12 +13:00
parent 78ab5754f2
commit 6285adc572
5 changed files with 628 additions and 0 deletions

52
src/config/app.config.js Normal file
View file

@ -0,0 +1,52 @@
/**
* Application Configuration
*/
module.exports = {
// Server
port: process.env.PORT || 9000,
env: process.env.NODE_ENV || 'development',
appName: process.env.APP_NAME || 'Tractatus',
// MongoDB
mongodb: {
uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/tractatus_dev',
db: process.env.MONGODB_DB || 'tractatus_dev'
},
// JWT
jwt: {
secret: process.env.JWT_SECRET || 'CHANGE_THIS_IN_PRODUCTION',
expiry: process.env.JWT_EXPIRY || '7d'
},
// Admin
admin: {
email: process.env.ADMIN_EMAIL || 'john.stroh.nz@pm.me'
},
// Logging
logging: {
level: process.env.LOG_LEVEL || 'info',
file: process.env.LOG_FILE || 'logs/app.log'
},
// Feature Flags
features: {
aiCuration: process.env.ENABLE_AI_CURATION === 'true',
mediaTriage: process.env.ENABLE_MEDIA_TRIAGE === 'true',
caseSubmissions: process.env.ENABLE_CASE_SUBMISSIONS === 'true'
},
// Security
security: {
rateLimitWindowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'), // 15 min
rateLimitMaxRequests: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100')
},
// CORS
cors: {
origin: process.env.CORS_ORIGIN || '*',
credentials: true
}
};

View file

@ -0,0 +1,109 @@
/**
* Authentication Middleware
* JWT-based authentication for admin routes
*/
const { verifyToken, extractTokenFromHeader } = require('../utils/jwt.util');
const { User } = require('../models');
const logger = require('../utils/logger.util');
/**
* Verify JWT token and attach user to request
*/
async function authenticateToken(req, res, next) {
try {
const token = extractTokenFromHeader(req.headers.authorization);
if (!token) {
return res.status(401).json({
error: 'Authentication required',
message: 'No token provided'
});
}
// Verify token
const decoded = verifyToken(token);
// Get user from database
const user = await User.findById(decoded.userId);
if (!user) {
return res.status(401).json({
error: 'Authentication failed',
message: 'User not found'
});
}
if (!user.active) {
return res.status(401).json({
error: 'Authentication failed',
message: 'User account is inactive'
});
}
// Attach user to request
req.user = user;
req.userId = user._id;
next();
} catch (error) {
logger.error('Authentication error:', error);
return res.status(401).json({
error: 'Authentication failed',
message: error.message
});
}
}
/**
* Check if user has required role
*/
function requireRole(...roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
error: 'Authentication required'
});
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({
error: 'Insufficient permissions',
message: `Required role: ${roles.join(' or ')}`
});
}
next();
};
}
/**
* Optional authentication (attach user if token present, continue if not)
*/
async function optionalAuth(req, res, next) {
try {
const token = extractTokenFromHeader(req.headers.authorization);
if (token) {
const decoded = verifyToken(token);
const user = await User.findById(decoded.userId);
if (user && user.active) {
req.user = user;
req.userId = user._id;
}
}
} catch (error) {
// Silently fail - authentication is optional
logger.debug('Optional auth failed:', error.message);
}
next();
}
module.exports = {
authenticateToken,
requireRole,
optionalAuth
};

View file

@ -0,0 +1,92 @@
/**
* Error Handling Middleware
*/
const logger = require('../utils/logger.util');
/**
* 404 Not Found handler
*/
function notFound(req, res, next) {
res.status(404).json({
error: 'Not Found',
message: `Route ${req.method} ${req.originalUrl} not found`
});
}
/**
* Global error handler
*/
function errorHandler(err, req, res, next) {
// Log error
logger.error('Error:', {
message: err.message,
stack: err.stack,
url: req.originalUrl,
method: req.method,
ip: req.ip
});
// Determine status code
const statusCode = err.statusCode || err.status || 500;
// MongoDB errors
if (err.name === 'MongoError' || err.name === 'MongoServerError') {
if (err.code === 11000) {
// Duplicate key error
return res.status(409).json({
error: 'Duplicate Entry',
message: 'A resource with this value already exists'
});
}
}
// Validation errors
if (err.name === 'ValidationError') {
return res.status(400).json({
error: 'Validation Error',
message: err.message,
details: err.errors
});
}
// JWT errors
if (err.name === 'JsonWebTokenError') {
return res.status(401).json({
error: 'Invalid Token',
message: 'Authentication token is invalid'
});
}
if (err.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'Token Expired',
message: 'Authentication token has expired'
});
}
// Generic error response
res.status(statusCode).json({
error: err.name || 'Internal Server Error',
message: process.env.NODE_ENV === 'production'
? 'An error occurred processing your request'
: err.message,
...(process.env.NODE_ENV !== 'production' && { stack: err.stack })
});
}
/**
* Async error wrapper
* Wraps async route handlers to catch errors
*/
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
module.exports = {
notFound,
errorHandler,
asyncHandler
};

View file

@ -0,0 +1,182 @@
/**
* Validation Middleware
* Input validation and sanitization
*/
const validator = require('validator');
const sanitizeHtml = require('sanitize-html');
/**
* Validate email
*/
function validateEmail(req, res, next) {
const { email } = req.body;
if (!email || !validator.isEmail(email)) {
return res.status(400).json({
error: 'Validation failed',
message: 'Valid email address is required'
});
}
// Normalize email
req.body.email = validator.normalizeEmail(email);
next();
}
/**
* Validate required fields
*/
function validateRequired(fields) {
return (req, res, next) => {
const missing = [];
for (const field of fields) {
if (!req.body[field] || req.body[field].trim() === '') {
missing.push(field);
}
}
if (missing.length > 0) {
return res.status(400).json({
error: 'Validation failed',
message: 'Required fields missing',
missing: missing
});
}
next();
};
}
/**
* Sanitize string inputs
*/
function sanitizeInputs(req, res, next) {
const sanitizeString = (str) => {
if (typeof str !== 'string') return str;
return validator.escape(str.trim());
};
const sanitizeObject = (obj) => {
const sanitized = {};
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'string') {
sanitized[key] = sanitizeString(value);
} else if (typeof value === 'object' && value !== null) {
sanitized[key] = sanitizeObject(value);
} else {
sanitized[key] = value;
}
}
return sanitized;
};
req.body = sanitizeObject(req.body);
next();
}
/**
* Validate MongoDB ObjectId
*/
function validateObjectId(paramName = 'id') {
return (req, res, next) => {
const id = req.params[paramName];
if (!id || !validator.isMongoId(id)) {
return res.status(400).json({
error: 'Validation failed',
message: 'Invalid ID format'
});
}
next();
};
}
/**
* Validate slug format
*/
function validateSlug(req, res, next) {
const { slug } = req.body;
if (!slug) {
return next();
}
const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
if (!slugRegex.test(slug)) {
return res.status(400).json({
error: 'Validation failed',
message: 'Slug must be lowercase letters, numbers, and hyphens only'
});
}
next();
}
/**
* Validate URL
*/
function validateUrl(fieldName = 'url') {
return (req, res, next) => {
const url = req.body[fieldName];
if (!url || !validator.isURL(url, { require_protocol: true })) {
return res.status(400).json({
error: 'Validation failed',
message: `Valid URL required for ${fieldName}`
});
}
next();
};
}
/**
* Sanitize HTML content
*/
function sanitizeContent(fieldName = 'content') {
return (req, res, next) => {
const content = req.body[fieldName];
if (!content) {
return next();
}
req.body[fieldName] = sanitizeHtml(content, {
allowedTags: [
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'p', 'br', 'hr',
'strong', 'em', 'u', 'code', 'pre',
'a', 'img',
'ul', 'ol', 'li',
'blockquote',
'table', 'thead', 'tbody', 'tr', 'th', 'td'
],
allowedAttributes: {
'a': ['href', 'title', 'target', 'rel'],
'img': ['src', 'alt', 'title'],
'code': ['class'],
'pre': ['class']
}
});
next();
};
}
module.exports = {
validateEmail,
validateRequired,
sanitizeInputs,
validateObjectId,
validateSlug,
validateUrl,
sanitizeContent
};

193
src/server.js Normal file
View file

@ -0,0 +1,193 @@
/**
* Tractatus Express Server
* Main application entry point
*/
require('dotenv').config();
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const config = require('./config/app.config');
const logger = require('./utils/logger.util');
const { connect: connectDb, close: closeDb } = require('./utils/db.util');
const { notFound, errorHandler } = require('./middleware/error.middleware');
// Create Express app
const app = express();
// Trust proxy (for rate limiting behind reverse proxy)
app.set('trust proxy', 1);
// Security middleware
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
}));
// CORS
app.use(cors(config.cors));
// Body parsers
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Request logging
app.use(logger.request);
// Rate limiting
const limiter = rateLimit({
windowMs: config.security.rateLimitWindowMs,
max: config.security.rateLimitMaxRequests,
message: 'Too many requests from this IP, please try again later',
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api/', limiter);
// Static files
app.use(express.static('public'));
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: config.env
});
});
// API routes (placeholder - will be implemented)
app.get('/api', (req, res) => {
res.json({
name: config.appName,
version: '1.0.0',
message: 'Tractatus AI Safety Framework API',
documentation: '/api/docs',
endpoints: {
documents: '/api/documents',
blog: '/api/blog',
media: '/api/media',
cases: '/api/cases',
resources: '/api/resources',
admin: '/api/admin'
}
});
});
// Homepage (temporary)
app.get('/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Tractatus AI Safety Framework</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
line-height: 1.6;
}
h1 { color: #2563eb; }
.status { color: #059669; font-weight: bold; }
code { background: #f3f4f6; padding: 2px 6px; border-radius: 3px; }
</style>
</head>
<body>
<h1>Tractatus AI Safety Framework</h1>
<p class="status"> Server Running</p>
<p>Development environment for the Tractatus-Based LLM Safety Framework website.</p>
<h2>Status</h2>
<ul>
<li> MongoDB connected (port 27017)</li>
<li> Express server running (port ${config.port})</li>
<li> Database initialized (10 collections)</li>
<li> Core models implemented</li>
<li> API routes (in development)</li>
<li> Frontend (pending)</li>
</ul>
<h2>Available Endpoints</h2>
<ul>
<li><code>GET /health</code> - Health check</li>
<li><code>GET /api</code> - API information</li>
</ul>
<p><em>Phase 1 Development - Not for public use</em></p>
</body>
</html>
`);
});
// 404 handler
app.use(notFound);
// Error handler
app.use(errorHandler);
// Server startup
async function start() {
try {
// Connect to MongoDB
await connectDb();
// Start server
const server = app.listen(config.port, () => {
logger.info(`🚀 Tractatus server started`);
logger.info(`✅ Environment: ${config.env}`);
logger.info(`✅ Port: ${config.port}`);
logger.info(`✅ MongoDB: ${config.mongodb.db}`);
logger.info(`✨ Ready for development`);
console.log(`\n🌐 http://localhost:${config.port}\n`);
});
// Graceful shutdown
process.on('SIGTERM', () => shutdown(server));
process.on('SIGINT', () => shutdown(server));
} catch (error) {
logger.error('Failed to start server:', error);
process.exit(1);
}
}
// Graceful shutdown
async function shutdown(server) {
logger.info('Shutting down gracefully...');
server.close(async () => {
logger.info('HTTP server closed');
await closeDb();
logger.info('Database connection closed');
process.exit(0);
});
// Force shutdown after 10 seconds
setTimeout(() => {
logger.error('Forced shutdown after timeout');
process.exit(1);
}, 10000);
}
// Start server if run directly
if (require.main === module) {
start();
}
module.exports = app;