Initial
This commit is contained in:
1
server/middleware/auth.js
Normal file
1
server/middleware/auth.js
Normal file
@@ -0,0 +1 @@
|
||||
export { requireAdmin, requireAuth } from '../auth.js';
|
||||
37
server/middleware/rateLimit.js
Normal file
37
server/middleware/rateLimit.js
Normal file
@@ -0,0 +1,37 @@
|
||||
// Lightweight in-memory rate limiter for sensitive endpoints such as login.
|
||||
// Keeps POSHManager dependency-free for a single-process deployment. For a
|
||||
// multi-replica deployment behind a load balancer, replace the Map with a
|
||||
// shared store (Redis) so limits are enforced across instances.
|
||||
|
||||
const buckets = new Map();
|
||||
|
||||
export function rateLimit({ windowMs = 60_000, max = 10, keyPrefix = 'rl' } = {}) {
|
||||
return function rateLimiter(req, res, next) {
|
||||
const ip = req.ip || req.socket?.remoteAddress || 'unknown';
|
||||
const key = `${keyPrefix}:${ip}`;
|
||||
const nowTs = Date.now();
|
||||
const entry = buckets.get(key);
|
||||
|
||||
if (!entry || nowTs > entry.resetAt) {
|
||||
buckets.set(key, { count: 1, resetAt: nowTs + windowMs });
|
||||
return next();
|
||||
}
|
||||
|
||||
entry.count += 1;
|
||||
if (entry.count > max) {
|
||||
const retryAfter = Math.max(1, Math.ceil((entry.resetAt - nowTs) / 1000));
|
||||
res.setHeader('Retry-After', String(retryAfter));
|
||||
return res.status(429).json({ error: 'Too many requests. Please slow down and try again shortly.' });
|
||||
}
|
||||
return next();
|
||||
};
|
||||
}
|
||||
|
||||
// Periodically drop expired buckets so the Map cannot grow unbounded.
|
||||
const sweep = setInterval(() => {
|
||||
const nowTs = Date.now();
|
||||
for (const [key, entry] of buckets) {
|
||||
if (nowTs > entry.resetAt) buckets.delete(key);
|
||||
}
|
||||
}, 5 * 60_000);
|
||||
sweep.unref?.();
|
||||
25
server/middleware/requestLogger.js
Normal file
25
server/middleware/requestLogger.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { logger } from '../services/logger.js';
|
||||
|
||||
export function requestLogger(req, res, next) {
|
||||
const start = Date.now();
|
||||
res.on('finish', () => {
|
||||
logger.info('request', {
|
||||
method: req.method,
|
||||
path: req.originalUrl,
|
||||
status: res.statusCode,
|
||||
durationMs: Date.now() - start,
|
||||
ip: req.ip,
|
||||
userId: req.user?.id || null,
|
||||
headers: redactHeaders(req.headers)
|
||||
});
|
||||
});
|
||||
next();
|
||||
}
|
||||
|
||||
function redactHeaders(headers) {
|
||||
const safe = { ...headers };
|
||||
for (const key of ['authorization', 'cookie', 'set-cookie']) {
|
||||
if (safe[key]) safe[key] = '[redacted]';
|
||||
}
|
||||
return safe;
|
||||
}
|
||||
32
server/middleware/upload.js
Normal file
32
server/middleware/upload.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import multer from 'multer';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { config } from '../config.js';
|
||||
|
||||
const incomingDir = path.join(config.fileLockerDir, '_incoming');
|
||||
fs.mkdirSync(incomingDir, { recursive: true });
|
||||
|
||||
function sanitizeFileName(name) {
|
||||
return String(name || 'asset.bin')
|
||||
.replace(/[/\\?%*:|"<>]/g, '-')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.slice(0, 180) || 'asset.bin';
|
||||
}
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination(req, file, callback) {
|
||||
fs.mkdirSync(incomingDir, { recursive: true });
|
||||
callback(null, incomingDir);
|
||||
},
|
||||
filename(req, file, callback) {
|
||||
callback(null, `${Date.now()}-${nanoid(8)}-${sanitizeFileName(file.originalname)}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Asset uploads are staged in FileLocker before the model promotes them into the typed library tree.
|
||||
export const assetUpload = multer({
|
||||
storage,
|
||||
limits: { fileSize: config.maxAssetBytes }
|
||||
});
|
||||
Reference in New Issue
Block a user