Merge pull request #618 from Luca-Timo/fix/maintenance-locks-out-admin-login

Enabling maintenance mode locks every admin out of the panel
This commit is contained in:
Paul Nothaft
2026-06-14 00:06:06 +02:00
committed by GitHub
2 changed files with 21 additions and 37 deletions
+8 -3
View File
@@ -64,10 +64,15 @@ async function checkMaintenanceMode() {
// Middleware to enforce maintenance mode // Middleware to enforce maintenance mode
async function maintenanceMiddleware(req, res, next) { async function maintenanceMiddleware(req, res, next) {
// Skip maintenance check for certain paths // Skip maintenance check for certain paths. Admin auth MUST work during
// maintenance — otherwise enabling it locks every admin out, including
// already-logged-in ones (their /auth/session check would 503 and read as
// logged-out). These are the REAL endpoints: the admin login + session
// routes live under /api/auth, NOT /api/admin (the old /api/admin/login
// entries here matched nothing, which is exactly why the lockout happened).
const skipPaths = [ const skipPaths = [
'/api/admin/login', '/api/auth/admin/login',
'/api/admin/auth/login', '/api/auth/session',
'/api/public/settings', '/api/public/settings',
'/health' '/health'
]; ];
+13 -34
View File
@@ -1,60 +1,39 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { MaintenanceMode } from './MaintenanceMode'; import { MaintenanceMode } from './MaintenanceMode';
import { useMaintenanceMode } from '../contexts/MaintenanceContext'; import { useMaintenanceMode } from '../contexts/MaintenanceContext';
import { setMaintenanceModeCallback, api } from '../config/api'; import { setMaintenanceModeCallback } from '../config/api';
interface MaintenanceWrapperProps { interface MaintenanceWrapperProps {
children: React.ReactNode; children: React.ReactNode;
} }
// Maintenance detection now lives in two places: // Maintenance detection lives in two places:
// 1. The axios interceptor in config/api.ts flips the flag on any 503 response. // 1. The axios interceptor in config/api.ts flips the flag on any 503 response.
// 2. MaintenanceContext polls /public/settings every 30s and reads the explicit // 2. MaintenanceContext polls /public/settings every 30s and reads the explicit
// maintenance_mode field (via the shared usePublicSettings hook). // maintenance_mode field (via the shared usePublicSettings hook).
// This wrapper only needs to gate the rendered tree on the resulting state. //
// The maintenance screen ONLY blocks customer/gallery/public routes. Admin
// routes (/admin/*) are never blocked: an admin must always be able to reach
// the panel to turn maintenance back off, and the admin auth layer already
// handles access (AdminLayout redirects a logged-out admin to /admin/login).
// Gating /admin/* here on an "is the admin logged in?" check is what caused the
// lockout — it hid the login page itself, and after login the check went stale
// (login → dashboard is a client-side nav within /admin, so it never re-ran),
// leaving a logged-in admin stuck on the maintenance screen.
export const MaintenanceWrapper: React.FC<MaintenanceWrapperProps> = ({ children }) => { export const MaintenanceWrapper: React.FC<MaintenanceWrapperProps> = ({ children }) => {
const location = useLocation(); const location = useLocation();
const { isMaintenanceMode, setMaintenanceMode } = useMaintenanceMode(); const { isMaintenanceMode, setMaintenanceMode } = useMaintenanceMode();
const [hasAdminSession, setHasAdminSession] = useState(false);
const isAdminRoute = location.pathname.startsWith('/admin'); const isAdminRoute = location.pathname.startsWith('/admin');
useEffect(() => {
let isMounted = true;
const checkAdminSession = async () => {
if (!isAdminRoute) {
setHasAdminSession(false);
return;
}
try {
const response = await api.get<{ valid: boolean; type: string }>('/auth/session');
if (isMounted) {
setHasAdminSession(Boolean(response.data?.valid && response.data.type === 'admin'));
}
} catch {
if (isMounted) {
setHasAdminSession(false);
}
}
};
checkAdminSession();
return () => {
isMounted = false;
};
}, [isAdminRoute]);
useEffect(() => { useEffect(() => {
setMaintenanceModeCallback((enabled: boolean) => { setMaintenanceModeCallback((enabled: boolean) => {
setMaintenanceMode(enabled); setMaintenanceMode(enabled);
}); });
}, [setMaintenanceMode]); }, [setMaintenanceMode]);
if (isMaintenanceMode && (!isAdminRoute || !hasAdminSession)) { if (isMaintenanceMode && !isAdminRoute) {
return <MaintenanceMode />; return <MaintenanceMode />;
} }