Initial commit

This commit is contained in:
Michilis
2025-12-19 23:56:07 -03:00
commit 23f716255e
48 changed files with 14834 additions and 0 deletions

View 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();
};
}

View 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 })
});
}

View 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();
}