feat(admin): super admin role, seed phrase viewer with QR code, CSRF disabled for Tor
- Add super admin role system (SUPER_ADMIN_SECRET env var) - requireSuperAuth middleware for sensitive routes - isSuperAdminWeb() helper for template access - Role badge in header (Super Admin / Admin) - Seed Viewer nav item visible only to super admins - Add seed phrase viewer with QR code generation - GET /wallets/seed/:walletId — JSON seed phrase (super admin only) - GET /wallets/seed-qr/:walletId — QR PNG image (super admin only) - Modal UI with reveal-on-click, 60s auto-hide countdown - Copy-to-clipboard and download QR as PNG - Audit logging for every seed phrase access - Disable CSRF completely for Tor/onion compatibility - csrfMiddleware no longer sets _csrf cookie - validateCsrf and validateCsrfFromBody are no-ops - res.locals.csrfToken set to empty string (prevents template errors) - .env.example: document SUPER_ADMIN_SECRET variable
This commit is contained in:
@@ -12,9 +12,10 @@ const TOKEN_SECRET = envSecret || configSecret;
|
||||
const COOKIE_NAME = 'admin_token';
|
||||
const MAX_AGE = 24 * 60 * 60 * 1000;
|
||||
|
||||
// Server-side session revocation (H7 fix)
|
||||
const revokedTokens = new Set(); // stores jti of revoked tokens
|
||||
let globalLogoutTimestamp = 0; // any token issued before this is invalid
|
||||
const SUPER_ADMIN_SECRET = process.env.SUPER_ADMIN_SECRET || undefined;
|
||||
|
||||
const revokedTokens = new Set();
|
||||
let globalLogoutTimestamp = 0;
|
||||
|
||||
// Rate limiting for login
|
||||
const loginAttempts = new Map();
|
||||
@@ -57,9 +58,40 @@ export function requireAuth(req, res, next) {
|
||||
return res.redirect('/login');
|
||||
}
|
||||
req.admin = payload;
|
||||
res.locals.adminRole = payload.role || 'admin';
|
||||
next();
|
||||
}
|
||||
|
||||
export function requireSuperAuth(req, res, next) {
|
||||
const token = req.cookies?.[COOKIE_NAME];
|
||||
if (!token) {
|
||||
if (req.xhr || req.headers.accept?.includes('application/json')) {
|
||||
return res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
return res.redirect('/login');
|
||||
}
|
||||
const payload = verifyToken(token);
|
||||
if (!payload) {
|
||||
res.clearCookie(COOKIE_NAME);
|
||||
if (req.xhr || req.headers.accept?.includes('application/json')) {
|
||||
return res.status(401).json({ error: 'Invalid or expired token' });
|
||||
}
|
||||
return res.redirect('/login');
|
||||
}
|
||||
if (payload.role !== 'super_admin') {
|
||||
if (req.xhr || req.headers.accept?.includes('application/json')) {
|
||||
return res.status(403).json({ error: 'Super admin access required' });
|
||||
}
|
||||
return res.status(403).send('Access denied. Super admin privileges required.');
|
||||
}
|
||||
req.admin = payload;
|
||||
next();
|
||||
}
|
||||
|
||||
export function isSuperAdminWeb(req) {
|
||||
return req.admin && req.admin.role === 'super_admin';
|
||||
}
|
||||
|
||||
export function handleLogin(req, res) {
|
||||
const clientIp = req.ip || req.connection.remoteAddress;
|
||||
const now = Date.now();
|
||||
@@ -75,9 +107,13 @@ export function handleLogin(req, res) {
|
||||
|
||||
const { token } = req.body || {};
|
||||
const provided = Buffer.from(token || '', 'utf8');
|
||||
const expected = Buffer.from(TOKEN_SECRET, 'utf8');
|
||||
const adminExpected = Buffer.from(TOKEN_SECRET, 'utf8');
|
||||
const superAdminExpected = SUPER_ADMIN_SECRET ? Buffer.from(SUPER_ADMIN_SECRET, 'utf8') : null;
|
||||
|
||||
if (provided.length !== expected.length || !crypto.timingSafeEqual(provided, expected)) {
|
||||
const isAdminLogin = provided.length === adminExpected.length && crypto.timingSafeEqual(provided, adminExpected);
|
||||
const isSuperAdminLogin = superAdminExpected && provided.length === superAdminExpected.length && crypto.timingSafeEqual(provided, superAdminExpected);
|
||||
|
||||
if (!isAdminLogin && !isSuperAdminLogin) {
|
||||
if (!record || (now - record.firstAttempt) >= WINDOW_MS) {
|
||||
loginAttempts.set(clientIp, { count: 1, firstAttempt: now });
|
||||
} else {
|
||||
@@ -92,7 +128,9 @@ export function handleLogin(req, res) {
|
||||
|
||||
loginAttempts.delete(clientIp);
|
||||
|
||||
const { token: signed, jti } = signToken({ role: 'admin' });
|
||||
const role = isSuperAdminLogin ? 'super_admin' : 'admin';
|
||||
|
||||
const { token: signed, jti } = signToken({ role });
|
||||
res.cookie(COOKIE_NAME, signed, {
|
||||
httpOnly: true,
|
||||
sameSite: false,
|
||||
|
||||
Reference in New Issue
Block a user