From 249313072b08ad79146b397bc08436e9de366d22 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Sat, 13 Jun 2026 13:57:21 +0200 Subject: [PATCH 1/2] fix(maintenance): enabling maintenance mode no longer locks admins out MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turning on maintenance mode locked out every admin — including ones already logged in — with no way back in from the browser. Two causes: 1. Backend (middleware/maintenance.js): the skipPaths allow-list pointed at /api/admin/login and /api/admin/auth/login, but the real admin auth routes live under /api/auth (POST /api/auth/admin/login, GET /api/auth/session). So during maintenance both the login POST and the session check 503'd. The 503 on /auth/session made the frontend read every admin as logged-out, and also tripped the axios interceptor that force-enables maintenance globally. Fixed the allow-list to the actual endpoints. 2. Frontend (MaintenanceWrapper.tsx): the maintenance screen rendered over every /admin/* route unless an admin session already existed — covering the /admin/login page itself. A logged-out admin could never reach the form to get a session (catch-22). /admin/login is now always allowed through. With both: a logged-in admin keeps working (session check passes), and a logged-out admin can reach /admin/login and sign back in, all while maintenance mode correctly blocks customers. --- backend/src/middleware/maintenance.js | 11 ++++++++--- frontend/src/components/MaintenanceWrapper.tsx | 8 +++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/backend/src/middleware/maintenance.js b/backend/src/middleware/maintenance.js index e3be0e0e..37ffb202 100644 --- a/backend/src/middleware/maintenance.js +++ b/backend/src/middleware/maintenance.js @@ -64,10 +64,15 @@ async function checkMaintenanceMode() { // Middleware to enforce maintenance mode 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 = [ - '/api/admin/login', - '/api/admin/auth/login', + '/api/auth/admin/login', + '/api/auth/session', '/api/public/settings', '/health' ]; diff --git a/frontend/src/components/MaintenanceWrapper.tsx b/frontend/src/components/MaintenanceWrapper.tsx index ccd3e159..f3606e4e 100644 --- a/frontend/src/components/MaintenanceWrapper.tsx +++ b/frontend/src/components/MaintenanceWrapper.tsx @@ -19,6 +19,12 @@ export const MaintenanceWrapper: React.FC = ({ children const [hasAdminSession, setHasAdminSession] = useState(false); const isAdminRoute = location.pathname.startsWith('/admin'); + // The admin login page must ALWAYS render during maintenance — it's how an + // admin gets a session to bypass it. Without this exemption a logged-out + // admin sees the maintenance screen over the login form (catch-22: needs a + // session to get past maintenance, but the login page that grants one is + // hidden). + const isAdminLoginRoute = location.pathname.startsWith('/admin/login'); useEffect(() => { let isMounted = true; @@ -54,7 +60,7 @@ export const MaintenanceWrapper: React.FC = ({ children }); }, [setMaintenanceMode]); - if (isMaintenanceMode && (!isAdminRoute || !hasAdminSession)) { + if (isMaintenanceMode && !isAdminLoginRoute && (!isAdminRoute || !hasAdminSession)) { return ; } From fdde4696e7025d7e41dd85eeda26b946f6713e13 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Sat, 13 Jun 2026 14:18:02 +0200 Subject: [PATCH 2/2] fix(maintenance): never block /admin/* with the maintenance screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous wrapper gated /admin/* on a /auth/session check and only showed the panel when an admin session was detected. Two failures: 1. The session check effect depended on `isAdminRoute` (a boolean), so the client-side login → dashboard navigation (both /admin/*) never re-ran it. hasAdminSession stayed stale-false from the logged-out /admin/login render, so a freshly logged-in admin landed on the maintenance screen anyway. 2. It also hid /admin/login itself (the catch-22). Fix: the maintenance screen only blocks customer/gallery/public routes — /admin/* is never blocked. The admin auth layer already handles access (AdminLayout redirects a logged-out admin to /admin/login), so no session probe is needed here. Removes the fragile /auth/session dependency entirely. Backend skipPaths (/api/auth/admin/login + /api/auth/session) stays: login and AdminAuthContext's token validation must still work during maintenance. --- .../src/components/MaintenanceWrapper.tsx | 53 +++++-------------- 1 file changed, 13 insertions(+), 40 deletions(-) diff --git a/frontend/src/components/MaintenanceWrapper.tsx b/frontend/src/components/MaintenanceWrapper.tsx index f3606e4e..ab9df689 100644 --- a/frontend/src/components/MaintenanceWrapper.tsx +++ b/frontend/src/components/MaintenanceWrapper.tsx @@ -1,58 +1,31 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import { MaintenanceMode } from './MaintenanceMode'; import { useMaintenanceMode } from '../contexts/MaintenanceContext'; -import { setMaintenanceModeCallback, api } from '../config/api'; +import { setMaintenanceModeCallback } from '../config/api'; interface MaintenanceWrapperProps { 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. // 2. MaintenanceContext polls /public/settings every 30s and reads the explicit // 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 = ({ children }) => { const location = useLocation(); const { isMaintenanceMode, setMaintenanceMode } = useMaintenanceMode(); - const [hasAdminSession, setHasAdminSession] = useState(false); const isAdminRoute = location.pathname.startsWith('/admin'); - // The admin login page must ALWAYS render during maintenance — it's how an - // admin gets a session to bypass it. Without this exemption a logged-out - // admin sees the maintenance screen over the login form (catch-22: needs a - // session to get past maintenance, but the login page that grants one is - // hidden). - const isAdminLoginRoute = location.pathname.startsWith('/admin/login'); - - 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(() => { setMaintenanceModeCallback((enabled: boolean) => { @@ -60,7 +33,7 @@ export const MaintenanceWrapper: React.FC = ({ children }); }, [setMaintenanceMode]); - if (isMaintenanceMode && !isAdminLoginRoute && (!isAdminRoute || !hasAdminSession)) { + if (isMaintenanceMode && !isAdminRoute) { return ; }