ALIEZ 1 mesiac pred
rodič
commit
c5d2feffe9
2 zmenil súbory, kde vykonal 61 pridanie a 88 odobranie
  1. 61 2
      src/lib/api.ts
  2. 0 86
      src/lib/env.ts

+ 61 - 2
src/lib/api.ts

@@ -5,7 +5,7 @@ import axios, {
 } from "axios";
 
 import { clearUser } from "@/lib/auth-types";
-import { getApiBaseUrl, resolveAuthLoginHref } from "@/lib/env";
+import { getApiBaseUrl } from "@/lib/env";
 
 /**
  * 业务后端接口约定:
@@ -72,12 +72,71 @@ 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.assign(resolveAuthLoginHref());
+  window.location.replace(loginHrefAfterSessionExpired());
 }
 
 function isPlainObject(v: unknown): v is Record<string, unknown> {

+ 0 - 86
src/lib/env.ts

@@ -47,92 +47,6 @@ function resolveApiBaseUrlValue(raw: string | undefined): string {
   return s;
 }
 
-/** PM2 / server.js 常用对外误显端口;登录跳转一律不写进地址栏(本机回环除外)。 */
-const DEFAULT_NODE_PUBLIC_PORT = "4000";
-
-function isLoopbackHostname(hostname: string): boolean {
-  return (
-    hostname === "localhost" ||
-    hostname === "127.0.0.1" ||
-    hostname === "[::1]"
-  );
-}
-
-/** 本地 / 局域网访问时保留当前 origin(含端口),避免误跳到 :80/:443 上无服务。 */
-function shouldPreserveCurrentOriginPort(hostname: string): boolean {
-  if (
-    hostname === "localhost" ||
-    hostname === "127.0.0.1" ||
-    hostname === "[::1]"
-  ) {
-    return true;
-  }
-  const parts = hostname.split(".");
-  if (
-    parts.length === 4 &&
-    parts.every((p) => /^\d+$/.test(p)) &&
-    parts[0] !== undefined &&
-    parts[1] !== undefined
-  ) {
-    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;
-}
-
-/**
- * 登录页跳转地址(仅浏览器)。
- * - 当前端口为 **4000** 且非本机回环时,强制用「协议 + 主机名(无端口)」拼 `/auth/login`,去掉地址栏里的 `:4000`。
- * - 若已配置 `NEXT_PUBLIC_SITE_URL` 且当前 host 与站点一致,优先用该 canonical origin。
- * - localhost / 局域网 IP 且非上述 4000 规则:仍用相对路径,保留端口。
- */
-export function resolveAuthLoginHref(): string {
-  const path = "/auth/login";
-  if (typeof window === "undefined") return path;
-
-  const { protocol, hostname, port } = window.location;
-
-  if (port === DEFAULT_NODE_PUBLIC_PORT && !isLoopbackHostname(hostname)) {
-    try {
-      return new URL(path, `${protocol}//${hostname}/`).href;
-    } catch {
-      /* fall through */
-    }
-  }
-
-  const site = process.env.NEXT_PUBLIC_SITE_URL?.trim().replace(/\/$/, "");
-  if (site) {
-    try {
-      const siteUrl = new URL(site);
-      const canonical = siteUrl.hostname;
-      const allowedHosts = new Set([canonical, `www.${canonical}`]);
-      try {
-        const current = new URL(window.location.href);
-        if (allowedHosts.has(current.hostname)) {
-          return `${siteUrl.origin}${path}`;
-        }
-      } catch {
-        /* fall through */
-      }
-    } catch {
-      /* invalid NEXT_PUBLIC_SITE_URL */
-    }
-  }
-
-  if (shouldPreserveCurrentOriginPort(hostname)) {
-    return path;
-  }
-
-  try {
-    return new URL(path, `${protocol}//${hostname}/`).href;
-  } catch {
-    return path;
-  }
-}
-
 export function resolveAppEnv(): AppEnv {
   const fromEnv = process.env.NEXT_PUBLIC_APP_ENV;
   if (fromEnv === "local" || fromEnv === "test" || fromEnv === "production") {