ALIEZ преди 1 месец
родител
ревизия
cf0c69092f
променени са 5 файла, в които са добавени 27 реда и са изтрити 60 реда
  1. 1 0
      .env.production
  2. 2 0
      src/app/[locale]/layout.tsx
  3. 20 0
      src/components/session-expired-navigation-bridge.tsx
  4. 2 60
      src/lib/api.ts
  5. 2 0
      src/lib/session-expired-events.ts

+ 1 - 0
.env.production

@@ -1,6 +1,7 @@
 NEXT_PUBLIC_APP_ENV=production
 
 # 线上站点根地址(与浏览器访问域名一致,无尾斜杠);SSR / 服务端 axios 解析 __ORIGIN__ 时必需
+# 勿写 Node 监听端口(如 :4000),否则前端跳转登录页会把 :4000 带进地址栏。
 # www 与裸域共用同一套前端时:DNS 需有 www 记录、证书需含 www SAN、Nginx server_name 需含 www.jinclab.com(勿拼成 wwww)
 NEXT_PUBLIC_SITE_URL=https://jinclab.com
 

+ 2 - 0
src/app/[locale]/layout.tsx

@@ -8,6 +8,7 @@ import { LocaleHtmlLang } from "@/components/locale-html-lang";
 import { TopLoader } from "@/components/top-loader";
 import { routing } from "@/i18n/routing";
 import { AuthProvider } from "@/providers/auth-provider";
+import { SessionExpiredNavigationBridge } from "@/components/session-expired-navigation-bridge";
 
 export function generateStaticParams() {
   return routing.locales.map((locale) => ({ locale }));
@@ -44,6 +45,7 @@ export default async function LocaleLayout({
   return (
     <NextIntlClientProvider locale={locale} messages={messages}>
       <AuthProvider>
+        <SessionExpiredNavigationBridge />
         <LocaleHtmlLang />
         <TopLoader />
         <SiteHeader />

+ 20 - 0
src/components/session-expired-navigation-bridge.tsx

@@ -0,0 +1,20 @@
+"use client";
+
+import { useEffect } from "react";
+import { useRouter } from "@/i18n/navigation";
+import { SESSION_EXPIRED_GO_LOGIN } from "@/lib/session-expired-events";
+
+/** 监听 API 会话失效事件,用 next-intl 客户端路由进登录页(同源、不写死端口)。 */
+export function SessionExpiredNavigationBridge() {
+  const router = useRouter();
+
+  useEffect(() => {
+    const goLogin = () => {
+      router.replace("/auth/login");
+    };
+    window.addEventListener(SESSION_EXPIRED_GO_LOGIN, goLogin);
+    return () => window.removeEventListener(SESSION_EXPIRED_GO_LOGIN, goLogin);
+  }, [router]);
+
+  return null;
+}

+ 2 - 60
src/lib/api.ts

@@ -6,6 +6,7 @@ import axios, {
 
 import { clearUser } from "@/lib/auth-types";
 import { getApiBaseUrl } from "@/lib/env";
+import { SESSION_EXPIRED_GO_LOGIN } from "@/lib/session-expired-events";
 
 /**
  * 业务后端接口约定:
@@ -72,71 +73,12 @@ export class ApiError extends Error {
 
 let authTimeoutDialogShown = false;
 
-/** 局域网 IP(无 NEXT_PUBLIC_SITE_URL 时仍用相对路径,避免误跳到 :80)。 */
-function isPrivateLanIpv4Hostname(hostname: string): boolean {
-  const parts = hostname.split(".");
-  if (
-    parts.length !== 4 ||
-    !parts.every((p) => /^\d+$/.test(p)) ||
-    parts[0] === undefined ||
-    parts[1] === undefined
-  ) {
-    return false;
-  }
-  const a = Number(parts[0]);
-  const b = Number(parts[1]);
-  if (a === 10) return true;
-  if (a === 172 && b >= 16 && b <= 31) return true;
-  if (a === 192 && b === 168) return true;
-  return false;
-}
-
-/**
- * 会话失效后整页跳转登录(必须用绝对 URL,禁止相对 `/auth/login`):
- * 相对路径会保留当前 origin 的 `:4000`,国际化再补 `/zh` 就会变成 `host:4000/zh/...`。
- * - 配置了 `NEXT_PUBLIC_SITE_URL` 时一律用它(不再做 hostname 白名单,避免 www/裸域/直连端口不一致时误回退相对路径)。
- * - 本机回环、内网 IP 且无 SITE_URL:仍用相对路径。
- * - 其它:用 `协议//主机名`(不含端口)为基址拼 `/auth/login`。
- */
-function loginHrefAfterSessionExpired(): string {
-  const path = "/auth/login";
-  if (typeof window === "undefined") return path;
-
-  const { hostname, protocol } = window.location;
-  if (
-    hostname === "localhost" ||
-    hostname === "127.0.0.1" ||
-    hostname === "[::1]"
-  ) {
-    return path;
-  }
-
-  const site = process.env.NEXT_PUBLIC_SITE_URL?.trim().replace(/\/$/, "");
-  if (site) {
-    try {
-      return new URL(path, `${site}/`).href;
-    } catch {
-      /* fall through */
-    }
-  }
-
-  if (isPrivateLanIpv4Hostname(hostname)) {
-    return path;
-  }
-
-  try {
-    return new URL(path, `${protocol}//${hostname}/`).href;
-  } catch {
-    return path;
-  }
-}
-
 function notifySessionTimeoutAndRedirect() {
   if (typeof window === "undefined") return;
   if (authTimeoutDialogShown) return;
   authTimeoutDialogShown = true;
   window.alert("登录超时,请重新登录");
-  window.location.replace(loginHrefAfterSessionExpired());
+  window.dispatchEvent(new Event(SESSION_EXPIRED_GO_LOGIN));
 }
 
 function isPlainObject(v: unknown): v is Record<string, unknown> {

+ 2 - 0
src/lib/session-expired-events.ts

@@ -0,0 +1,2 @@
+/** axios 拦截器等非 React 代码派发;{@link SessionExpiredNavigationBridge} 内用 router 跳转,与站内 Link 行为一致。 */
+export const SESSION_EXPIRED_GO_LOGIN = "jchl:session-expired-go-login";