登录修复还未升级
parent
4b308e8381
commit
f72fb86c16
|
|
@ -3,7 +3,7 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* 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 { BrowserRouter, Navigate, Routes, Route } from "react-router-dom";
|
||||||
import Layout from "./components/Layout";
|
import Layout from "./components/Layout";
|
||||||
import Dashboard from "./pages/Dashboard";
|
import Dashboard from "./pages/Dashboard";
|
||||||
|
|
@ -15,7 +15,7 @@ import OwnerTransfer from "./pages/OwnerTransfer";
|
||||||
import { ThemeProvider } from "./components/ThemeProvider";
|
import { ThemeProvider } from "./components/ThemeProvider";
|
||||||
import LoginPage from "./pages/Login";
|
import LoginPage from "./pages/Login";
|
||||||
import WecomLoginCallbackPage from "./pages/WecomLoginCallback";
|
import WecomLoginCallbackPage from "./pages/WecomLoginCallback";
|
||||||
import { isAuthed } from "./lib/auth";
|
import { isAuthed, startAuthSessionMonitor } from "./lib/auth";
|
||||||
|
|
||||||
function RequireAuth({ children }: { children: ReactNode }) {
|
function RequireAuth({ children }: { children: ReactNode }) {
|
||||||
if (!isAuthed()) {
|
if (!isAuthed()) {
|
||||||
|
|
@ -26,6 +26,8 @@ function RequireAuth({ children }: { children: ReactNode }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
useEffect(() => startAuthSessionMonitor(), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider defaultTheme="light" storageKey="crm-theme">
|
<ThemeProvider defaultTheme="light" storageKey="crm-theme">
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
|
|
||||||
|
|
@ -411,6 +411,7 @@ export interface OpportunityItem {
|
||||||
name?: string;
|
name?: string;
|
||||||
client?: string;
|
client?: string;
|
||||||
owner?: string;
|
owner?: string;
|
||||||
|
createdAt?: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
projectLocation?: string;
|
projectLocation?: string;
|
||||||
operatorCode?: string;
|
operatorCode?: string;
|
||||||
|
|
@ -519,6 +520,7 @@ export interface SalesExpansionItem {
|
||||||
ownerUserId?: number;
|
ownerUserId?: number;
|
||||||
owner?: string;
|
owner?: string;
|
||||||
type: "sales";
|
type: "sales";
|
||||||
|
createdAt?: string;
|
||||||
employeeNo?: string;
|
employeeNo?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
officeCode?: string;
|
officeCode?: string;
|
||||||
|
|
@ -559,6 +561,7 @@ export interface ChannelExpansionItem {
|
||||||
ownerUserId?: number;
|
ownerUserId?: number;
|
||||||
owner?: string;
|
owner?: string;
|
||||||
type: "channel";
|
type: "channel";
|
||||||
|
createdAt?: string;
|
||||||
channelCode?: string;
|
channelCode?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
provinceCode?: 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 PROFILE_OVERVIEW_CACHE_KEY = "auth-cache:profile-overview";
|
||||||
const memoryRequestCache = new Map<string, { expiresAt: number; value: unknown }>();
|
const memoryRequestCache = new Map<string, { expiresAt: number; value: unknown }>();
|
||||||
const inFlightRequestCache = new Map<string, Promise<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) {
|
function isSuccessCode(code: unknown) {
|
||||||
return code === "0" || code === "200" || code === 0 || code === 200;
|
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) {
|
function applyAuthHeaders(headers: Headers) {
|
||||||
const token = localStorage.getItem("accessToken");
|
const token = localStorage.getItem("accessToken");
|
||||||
if (token) {
|
if (token) {
|
||||||
|
|
@ -785,7 +855,26 @@ function applyAuthHeaders(headers: Headers) {
|
||||||
|
|
||||||
function handleUnauthorizedResponse() {
|
function handleUnauthorizedResponse() {
|
||||||
clearAuth();
|
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 {
|
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() {
|
export function isAuthed() {
|
||||||
return Boolean(localStorage.getItem("accessToken"));
|
const token = localStorage.getItem("accessToken");
|
||||||
|
return Boolean(token) && !hasAccessTokenExpired(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearAuth() {
|
export function clearAuth() {
|
||||||
|
clearAuthExpiryTimer();
|
||||||
localStorage.removeItem("accessToken");
|
localStorage.removeItem("accessToken");
|
||||||
localStorage.removeItem("refreshToken");
|
localStorage.removeItem("refreshToken");
|
||||||
localStorage.removeItem("username");
|
localStorage.removeItem("username");
|
||||||
|
|
@ -921,7 +1012,7 @@ export function persistLogin(payload: TokenResponse, username: string) {
|
||||||
if (payload.availableTenants) {
|
if (payload.availableTenants) {
|
||||||
localStorage.setItem("availableTenants", JSON.stringify(payload.availableTenants));
|
localStorage.setItem("availableTenants", JSON.stringify(payload.availableTenants));
|
||||||
try {
|
try {
|
||||||
const tokenPayload = JSON.parse(atob(payload.accessToken.split(".")[1]));
|
const tokenPayload = getAccessTokenPayload(payload.accessToken);
|
||||||
if (tokenPayload?.tenantId !== undefined) {
|
if (tokenPayload?.tenantId !== undefined) {
|
||||||
localStorage.setItem("activeTenantId", String(tokenPayload.tenantId));
|
localStorage.setItem("activeTenantId", String(tokenPayload.tenantId));
|
||||||
}
|
}
|
||||||
|
|
@ -929,6 +1020,8 @@ export function persistLogin(payload: TokenResponse, username: string) {
|
||||||
localStorage.removeItem("activeTenantId");
|
localStorage.removeItem("activeTenantId");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncAuthExpiryTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStoredUserId() {
|
function getStoredUserId() {
|
||||||
|
|
@ -950,7 +1043,7 @@ function getStoredUserId() {
|
||||||
return undefined;
|
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)) {
|
if (typeof tokenPayload.userId === "number" && Number.isFinite(tokenPayload.userId)) {
|
||||||
return tokenPayload.userId;
|
return tokenPayload.userId;
|
||||||
}
|
}
|
||||||
|
|
@ -965,6 +1058,32 @@ export function getStoredCurrentUserId() {
|
||||||
return getStoredUserId();
|
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() {
|
export async function fetchCaptcha() {
|
||||||
return request<CaptchaResponse>("/api/sys/auth/captcha");
|
return request<CaptchaResponse>("/api/sys/auth/captcha");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue