From 8fdb67f1d3acf6403a8f2363a6db4880957ff9ef Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Thu, 6 Aug 2026 10:39:05 -0300 Subject: [PATCH] fix(auth): redirect active sessions from /login (#9491) Validated in local merge-train (diegosouzapw batch) --- .../9491-port-3005-auth-redirect-login.md | 1 + src/app/api/settings/require-login/route.ts | 23 +++++++++++ src/app/login/page.tsx | 2 +- tests/unit/auth-redirect-login.test.ts | 41 +++++++++++++++++++ 4 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 changelog.d/fixes/9491-port-3005-auth-redirect-login.md create mode 100644 tests/unit/auth-redirect-login.test.ts diff --git a/changelog.d/fixes/9491-port-3005-auth-redirect-login.md b/changelog.d/fixes/9491-port-3005-auth-redirect-login.md new file mode 100644 index 0000000000..8c82bc1b43 --- /dev/null +++ b/changelog.d/fixes/9491-port-3005-auth-redirect-login.md @@ -0,0 +1 @@ +- **fix(auth):** redirect active sessions from /login by checking the session cookie before showing the login form. (thanks @DaDecky) diff --git a/src/app/api/settings/require-login/route.ts b/src/app/api/settings/require-login/route.ts index 88fb170cce..8b1f9e8e63 100644 --- a/src/app/api/settings/require-login/route.ts +++ b/src/app/api/settings/require-login/route.ts @@ -1,4 +1,6 @@ import { NextResponse } from "next/server"; +import { cookies } from "next/headers"; +import { jwtVerify } from "jose"; import { getSettings, updateSettings } from "@/lib/localDb"; import { hasManagementPasswordConfigured, @@ -9,6 +11,24 @@ import { getNodeRuntimeSupport } from "@/shared/utils/nodeRuntimeSupport.ts"; import { updateRequireLoginSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; +function getJwtSecret(): Uint8Array | null { + const secret = process.env.JWT_SECRET?.trim(); + return secret ? new TextEncoder().encode(secret) : null; +} + +async function checkSessionAuthenticated(): Promise { + try { + const cookieStore = await cookies(); + const token = cookieStore.get("auth_token")?.value; + const secret = getJwtSecret(); + if (!token || !secret) return false; + await jwtVerify(token, secret); + return true; + } catch { + return false; + } +} + // Node.js compatibility check — reflect the supported secure runtime floors used by CLI/CI. function getNodeCompatibility() { const { nodeVersion, nodeCompatible } = getNodeRuntimeSupport(); @@ -28,10 +48,12 @@ export async function GET() { try { const settings = await getSettings(); const requireLogin = settings.requireLogin !== false; + const authenticated = await checkSessionAuthenticated(); const hasPassword = hasManagementPasswordConfigured(settings); const setupComplete = !!settings.setupComplete; const oidcEnabled = !!settings.oidcEnabled; return NextResponse.json({ + authenticated, requireLogin, hasPassword, setupComplete, @@ -42,6 +64,7 @@ export async function GET() { console.error("[API] Error fetching require-login settings:", error); return NextResponse.json( { + authenticated: false, requireLogin: true, hasPassword: true, setupComplete: true, diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 7019216ed6..057e6cad86 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -36,7 +36,7 @@ export default function LoginPage() { const data = await res.json(); if (data.nodeVersion) setNodeVersion(data.nodeVersion); if (data.nodeCompatible === false) setNodeCompatible(false); - if (data.requireLogin === false) { + if (data.authenticated === true || data.requireLogin === false) { router.push("/dashboard"); router.refresh(); return; diff --git a/tests/unit/auth-redirect-login.test.ts b/tests/unit/auth-redirect-login.test.ts new file mode 100644 index 0000000000..5dac024e67 --- /dev/null +++ b/tests/unit/auth-redirect-login.test.ts @@ -0,0 +1,41 @@ +/** + * Auth redirect: active sessions are redirected from /login to /dashboard. + * + * Upstream: decolua/9router#3005 — fix(auth): redirect active sessions from /login + * When a user navigates to /login while already authenticated, the login page + * fetches /api/settings/require-login and redirects to /dashboard. + */ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; + +describe("auth redirect login (port from 9router#3005)", () => { + it("login page checks data.authenticated === true before showing the form", () => { + const source = fs.readFileSync(path.resolve("src/app/login/page.tsx"), "utf-8"); + // The redirect guard must check both: + // - authenticated=true (active session → redirect to dashboard) + // - requireLogin=false (no auth configured → allow access) + const redirectCheck = source.match(/if\s*\(.*authenticated.*requireLogin.*\)/); + assert.ok(redirectCheck, "login page must check both authenticated and requireLogin"); + assert.ok( + source.includes("data.authenticated === true"), + "login page must check data.authenticated === true for redirect", + ); + }); + + it("require-login API route returns authenticated field", () => { + const source = fs.readFileSync( + path.resolve("src/app/api/settings/require-login/route.ts"), + "utf-8", + ); + assert.ok( + source.includes("authenticated:"), + "require-login route must include authenticated in the response", + ); + assert.ok( + source.includes("authenticated,"), + "authenticated must be part of the JSON response object (spread or key)", + ); + }); +});