diff --git a/server/routers/external.ts b/server/routers/external.ts index 8a22393ca7..3151602aa9 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -1338,6 +1338,12 @@ authenticated.get("/ws/round-trip-message/:messageId", checkRoundTripMessage); // Auth routes export const authRouter = Router(); unauthenticated.use("/auth", authRouter); + +// Register setup-check BEFORE the global auth rate limiter. +// This endpoint is called on every dashboard root page load (pure boolean +// read, no secrets) and must not consume the auth rate-limit budget. +authRouter.get("/initial-setup-complete", auth.initialSetupComplete); + authRouter.use( rateLimit({ windowMs: @@ -1642,7 +1648,6 @@ authRouter.post("/idp/:idpId/oidc/generate-url", idp.generateOidcUrl); authRouter.post("/idp/:idpId/oidc/validate-callback", idp.validateOidcCallback); authRouter.put("/set-server-admin", auth.setServerAdmin); -authRouter.get("/initial-setup-complete", auth.initialSetupComplete); authRouter.post("/validate-setup-token", auth.validateSetupToken); // Security Key routes diff --git a/src/app/page.tsx b/src/app/page.tsx index 7f0f05b570..c07475eb50 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -30,14 +30,19 @@ export default async function Page(props: { const getUser = cache(verifySession); const user = await getUser({ skipCheckVerifyEmail: true }); - let complete = false; + let complete: boolean | null = null; // null means "unknown" (request errored) try { const setupRes = await internal.get< AxiosResponse >(`/auth/initial-setup-complete`, await authCookieHeader()); complete = setupRes.data.data.complete; - } catch (e) {} - if (!complete) { + } catch (e) { + // Swallow errors (e.g. 429 rate limit, 500, network failure). + // Only redirect to initial-setup when the server *confirms* setup + // is incomplete (complete === false). If the request itself failed we + // cannot tell, so fall through to the login redirect instead. + } + if (complete === false) { redirect("/auth/initial-setup"); }