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' });
|
||||
}
|
||||
Reference in New Issue
Block a user