Files
dotcom-arcade/backend/src/middleware/maintenanceMode.ts
T

106 lines
4.0 KiB
TypeScript

import { Request, Response, NextFunction } from 'express';
import { CachedSystemSettingsService } from '../services/CachedSystemSettingsService';
import { logger } from '../utils/logger';
// Singleton instance - shared across all requests
const systemSettings = new CachedSystemSettingsService();
// MINIMAL whitelist - only endpoints needed for admin to LOGIN
// Everything else requires admin authentication
const MINIMAL_PUBLIC_PATHS = [
'/health', // Health checks for monitoring
'/api/auth/login', // Admin needs to login
'/api/auth/register', // Initial admin setup (first user)
'/api/auth/setup-status', // Check if system needs initial setup
'/api/auth/refresh', // Token refresh
'/api/settings/public', // UI needs public settings
'/proxy', // Game images, logos, screenshots, and game files
'/api/playlists/shared', // Anonymous shared playlist access (bypasses guest access settings)
];
/**
* Maintenance mode middleware - SIMPLIFIED VERSION
*
* Prerequisites:
* - softAuth middleware MUST be applied globally before this middleware
* - softAuth populates req.user with user data if valid token exists
*
* When maintenance mode is ON:
* - Admin users (with 'settings.update' permission) can access EVERYTHING
* - Non-admin users are blocked from EVERYTHING except minimal public paths
* - Guests are blocked from EVERYTHING except minimal public paths
*
* Strategy:
* 1. Allow minimal public paths (login, health, etc.)
* 2. Check if maintenance mode enabled
* 3. If enabled: only allow admins through, block everyone else
* 4. If disabled: allow everyone (normal operation)
*/
export async function maintenanceMode(
req: Request,
res: Response,
next: NextFunction
): Promise<void | Response> {
try {
// Step 1: Check if path is in minimal public whitelist
// These paths must work for admin to LOGIN during maintenance
const isPublicPath = MINIMAL_PUBLIC_PATHS.some(
(path) => req.path === path || req.path.startsWith(path + '/')
);
if (isPublicPath) {
return next(); // Allow public paths for everyone
}
// Step 2: Check if maintenance mode is enabled
const appSettings = systemSettings.getCategory('app');
const isMaintenanceEnabled = appSettings.maintenanceMode === true;
if (!isMaintenanceEnabled) {
return next(); // Normal operation - allow everyone
}
// Step 3: Maintenance mode is ON - check if user is admin
// req.user is populated by softAuth middleware (applied globally before this)
const user = req.user;
const isAdmin = user?.permissions?.includes('settings.update');
// Debug logging to help diagnose permission issues
if (user) {
logger.debug(
`[Maintenance] User ${user.username} (role: ${user.role}) - permissions: ${user.permissions?.join(', ') || 'none'}`
);
} else {
logger.debug('[Maintenance] No user found in request (unauthenticated)');
}
if (isAdmin) {
// Admin user - ALLOW EVERYTHING (all endpoints, all features)
// user is guaranteed to exist if isAdmin is true
logger.info(`[Maintenance] Admin ${user!.username} accessing ${req.method} ${req.path}`);
return next();
}
// Step 4: Non-admin user or guest - BLOCK EVERYTHING
const userInfo = user ? `user ${user.username}` : 'unauthenticated user';
logger.warn(`[Maintenance] Blocked ${req.method} ${req.path} from ${userInfo} (IP: ${req.ip})`);
return res.status(503).set('Retry-After', '3600').json({
error: 'Service Unavailable',
message: 'The application is currently undergoing maintenance. Please try again later.',
maintenanceMode: true,
retryAfter: 3600,
});
} catch (error) {
logger.error('[Maintenance] Critical error in maintenance middleware:', error);
// FAIL CLOSED - Block requests on error (secure by default)
// This is the opposite of the previous behavior
return res.status(503).json({
error: 'Service Unavailable',
message: 'System maintenance in progress',
maintenanceMode: true,
});
}
}