Initial commit
This commit is contained in:
97
src/middleware/adminAuth.js
Normal file
97
src/middleware/adminAuth.js
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Admin Authentication Middleware
|
||||
*
|
||||
* Validates ADMIN_API_KEY header for admin endpoints.
|
||||
* All admin requests are logged.
|
||||
*/
|
||||
|
||||
import { config } from '../config.js';
|
||||
import { logAdminAction } from '../services/AdminService.js';
|
||||
|
||||
/**
|
||||
* Validate admin API key
|
||||
*/
|
||||
export function adminAuth(req, res, next) {
|
||||
// Get API key from header
|
||||
let apiKey = req.headers['x-admin-api-key'];
|
||||
if (!apiKey && req.headers['authorization']) {
|
||||
apiKey = req.headers['authorization'].replace('Bearer ', '');
|
||||
}
|
||||
|
||||
if (!config.adminApiKey) {
|
||||
console.error('[AdminAuth] ADMIN_API_KEY not configured');
|
||||
return res.status(503).json({
|
||||
error: 'Admin API not configured',
|
||||
message: 'Set ADMIN_API_KEY environment variable'
|
||||
});
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
return res.status(401).json({
|
||||
error: 'Unauthorized',
|
||||
message: 'Missing X-Admin-Api-Key header'
|
||||
});
|
||||
}
|
||||
|
||||
if (apiKey !== config.adminApiKey) {
|
||||
// Log failed auth attempt
|
||||
console.warn(`[AdminAuth] Invalid API key attempt from ${req.ip}`);
|
||||
return res.status(403).json({
|
||||
error: 'Forbidden',
|
||||
message: 'Invalid admin API key'
|
||||
});
|
||||
}
|
||||
|
||||
// Attach admin context to request
|
||||
req.admin = {
|
||||
id: 'admin', // Could be extended for multi-admin support
|
||||
ip: req.ip || req.headers['x-forwarded-for'] || 'unknown',
|
||||
userAgent: req.headers['user-agent'] || 'unknown'
|
||||
};
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Audit logging middleware - runs after admin routes
|
||||
*/
|
||||
export function auditLog(action, targetType) {
|
||||
return (req, res, next) => {
|
||||
// Store original json method
|
||||
const originalJson = res.json.bind(res);
|
||||
|
||||
// Override json to capture response
|
||||
res.json = (data) => {
|
||||
// Log the admin action
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
try {
|
||||
const adminId = (req.admin && req.admin.id) ? req.admin.id : 'unknown';
|
||||
const targetId = req.params.mint_id ||
|
||||
(req.body && req.body.mint_id) ||
|
||||
(req.body && req.body.source_mint_id);
|
||||
const notes = (req.body && req.body.notes) || (req.body && req.body.reason);
|
||||
const ipAddress = req.admin && req.admin.ip;
|
||||
const userAgent = req.admin && req.admin.userAgent;
|
||||
|
||||
logAdminAction({
|
||||
adminId,
|
||||
action,
|
||||
targetType,
|
||||
targetId,
|
||||
beforeState: req.beforeState,
|
||||
afterState: data,
|
||||
notes,
|
||||
ipAddress,
|
||||
userAgent
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[AuditLog] Failed to log action:', err);
|
||||
}
|
||||
}
|
||||
|
||||
return originalJson(data);
|
||||
};
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
43
src/middleware/errorHandler.js
Normal file
43
src/middleware/errorHandler.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Error Handling Middleware
|
||||
*/
|
||||
|
||||
import { config } from '../config.js';
|
||||
|
||||
/**
|
||||
* 404 Not Found handler
|
||||
*/
|
||||
export function notFound(req, res, next) {
|
||||
res.status(404).json({
|
||||
error: 'Not found',
|
||||
path: req.path,
|
||||
method: req.method
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Global error handler
|
||||
*/
|
||||
export function errorHandler(err, req, res, next) {
|
||||
console.error('[Error]', err);
|
||||
|
||||
// Handle specific error types
|
||||
if (err.type === 'entity.parse.failed') {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid JSON',
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
|
||||
// Default error response
|
||||
const statusCode = err.statusCode || err.status || 500;
|
||||
const message = config.nodeEnv === 'production'
|
||||
? 'Internal server error'
|
||||
: err.message;
|
||||
|
||||
res.status(statusCode).json({
|
||||
error: message,
|
||||
...(config.nodeEnv !== 'production' && { stack: err.stack })
|
||||
});
|
||||
}
|
||||
|
||||
77
src/middleware/rateLimit.js
Normal file
77
src/middleware/rateLimit.js
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Rate Limiting Middleware
|
||||
*
|
||||
* Simple in-memory rate limiter.
|
||||
* For production, consider using Redis-backed solution.
|
||||
*/
|
||||
|
||||
import { config } from '../config.js';
|
||||
|
||||
// In-memory store for request counts
|
||||
const requestCounts = new Map();
|
||||
|
||||
// Cleanup old entries periodically
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [key, data] of requestCounts) {
|
||||
if (now - data.windowStart > config.rateLimitWindowMs * 2) {
|
||||
requestCounts.delete(key);
|
||||
}
|
||||
}
|
||||
}, 60000); // Every minute
|
||||
|
||||
/**
|
||||
* Get client identifier from request
|
||||
*/
|
||||
function getClientId(req) {
|
||||
// Use X-Forwarded-For header if behind proxy
|
||||
const forwarded = req.headers['x-forwarded-for'];
|
||||
if (forwarded) {
|
||||
return forwarded.split(',')[0].trim();
|
||||
}
|
||||
return req.ip || (req.connection ? req.connection.remoteAddress : null) || 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limiting middleware
|
||||
*/
|
||||
export function rateLimit(req, res, next) {
|
||||
const clientId = getClientId(req);
|
||||
const now = Date.now();
|
||||
|
||||
let data = requestCounts.get(clientId);
|
||||
|
||||
if (!data || now - data.windowStart > config.rateLimitWindowMs) {
|
||||
// New window
|
||||
data = {
|
||||
windowStart: now,
|
||||
count: 1
|
||||
};
|
||||
requestCounts.set(clientId, data);
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
data.count++;
|
||||
|
||||
if (data.count > config.rateLimitMaxRequests) {
|
||||
const retryAfter = Math.ceil((data.windowStart + config.rateLimitWindowMs - now) / 1000);
|
||||
|
||||
res.set('Retry-After', retryAfter.toString());
|
||||
res.set('X-RateLimit-Limit', config.rateLimitMaxRequests.toString());
|
||||
res.set('X-RateLimit-Remaining', '0');
|
||||
res.set('X-RateLimit-Reset', new Date(data.windowStart + config.rateLimitWindowMs).toISOString());
|
||||
|
||||
return res.status(429).json({
|
||||
error: 'Too many requests',
|
||||
retry_after: retryAfter
|
||||
});
|
||||
}
|
||||
|
||||
// Add rate limit headers
|
||||
res.set('X-RateLimit-Limit', config.rateLimitMaxRequests.toString());
|
||||
res.set('X-RateLimit-Remaining', (config.rateLimitMaxRequests - data.count).toString());
|
||||
res.set('X-RateLimit-Reset', new Date(data.windowStart + config.rateLimitWindowMs).toISOString());
|
||||
|
||||
next();
|
||||
}
|
||||
Reference in New Issue
Block a user