diff --git a/backend/src/main/java/com/unis/crm/controller/DashboardController.java b/backend/src/main/java/com/unis/crm/controller/DashboardController.java index a5a5b6ba..cdd90ccc 100644 --- a/backend/src/main/java/com/unis/crm/controller/DashboardController.java +++ b/backend/src/main/java/com/unis/crm/controller/DashboardController.java @@ -1,6 +1,7 @@ package com.unis.crm.controller; import com.unis.crm.common.ApiResponse; +import com.unis.crm.common.CurrentUserUtils; import com.unis.crm.dto.dashboard.DashboardAnalyticsCardDTO; import com.unis.crm.dto.dashboard.DashboardHomeDTO; import com.unis.crm.service.DashboardService; @@ -29,7 +30,7 @@ public class DashboardController { @GetMapping("/home") public ApiResponse getHome( @RequestHeader("X-User-Id") @Min(1) Long userId) { - return ApiResponse.success(dashboardService.getHome(userId)); + return ApiResponse.success(dashboardService.getHome(CurrentUserUtils.requireCurrentUserId(userId))); } @PostMapping("/todos/{todoId}/complete") @@ -37,7 +38,7 @@ public class DashboardController { public ApiResponse completeTodo( @RequestHeader("X-User-Id") @Min(1) Long userId, @PathVariable("todoId") @Min(1) Long todoId) { - dashboardService.completeTodo(userId, todoId); + dashboardService.completeTodo(CurrentUserUtils.requireCurrentUserId(userId), todoId); return ApiResponse.success(null); } @@ -46,6 +47,6 @@ public class DashboardController { @RequestHeader("X-User-Id") @Min(1) Long userId, @PathVariable("cardKey") String cardKey, @RequestParam(value = "dimension", required = false) String dimension) { - return ApiResponse.success(dashboardService.getAnalyticsCardDetail(userId, cardKey, dimension)); + return ApiResponse.success(dashboardService.getAnalyticsCardDetail(CurrentUserUtils.requireCurrentUserId(userId), cardKey, dimension)); } } diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts index 51c9578c..53b16640 100644 --- a/frontend/src/lib/auth.ts +++ b/frontend/src/lib/auth.ts @@ -763,6 +763,16 @@ const PROFILE_OVERVIEW_CACHE_KEY = "auth-cache:profile-overview"; const memoryRequestCache = new Map(); const inFlightRequestCache = new Map>(); let authExpiryTimer: number | null = null; +const AUTH_REQUIRED_MESSAGE_PATTERNS = [ + "登录已失效", + "重新登录", + "未登录", + "未获取到当前登录用户", + "禁止查询他人数据", + "禁止操作他人数据", + "unauthorized", + "authorization", +] as const; type AccessTokenPayload = { exp?: number; @@ -935,6 +945,40 @@ function buildApiErrorMessage(rawText: string, status: number, body?: ApiErrorBo return normalizedText.length > 300 ? normalizedText.slice(0, 300) : normalizedText; } +function normalizeAuthErrorSource(value?: string | null) { + return value?.trim().toLowerCase() || ""; +} + +function isUnauthorizedMessage(value?: string | null) { + const normalized = normalizeAuthErrorSource(value); + if (!normalized) { + return false; + } + + return AUTH_REQUIRED_MESSAGE_PATTERNS.some((pattern) => normalized.includes(pattern.toLowerCase())); +} + +function isLoginRedirectResponse(response: Response) { + if (!response.redirected || !response.url) { + return false; + } + + try { + const responseUrl = new URL(response.url, window.location.origin); + return responseUrl.pathname === LOGIN_PATH; + } catch { + return false; + } +} + +function shouldTreatAsUnauthorized(response: Response, message?: string | null, body?: ApiErrorBody | null) { + if (response.status === 401 || isLoginRedirectResponse(response)) { + return true; + } + + return isUnauthorizedMessage(message) || isUnauthorizedMessage(body?.msg) || isUnauthorizedMessage(body?.message); +} + async function request(input: string, init?: RequestInit, withAuth = false): Promise { const headers = new Headers(init?.headers); if (!headers.has("Content-Type") && init?.body && !(init.body instanceof FormData)) { @@ -950,24 +994,30 @@ async function request(input: string, init?: RequestInit, withAuth = false): headers, }); - if (response.status === 401) { + if (withAuth && shouldTreatAsUnauthorized(response)) { handleUnauthorizedResponse(); throw new Error("登录已失效,请重新登录"); } const rawText = await response.text(); const body = tryParseApiBody(rawText, response.headers.get("content-type")); + const errorMessage = buildApiErrorMessage(rawText, response.status, body); + + if (withAuth && shouldTreatAsUnauthorized(response, errorMessage, body)) { + handleUnauthorizedResponse(); + throw new Error("登录已失效,请重新登录"); + } if (!response.ok) { - throw new Error(buildApiErrorMessage(rawText, response.status, body)); + throw new Error(errorMessage); } if (!body) { - throw new Error(rawText.trim() ? buildApiErrorMessage(rawText, response.status, null) : "接口返回为空"); + throw new Error(rawText.trim() ? errorMessage : "接口返回为空"); } if (!isSuccessCode(body.code)) { - throw new Error(buildApiErrorMessage(rawText, response.status, body)); + throw new Error(errorMessage); } return body.data; @@ -979,7 +1029,7 @@ export async function fetchWithAuth(input: string, init?: RequestInit) { headers: applyAuthHeaders(new Headers(init?.headers)), }); - if (response.status === 401) { + if (shouldTreatAsUnauthorized(response)) { handleUnauthorizedResponse(); throw new Error("登录已失效,请重新登录"); }