This commit is contained in:
2026-06-25 12:05:53 -05:00
commit b58e50e423
141 changed files with 28190 additions and 0 deletions

View File

@@ -0,0 +1 @@
export { requireAdmin, requireAuth } from '../auth.js';

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

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

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