diff --git a/app/composables/useAuthSession.ts b/app/composables/useAuthSession.ts new file mode 100644 index 0000000..1c10ea6 --- /dev/null +++ b/app/composables/useAuthSession.ts @@ -0,0 +1,82 @@ +import { request, unwrapApiBody, type ApiResponse } from "../utils/http/factory"; + +export type AuthUser = { + id: number; + username: string; +}; + +type MeResult = { + user: AuthUser; +}; + +export type AuthSessionState = { + initialized: boolean; + pending: boolean; + loggedIn: boolean; + user: AuthUser | null; +}; + +export const AUTH_SESSION_STATE_KEY = "auth:session"; +export const DEFAULT_AUTH_SESSION_STATE: AuthSessionState = { + initialized: false, + pending: false, + loggedIn: false, + user: null, +}; + +function isUnauthorized(error: unknown) { + if (typeof error !== "object" || error === null) { + return false; + } + return "statusCode" in error && (error as { statusCode?: number }).statusCode === 401; +} + +export function useAuthSession() { + const state = useState(AUTH_SESSION_STATE_KEY, () => ({ + ...DEFAULT_AUTH_SESSION_STATE, + })); + + const applyUser = (user: AuthUser | null) => { + state.value.user = user; + state.value.loggedIn = Boolean(user); + state.value.initialized = true; + }; + + const clear = () => { + applyUser(null); + }; + + const refresh = async (force = false) => { + if (state.value.initialized && !force) { + return state.value.user; + } + if (state.value.pending) { + return state.value.user; + } + state.value.pending = true; + try { + const fetcher = import.meta.server ? useRequestFetch() : request; + const payload = await fetcher>("/api/auth/me"); + const data = unwrapApiBody(payload); + applyUser(data.user); + return data.user; + } catch (error: unknown) { + if (isUnauthorized(error)) { + clear(); + return null; + } + throw error; + } finally { + state.value.pending = false; + } + }; + + return { + initialized: computed(() => state.value.initialized), + loggedIn: computed(() => state.value.loggedIn), + user: computed(() => state.value.user), + pending: computed(() => state.value.pending), + refresh, + clear, + }; +} diff --git a/app/layouts/default.vue b/app/layouts/default.vue index b58d962..7a3e681 100644 --- a/app/layouts/default.vue +++ b/app/layouts/default.vue @@ -11,8 +11,9 @@ -
- +
+ +
@@ -67,7 +68,7 @@ const menuItems = [ }, { label: "首页页面", - to: "/index", + to: "/", }, ], }, diff --git a/app/layouts/not-login.vue b/app/layouts/not-login.vue index d41cb1d..d6ee668 100644 --- a/app/layouts/not-login.vue +++ b/app/layouts/not-login.vue @@ -64,7 +64,7 @@ const menuItems = [ }, { label: "首页页面", - to: "/index", + to: "/", }, ], }, diff --git a/app/middleware/auth.global.ts b/app/middleware/auth.global.ts new file mode 100644 index 0000000..e680556 --- /dev/null +++ b/app/middleware/auth.global.ts @@ -0,0 +1,40 @@ +import { + DEFAULT_AUTHENTICATED_LANDING_PATH, + isGuestOnlyRoute, + isPublicRoute, + normalizeSafeRedirect, +} from "../utils/auth-routes"; +import { useAuthSession } from "../composables/useAuthSession"; + +export default defineNuxtRouteMiddleware(async (to) => { + const { initialized, loggedIn, refresh } = useAuthSession(); + if (!initialized.value) { + await refresh(); + } + + const currentPath = to.path; + const currentFullPath = to.fullPath; + const isLoggedIn = loggedIn.value; + + if (!isLoggedIn && !isPublicRoute(currentPath)) { + return navigateTo({ + path: "/login", + query: { redirect: currentFullPath }, + }); + } + + if (isLoggedIn && isGuestOnlyRoute(currentPath)) { + const redirectCandidate = Array.isArray(to.query.redirect) + ? to.query.redirect[0] + : to.query.redirect; + const redirectTarget = normalizeSafeRedirect( + redirectCandidate, + DEFAULT_AUTHENTICATED_LANDING_PATH, + ); + + if (redirectTarget !== currentFullPath && redirectTarget !== currentPath) { + return navigateTo(redirectTarget); + } + return navigateTo(DEFAULT_AUTHENTICATED_LANDING_PATH); + } +}); diff --git a/app/pages/index/index.vue b/app/pages/index/index.vue index 3d08253..2a15808 100644 --- a/app/pages/index/index.vue +++ b/app/pages/index/index.vue @@ -1,11 +1,44 @@ \ No newline at end of file diff --git a/app/pages/login/index.vue b/app/pages/login/index.vue index c5c5caf..f619054 100644 --- a/app/pages/login/index.vue +++ b/app/pages/login/index.vue @@ -1,6 +1,8 @@