From b3f4ace13f520e6060151a7ae4204d19662acbb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BE=9D=E7=91=AA=E8=B2=93?= Date: Sun, 8 Mar 2026 19:07:56 +0800 Subject: [PATCH] Enforce requiresAuth routes in global router guard with login return-to redirects Co-Authored-By: Codex --- src/router/index.ts | 49 +++++++++++++++++++++++++++----- tests/router/routerGuard.test.js | 41 ++++++++++++++++++++++++-- 2 files changed, 80 insertions(+), 10 deletions(-) diff --git a/src/router/index.ts b/src/router/index.ts index d20ce00..812055b 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -10,6 +10,8 @@ */ import { createRouter, createWebHistory } from "vue-router"; +import { useLoginStore } from "@/stores/login"; +import { getCookie, setCookie } from "@/utils/cookieUtil"; import AuthContainer from "@/views/AuthContainer.vue"; import MainContainer from "@/views/MainContainer.vue"; import Login from "@/views/Login/LoginPage.vue"; @@ -171,18 +173,51 @@ const router = createRouter({ routes, }); +/** + * Builds a login redirect target with a return-to query. + * @param {string} fullPath - The destination path to return after login. + * @returns {{path: string, query: {"return-to": string}}} The redirect target. + */ +function buildLoginRedirect(fullPath: string) { + return { + path: "/login", + query: { + "return-to": btoa(fullPath), + }, + }; +} + // Global navigation guard -router.beforeEach((to, from) => { +router.beforeEach(async (to) => { // to: Route: the target route object being navigated to - // from: Route: the current route being navigated away from + const hasLoginMarker = Boolean(getCookie("isLuciaLoggedIn")); + const hasAccessToken = Boolean(getCookie("luciaToken")); + const hasRefreshToken = Boolean(getCookie("luciaRefreshToken")); + const isAuthenticated = hasLoginMarker && hasAccessToken; // When navigating to the login page, redirect to Files if already logged in - if (to.name === "Login") { - const isLoggedIn = document.cookie - .split(";") - .some((c) => c.trim().startsWith("isLuciaLoggedIn=")); - if (isLoggedIn) return { name: "Files" }; + if (to.name === "Login" && isAuthenticated) { + return { name: "Files" }; } + + const requiresAuth = to.matched.some((routeRecord) => routeRecord.meta.requiresAuth); + if (!requiresAuth || isAuthenticated) { + return undefined; + } + + if (hasRefreshToken) { + const loginStore = useLoginStore(); + try { + await loginStore.refreshToken(); + loginStore.setIsLoggedIn(true); + setCookie("isLuciaLoggedIn", "true"); + return undefined; + } catch { + return buildLoginRedirect(to.fullPath); + } + } + + return buildLoginRedirect(to.fullPath); }); export default router; diff --git a/tests/router/routerGuard.test.js b/tests/router/routerGuard.test.js index 60ae2f1..b8eec26 100644 --- a/tests/router/routerGuard.test.js +++ b/tests/router/routerGuard.test.js @@ -18,18 +18,34 @@ describe("router beforeEach guard logic", () => { // Simulate the guard logic from router/index.ts function runGuard(to) { - const isLoggedIn = document.cookie + const hasLoginMarker = document.cookie .split(";") .some((c) => c.trim().startsWith("isLuciaLoggedIn=")); + const hasAccessToken = document.cookie + .split(";") + .some((c) => c.trim().startsWith("luciaToken=")); + const isAuthenticated = hasLoginMarker && hasAccessToken; if (to.name === "Login") { - if (isLoggedIn) return { name: "Files" }; + if (isAuthenticated) return { name: "Files" }; } + + const requiresAuth = (to.matched || []).some((r) => r.meta?.requiresAuth); + if (requiresAuth && !isAuthenticated) { + return { + path: "/login", + query: { + "return-to": btoa(to.fullPath || to.path || "/"), + }, + }; + } + return undefined; } it("redirects logged-in user from Login to Files", () => { document.cookie = "isLuciaLoggedIn=true"; + document.cookie = "luciaToken=token"; expect(runGuard({ name: "Login" })).toEqual({ name: "Files" }); }); @@ -37,8 +53,27 @@ describe("router beforeEach guard logic", () => { expect(runGuard({ name: "Login" })).toBeUndefined(); }); + it("redirects unauthenticated user when route requiresAuth", () => { + const result = runGuard({ + name: "Files", + path: "/files", + fullPath: "/files", + matched: [{ meta: { requiresAuth: true } }], + }); + expect(result.path).toBe("/login"); + expect(atob(result.query["return-to"])).toBe("/files"); + }); + it("does not interfere with non-Login routes", () => { document.cookie = "isLuciaLoggedIn=true"; - expect(runGuard({ name: "Files" })).toBeUndefined(); + document.cookie = "luciaToken=token"; + expect( + runGuard({ + name: "Files", + path: "/files", + fullPath: "/files", + matched: [{ meta: { requiresAuth: true } }], + }), + ).toBeUndefined(); }); });