登录修复还未升级
parent
4b308e8381
commit
f72fb86c16
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect, type ReactNode } from "react";
|
||||
import { BrowserRouter, Navigate, Routes, Route } from "react-router-dom";
|
||||
import Layout from "./components/Layout";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
|
|
@ -15,7 +15,7 @@ import OwnerTransfer from "./pages/OwnerTransfer";
|
|||
import { ThemeProvider } from "./components/ThemeProvider";
|
||||
import LoginPage from "./pages/Login";
|
||||
import WecomLoginCallbackPage from "./pages/WecomLoginCallback";
|
||||
import { isAuthed } from "./lib/auth";
|
||||
import { isAuthed, startAuthSessionMonitor } from "./lib/auth";
|
||||
|
||||
function RequireAuth({ children }: { children: ReactNode }) {
|
||||
if (!isAuthed()) {
|
||||
|
|
@ -26,6 +26,8 @@ function RequireAuth({ children }: { children: ReactNode }) {
|
|||
}
|
||||
|
||||
export default function App() {
|
||||
useEffect(() => startAuthSessionMonitor(), []);
|
||||
|
||||
return (
|
||||
<ThemeProvider defaultTheme="light" storageKey="crm-theme">
|
||||
<BrowserRouter>
|
||||
|
|
|
|||
|
|
@ -411,6 +411,7 @@ export interface OpportunityItem {
|
|||
name?: string;
|
||||
client?: string;
|
||||
owner?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
projectLocation?: string;
|
||||
operatorCode?: string;
|
||||
|
|
@ -519,6 +520,7 @@ export interface SalesExpansionItem {
|
|||
ownerUserId?: number;
|
||||
owner?: string;
|
||||
type: "sales";
|
||||
createdAt?: string;
|
||||
employeeNo?: string;
|
||||
name?: string;
|
||||
officeCode?: string;
|
||||
|
|
@ -559,6 +561,7 @@ export interface ChannelExpansionItem {
|
|||
ownerUserId?: number;
|
||||
owner?: string;
|
||||
type: "channel";
|
||||
createdAt?: string;
|
||||
channelCode?: string;
|
||||
name?: string;
|
||||
provinceCode?: string;
|
||||
|
|
@ -759,11 +762,78 @@ const CURRENT_USER_CACHE_KEY = "auth-cache:current-user";
|
|||
const PROFILE_OVERVIEW_CACHE_KEY = "auth-cache:profile-overview";
|
||||
const memoryRequestCache = new Map<string, { expiresAt: number; value: unknown }>();
|
||||
const inFlightRequestCache = new Map<string, Promise<unknown>>();
|
||||
let authExpiryTimer: number | null = null;
|
||||
|
||||
type AccessTokenPayload = {
|
||||
exp?: number;
|
||||
userId?: number;
|
||||
tenantId?: number;
|
||||
};
|
||||
|
||||
function isSuccessCode(code: unknown) {
|
||||
return code === "0" || code === "200" || code === 0 || code === 200;
|
||||
}
|
||||
|
||||
function clearAuthExpiryTimer() {
|
||||
if (authExpiryTimer !== null) {
|
||||
window.clearTimeout(authExpiryTimer);
|
||||
authExpiryTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function decodeJwtPayload<T>(token: string): T | null {
|
||||
try {
|
||||
const [, payloadSegment] = token.split(".");
|
||||
if (!payloadSegment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedPayload = payloadSegment
|
||||
.replace(/-/g, "+")
|
||||
.replace(/_/g, "/")
|
||||
.padEnd(Math.ceil(payloadSegment.length / 4) * 4, "=");
|
||||
|
||||
return JSON.parse(atob(normalizedPayload)) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getAccessTokenPayload(token?: string | null) {
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return decodeJwtPayload<AccessTokenPayload>(token);
|
||||
}
|
||||
|
||||
function getAccessTokenExpiresAt(token?: string | null) {
|
||||
const payload = getAccessTokenPayload(token);
|
||||
if (!payload || typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return payload.exp * 1000;
|
||||
}
|
||||
|
||||
function hasAccessTokenExpired(token?: string | null, now = Date.now()) {
|
||||
const expiresAt = getAccessTokenExpiresAt(token);
|
||||
if (expiresAt === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return expiresAt <= now;
|
||||
}
|
||||
|
||||
function redirectToLoginForTimeout() {
|
||||
const currentUrl = new URL(window.location.href);
|
||||
if (currentUrl.pathname === LOGIN_PATH && currentUrl.searchParams.get("timeout") === "1") {
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.replace(`${LOGIN_PATH}?timeout=1`);
|
||||
}
|
||||
|
||||
function applyAuthHeaders(headers: Headers) {
|
||||
const token = localStorage.getItem("accessToken");
|
||||
if (token) {
|
||||
|
|
@ -785,7 +855,26 @@ function applyAuthHeaders(headers: Headers) {
|
|||
|
||||
function handleUnauthorizedResponse() {
|
||||
clearAuth();
|
||||
window.location.href = `${LOGIN_PATH}?timeout=1`;
|
||||
redirectToLoginForTimeout();
|
||||
}
|
||||
|
||||
function syncAuthExpiryTimer() {
|
||||
clearAuthExpiryTimer();
|
||||
|
||||
const token = localStorage.getItem("accessToken");
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expiresAt = getAccessTokenExpiresAt(token);
|
||||
if (expiresAt === null || expiresAt <= Date.now()) {
|
||||
handleUnauthorizedResponse();
|
||||
return;
|
||||
}
|
||||
|
||||
authExpiryTimer = window.setTimeout(() => {
|
||||
handleUnauthorizedResponse();
|
||||
}, expiresAt - Date.now());
|
||||
}
|
||||
|
||||
function tryParseApiBody<T>(rawText: string, contentType?: string | null): (ApiEnvelope<T> & ApiErrorBody) | null {
|
||||
|
|
@ -899,10 +988,12 @@ export async function fetchWithAuth(input: string, init?: RequestInit) {
|
|||
}
|
||||
|
||||
export function isAuthed() {
|
||||
return Boolean(localStorage.getItem("accessToken"));
|
||||
const token = localStorage.getItem("accessToken");
|
||||
return Boolean(token) && !hasAccessTokenExpired(token);
|
||||
}
|
||||
|
||||
export function clearAuth() {
|
||||
clearAuthExpiryTimer();
|
||||
localStorage.removeItem("accessToken");
|
||||
localStorage.removeItem("refreshToken");
|
||||
localStorage.removeItem("username");
|
||||
|
|
@ -921,7 +1012,7 @@ export function persistLogin(payload: TokenResponse, username: string) {
|
|||
if (payload.availableTenants) {
|
||||
localStorage.setItem("availableTenants", JSON.stringify(payload.availableTenants));
|
||||
try {
|
||||
const tokenPayload = JSON.parse(atob(payload.accessToken.split(".")[1]));
|
||||
const tokenPayload = getAccessTokenPayload(payload.accessToken);
|
||||
if (tokenPayload?.tenantId !== undefined) {
|
||||
localStorage.setItem("activeTenantId", String(tokenPayload.tenantId));
|
||||
}
|
||||
|
|
@ -929,6 +1020,8 @@ export function persistLogin(payload: TokenResponse, username: string) {
|
|||
localStorage.removeItem("activeTenantId");
|
||||
}
|
||||
}
|
||||
|
||||
syncAuthExpiryTimer();
|
||||
}
|
||||
|
||||
function getStoredUserId() {
|
||||
|
|
@ -950,7 +1043,7 @@ function getStoredUserId() {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
const tokenPayload = JSON.parse(atob(token.split(".")[1])) as { userId?: number };
|
||||
const tokenPayload = getAccessTokenPayload(token);
|
||||
if (typeof tokenPayload.userId === "number" && Number.isFinite(tokenPayload.userId)) {
|
||||
return tokenPayload.userId;
|
||||
}
|
||||
|
|
@ -965,6 +1058,32 @@ export function getStoredCurrentUserId() {
|
|||
return getStoredUserId();
|
||||
}
|
||||
|
||||
export function startAuthSessionMonitor() {
|
||||
const syncIfVisible = () => {
|
||||
if (document.visibilityState !== "hidden") {
|
||||
syncAuthExpiryTimer();
|
||||
}
|
||||
};
|
||||
|
||||
const handleStorageChange = (event: StorageEvent) => {
|
||||
if (!event.key || event.key === "accessToken") {
|
||||
syncAuthExpiryTimer();
|
||||
}
|
||||
};
|
||||
|
||||
syncAuthExpiryTimer();
|
||||
document.addEventListener("visibilitychange", syncIfVisible);
|
||||
window.addEventListener("focus", syncIfVisible);
|
||||
window.addEventListener("storage", handleStorageChange);
|
||||
|
||||
return () => {
|
||||
clearAuthExpiryTimer();
|
||||
document.removeEventListener("visibilitychange", syncIfVisible);
|
||||
window.removeEventListener("focus", syncIfVisible);
|
||||
window.removeEventListener("storage", handleStorageChange);
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchCaptcha() {
|
||||
return request<CaptchaResponse>("/api/sys/auth/captcha");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue