Phase 1-2 of server consolidation + security hardening
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import { extractBearerToken, verifyToken, TokenError } from './verify.js';
|
||||
|
||||
const USER_CACHE_TTL_MS = 60_000;
|
||||
|
||||
function createUserCache() {
|
||||
const entries = new Map();
|
||||
return {
|
||||
get(userId) {
|
||||
const hit = entries.get(userId);
|
||||
if (!hit) return null;
|
||||
if (Date.now() - hit.cachedAt > USER_CACHE_TTL_MS) {
|
||||
entries.delete(userId);
|
||||
return null;
|
||||
}
|
||||
return hit.user;
|
||||
},
|
||||
set(userId, user) {
|
||||
entries.set(userId, { user, cachedAt: Date.now() });
|
||||
},
|
||||
invalidate(userId) {
|
||||
entries.delete(userId);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function loadUser(pool, userId) {
|
||||
const userResult = await pool.query(
|
||||
'SELECT id, username, email, is_admin, is_active FROM users WHERE id = $1',
|
||||
[userId]
|
||||
);
|
||||
const user = userResult.rows[0];
|
||||
if (!user) return null;
|
||||
|
||||
if (user.is_admin) {
|
||||
user.permissions = [];
|
||||
return user;
|
||||
}
|
||||
|
||||
const permResult = await pool.query(
|
||||
`SELECT p.code
|
||||
FROM permissions p
|
||||
JOIN user_permissions up ON p.id = up.permission_id
|
||||
WHERE up.user_id = $1`,
|
||||
[userId]
|
||||
);
|
||||
user.permissions = permResult.rows.map((r) => r.code);
|
||||
return user;
|
||||
}
|
||||
|
||||
export function authenticate({ pool, secret = process.env.JWT_SECRET }) {
|
||||
const cache = createUserCache();
|
||||
|
||||
return async function authenticateMiddleware(req, res, next) {
|
||||
let decoded;
|
||||
try {
|
||||
const token = extractBearerToken(req.headers.authorization);
|
||||
decoded = verifyToken(token, secret);
|
||||
} catch (err) {
|
||||
if (err instanceof TokenError) {
|
||||
return res.status(401).json({ error: err.message });
|
||||
}
|
||||
return res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
|
||||
try {
|
||||
let user = cache.get(decoded.userId);
|
||||
if (!user) {
|
||||
user = await loadUser(pool, decoded.userId);
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'User not found' });
|
||||
}
|
||||
cache.set(decoded.userId, user);
|
||||
}
|
||||
if (!user.is_active) {
|
||||
cache.invalidate(decoded.userId);
|
||||
return res.status(403).json({ error: 'Account inactive' });
|
||||
}
|
||||
req.user = user;
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function requirePermission(code) {
|
||||
return function requirePermissionMiddleware(req, res, next) {
|
||||
if (!req.user) return res.status(401).json({ error: 'Authentication required' });
|
||||
if (req.user.is_admin) return next();
|
||||
if (Array.isArray(req.user.permissions) && req.user.permissions.includes(code)) {
|
||||
return next();
|
||||
}
|
||||
res.status(403).json({ error: 'Insufficient permissions' });
|
||||
};
|
||||
}
|
||||
|
||||
export function requireAdmin(req, res, next) {
|
||||
if (!req.user) return res.status(401).json({ error: 'Authentication required' });
|
||||
if (req.user.is_admin) return next();
|
||||
res.status(403).json({ error: 'Admin only' });
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
export class TokenError extends Error {
|
||||
constructor(message, code) {
|
||||
super(message);
|
||||
this.name = 'TokenError';
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
export function extractBearerToken(authorizationHeader) {
|
||||
if (!authorizationHeader || typeof authorizationHeader !== 'string') {
|
||||
throw new TokenError('No token provided', 'missing');
|
||||
}
|
||||
if (!authorizationHeader.startsWith('Bearer ')) {
|
||||
throw new TokenError('Malformed Authorization header', 'malformed');
|
||||
}
|
||||
const token = authorizationHeader.slice(7).trim();
|
||||
if (!token) {
|
||||
throw new TokenError('Empty bearer token', 'malformed');
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
export function verifyToken(token, secret) {
|
||||
if (!secret) {
|
||||
throw new TokenError('JWT_SECRET not configured', 'misconfigured');
|
||||
}
|
||||
try {
|
||||
return jwt.verify(token, secret);
|
||||
} catch (err) {
|
||||
if (err.name === 'TokenExpiredError') {
|
||||
throw new TokenError('Token expired', 'expired');
|
||||
}
|
||||
throw new TokenError('Invalid token', 'invalid');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user