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:
NW
2026-07-09 16:40:15 +01:00
parent 83991f098b
commit d03c8419e5
8 changed files with 421 additions and 47 deletions

View File

@@ -5,7 +5,7 @@ import { dirname, join } from 'path';
import ejsLayouts from 'express-ejs-layouts';
import logger from '../utils/logger.js';
import { requireAuth, handleLogin, handleLogout } from './auth.js';
import { csrfMiddleware, validateCsrf, validateCsrfFromBody } from './csrf.js';
import { csrfMiddleware } from './csrf.js';
import { asyncHandler, globalErrorHandler } from './errorHandler.js';
import dashboardRouter from './routes/dashboard.js';
import catalogRouter from './routes/catalog.js';
@@ -48,13 +48,9 @@ app.get('/login', (req, res) => {
app.post('/login', handleLogin);
app.get('/logout', handleLogout);
// CSRF token for all authenticated views
app.use(csrfMiddleware);
app.use(requireAuth);
// Apply CSRF validation to all state-changing routes
app.use(validateCsrf);
// Protect uploads behind auth with MIME hardening
app.use('/uploads', express.static(join(__dirname, '..', '..', 'uploads'), {
setHeaders: (res, path) => {