From 8836549697cf9693873af0e273e76d8bc2a28130 Mon Sep 17 00:00:00 2001 From: Huarch Date: Thu, 30 Jul 2026 14:21:20 +0800 Subject: [PATCH] fix(auth): unify scheme creator display --- src/app/RefineContext.tsx | 1 + src/app/api/auth/[...nextauth]/options.ts | 10 ++++++ .../olmap/BurstLocation/SchemeQuery.tsx | 4 ++- .../BurstSimulation/AnalysisReport.test.tsx | 2 +- .../olmap/BurstSimulation/AnalysisReport.tsx | 2 +- .../BurstPipeAnalysisPanel.test.tsx | 2 +- .../olmap/BurstSimulation/SchemeQuery.tsx | 6 ++-- src/components/olmap/BurstSimulation/types.ts | 2 +- .../ContaminantSimulation/SchemeQuery.tsx | 6 ++-- .../olmap/ContaminantSimulation/types.ts | 2 +- .../olmap/DMALeakDetection/SchemeQuery.tsx | 4 ++- .../olmap/FlushingAnalysis/SchemeQuery.tsx | 9 ++++-- .../olmap/FlushingAnalysis/types.ts | 2 +- .../SchemeQuery.tsx | 9 ++++-- .../olmap/core/useSchemeCreatorName.test.ts | 31 +++++++++++++++++++ .../olmap/core/useSchemeCreatorName.ts | 29 +++++++++++++++++ src/types/next-auth.d.ts | 3 ++ 17 files changed, 106 insertions(+), 18 deletions(-) create mode 100644 src/components/olmap/core/useSchemeCreatorName.test.ts create mode 100644 src/components/olmap/core/useSchemeCreatorName.ts diff --git a/src/app/RefineContext.tsx b/src/app/RefineContext.tsx index 4826171..87773c8 100644 --- a/src/app/RefineContext.tsx +++ b/src/app/RefineContext.tsx @@ -146,6 +146,7 @@ const App = (props: React.PropsWithChildren) => { const { user } = data; return { id: user.id, + username: user.username, name: user.name, avatar: user.image, }; diff --git a/src/app/api/auth/[...nextauth]/options.ts b/src/app/api/auth/[...nextauth]/options.ts index 194af83..5af6bae 100644 --- a/src/app/api/auth/[...nextauth]/options.ts +++ b/src/app/api/auth/[...nextauth]/options.ts @@ -66,6 +66,7 @@ const authOptions: NextAuthOptions = { profile(profile) { return { id: profile.sub, + username: profile.preferred_username, name: profile.name ?? profile.preferred_username, email: profile.email, image: Avatar.src, @@ -79,6 +80,12 @@ const authOptions: NextAuthOptions = { if (profile?.sub) { token.sub = profile.sub; } + const preferredUsername = ( + profile as { preferred_username?: unknown } | undefined + )?.preferred_username; + if (typeof preferredUsername === "string") { + token.username = preferredUsername; + } if (account) { if (account.access_token) { @@ -104,6 +111,9 @@ const authOptions: NextAuthOptions = { if (session.user && token.sub) { session.user.id = token.sub; } + if (session.user && token.username) { + session.user.username = token.username; + } if (token.accessToken) { session.accessToken = token.accessToken; } diff --git a/src/components/olmap/BurstLocation/SchemeQuery.tsx b/src/components/olmap/BurstLocation/SchemeQuery.tsx index a001daa..5899981 100644 --- a/src/components/olmap/BurstLocation/SchemeQuery.tsx +++ b/src/components/olmap/BurstLocation/SchemeQuery.tsx @@ -42,6 +42,7 @@ import { BurstSchemeRecord, } from "./types"; import { FLOW_DISPLAY_UNIT, toM3h } from "@utils/units"; +import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName"; interface Props { onViewResult: (result: BurstLocationResult) => void; @@ -86,6 +87,7 @@ const SchemeQuery: React.FC = ({ const simulationBurstIdsByName = queryState.simulationBurstIdsByName ?? {}; const [internalSchemes, setInternalSchemes] = useState([]); const [loading, setLoading] = useState(false); + const creatorName = useSchemeCreatorName(); const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes; const setSchemes = onSchemesChange || setInternalSchemes; const sortedSchemes = useMemo( @@ -526,7 +528,7 @@ const SchemeQuery: React.FC = ({ 用户: - {scheme.username || "-"} + {creatorName(scheme.username)} diff --git a/src/components/olmap/BurstSimulation/AnalysisReport.test.tsx b/src/components/olmap/BurstSimulation/AnalysisReport.test.tsx index b33ef70..1571911 100644 --- a/src/components/olmap/BurstSimulation/AnalysisReport.test.tsx +++ b/src/components/olmap/BurstSimulation/AnalysisReport.test.tsx @@ -21,7 +21,7 @@ const scheme: SchemeRecord = { id: 17, schemeName: "burst-report-demo", type: "burst_analysis", - user: "operator", + username: "operator", create_time: "2026-07-30T08:00:00+08:00", startTime: "2026-07-30T09:00:00+08:00", schemeDetail: { diff --git a/src/components/olmap/BurstSimulation/AnalysisReport.tsx b/src/components/olmap/BurstSimulation/AnalysisReport.tsx index 519eea2..330252e 100644 --- a/src/components/olmap/BurstSimulation/AnalysisReport.tsx +++ b/src/components/olmap/BurstSimulation/AnalysisReport.tsx @@ -255,7 +255,7 @@ const ReportDocument: React.FC = ({ {[ ["管网", NETWORK_NAME], ["方案名称", scheme.schemeName], - ["方案创建人", scheme.user || "未记录"], + ["方案创建人", scheme.username || "未记录"], ["方案创建时间", formatDateTime(scheme.create_time)], ["模拟开始时间", formatDateTime(scheme.startTime)], ["模拟持续时间", formatDuration(duration)], diff --git a/src/components/olmap/BurstSimulation/BurstPipeAnalysisPanel.test.tsx b/src/components/olmap/BurstSimulation/BurstPipeAnalysisPanel.test.tsx index 7e6b7a6..1089de5 100644 --- a/src/components/olmap/BurstSimulation/BurstPipeAnalysisPanel.test.tsx +++ b/src/components/olmap/BurstSimulation/BurstPipeAnalysisPanel.test.tsx @@ -18,7 +18,7 @@ jest.mock("./SchemeQuery", () => ({ id, schemeName, type: "burst_analysis", - user: "operator", + username: "operator", create_time: "2026-07-30T08:00:00+08:00", startTime: "2026-07-30T09:00:00+08:00", schemeDetail: { diff --git a/src/components/olmap/BurstSimulation/SchemeQuery.tsx b/src/components/olmap/BurstSimulation/SchemeQuery.tsx index 734ddef..e72af40 100644 --- a/src/components/olmap/BurstSimulation/SchemeQuery.tsx +++ b/src/components/olmap/BurstSimulation/SchemeQuery.tsx @@ -56,6 +56,7 @@ import { getPipeDiameterDisplay, type PipeDiameterMap, } from "./schemePipeDiameters"; +import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName"; interface SchemeQueryProps { schemes?: SchemeRecord[]; @@ -113,6 +114,7 @@ const SchemeQuery: React.FC = ({ const [internalSchemes, setInternalSchemes] = useState([]); const [loading, setLoading] = useState(false); + const creatorName = useSchemeCreatorName(); const [mapContainer, setMapContainer] = useState(null); // 地图容器元素 const [pipeDiametersByScheme, setPipeDiametersByScheme] = useState< Record @@ -177,7 +179,7 @@ const SchemeQuery: React.FC = ({ id: item.scheme_id, schemeName: item.scheme_name, type: item.scheme_type, - user: item.username, + username: item.username, create_time: item.create_time, startTime: item.scheme_start_time, schemeDetail: item.scheme_detail, @@ -723,7 +725,7 @@ const SchemeQuery: React.FC = ({ variant="caption" className="font-medium text-gray-900" > - {scheme.user} + {creatorName(scheme.username)} diff --git a/src/components/olmap/BurstSimulation/types.ts b/src/components/olmap/BurstSimulation/types.ts index 91abb26..8db31a0 100644 --- a/src/components/olmap/BurstSimulation/types.ts +++ b/src/components/olmap/BurstSimulation/types.ts @@ -11,7 +11,7 @@ export interface SchemeRecord { id: number; schemeName: string; type: string; - user: string; + username: string; create_time: string; startTime: string; // 详情信息 diff --git a/src/components/olmap/ContaminantSimulation/SchemeQuery.tsx b/src/components/olmap/ContaminantSimulation/SchemeQuery.tsx index c9607f4..5f4173c 100644 --- a/src/components/olmap/ContaminantSimulation/SchemeQuery.tsx +++ b/src/components/olmap/ContaminantSimulation/SchemeQuery.tsx @@ -40,6 +40,7 @@ import Feature from "ol/Feature"; import { bbox, featureCollection } from "@turf/turf"; import Timeline from "@components/olmap/core/Controls/Timeline"; import { ContaminantSchemaItem, ContaminantSchemeRecord } from "./types"; +import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName"; interface SchemeQueryProps { schemes?: ContaminantSchemeRecord[]; @@ -101,6 +102,7 @@ const SchemeQuery: React.FC = ({ >([]); const [loading, setLoading] = useState(false); const [mapContainer, setMapContainer] = useState(null); + const creatorName = useSchemeCreatorName(); const { open } = useNotification(); const map = useMap(); @@ -242,7 +244,7 @@ const SchemeQuery: React.FC = ({ id: item.scheme_id, schemeName: item.scheme_name, type: item.scheme_type, - user: item.username, + username: item.username, create_time: item.create_time, startTime: item.scheme_start_time, schemeDetail: item.scheme_detail, @@ -598,7 +600,7 @@ const SchemeQuery: React.FC = ({ variant="caption" className="font-medium text-gray-900" > - {scheme.user} + {creatorName(scheme.username)} diff --git a/src/components/olmap/ContaminantSimulation/types.ts b/src/components/olmap/ContaminantSimulation/types.ts index fd8ef45..8fc2841 100644 --- a/src/components/olmap/ContaminantSimulation/types.ts +++ b/src/components/olmap/ContaminantSimulation/types.ts @@ -9,7 +9,7 @@ export interface ContaminantSchemeRecord { id: number; schemeName: string; type: string; - user: string; + username: string; create_time: string; startTime: string; schemeDetail?: ContaminantSchemeDetail; diff --git a/src/components/olmap/DMALeakDetection/SchemeQuery.tsx b/src/components/olmap/DMALeakDetection/SchemeQuery.tsx index c92c110..c599550 100644 --- a/src/components/olmap/DMALeakDetection/SchemeQuery.tsx +++ b/src/components/olmap/DMALeakDetection/SchemeQuery.tsx @@ -26,6 +26,7 @@ import { NETWORK_NAME, config } from "@config/config"; import { useControllableObjectState } from "@components/olmap/core/useControllableState"; import { LeakageResultDetail, LeakageSchemeRecord } from "./types"; import { FLOW_DISPLAY_UNIT, toM3h } from "@utils/units"; +import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName"; interface Props { onViewResult: (result: LeakageResultDetail) => void; @@ -63,6 +64,7 @@ const SchemeQuery: React.FC = ({ const { queryAll, queryDate, expandedId } = queryState; const [internalSchemes, setInternalSchemes] = useState([]); const [loading, setLoading] = useState(false); + const creatorName = useSchemeCreatorName(); const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes; const setSchemes = onSchemesChange || setInternalSchemes; const sortedSchemes = useMemo( @@ -265,7 +267,7 @@ const SchemeQuery: React.FC = ({ 用户: - {scheme.username || "-"} + {creatorName(scheme.username)} diff --git a/src/components/olmap/FlushingAnalysis/SchemeQuery.tsx b/src/components/olmap/FlushingAnalysis/SchemeQuery.tsx index fdd0203..1515897 100644 --- a/src/components/olmap/FlushingAnalysis/SchemeQuery.tsx +++ b/src/components/olmap/FlushingAnalysis/SchemeQuery.tsx @@ -42,6 +42,7 @@ import { bbox, featureCollection } from "@turf/turf"; import Timeline from "@components/olmap/core/Controls/Timeline"; import { SchemeRecord, SchemaItem } from "./types"; import { FLOW_DISPLAY_UNIT } from "@utils/units"; +import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName"; interface SchemeQueryProps { schemes?: SchemeRecord[]; @@ -100,6 +101,7 @@ const SchemeQuery: React.FC = ({ const [highlightFeatures, setHighlightFeatures] = useState([]); const [mapContainer, setMapContainer] = useState(null); + const creatorName = useSchemeCreatorName(); const { open } = useNotification(); const map = useMap(); @@ -290,7 +292,7 @@ const SchemeQuery: React.FC = ({ id: item.scheme_id, schemeName: item.scheme_name, type: item.scheme_type, - user: item.username, + username: item.username, create_time: item.create_time, startTime: item.scheme_start_time, schemeDetail: item.scheme_detail, @@ -479,7 +481,8 @@ const SchemeQuery: React.FC = ({ variant="caption" className="text-gray-500 block" > - 用户: {scheme.user} · 时间: {formatTime(scheme.create_time)} + 用户: {creatorName(scheme.username)} · 时间:{" "} + {formatTime(scheme.create_time)} @@ -579,7 +582,7 @@ const SchemeQuery: React.FC = ({ 用户: - {scheme.user} + {creatorName(scheme.username)} diff --git a/src/components/olmap/FlushingAnalysis/types.ts b/src/components/olmap/FlushingAnalysis/types.ts index 776bcad..ec37ce6 100644 --- a/src/components/olmap/FlushingAnalysis/types.ts +++ b/src/components/olmap/FlushingAnalysis/types.ts @@ -9,7 +9,7 @@ export interface SchemeRecord { id: number; schemeName: string; type: string; - user: string; + username: string; create_time: string; startTime: string; // 详情信息 diff --git a/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx b/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx index 58ba297..6e59fc5 100644 --- a/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx +++ b/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx @@ -37,6 +37,7 @@ import VectorSource from "ol/source/Vector"; import { Style, Icon, Circle, Fill, Stroke } from "ol/style"; import Feature, { FeatureLike } from "ol/Feature"; import { bbox, featureCollection } from "@turf/turf"; +import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName"; interface SchemeRecord { id: number; @@ -96,6 +97,7 @@ const SchemeQuery: React.FC = ({ const { queryAll, queryDate, expandedId } = queryState; const [internalSchemes, setInternalSchemes] = useState([]); const [loading, setLoading] = useState(false); + const creatorName = useSchemeCreatorName(); const { open } = useNotification(); const map = useMap(); @@ -399,8 +401,9 @@ const SchemeQuery: React.FC = ({ variant="caption" className="text-gray-500 block" > - 最小半径: {scheme.minDiameter} · 用户: {scheme.user} · - 日期: {formatShortDate(scheme.create_time)} + 最小半径: {scheme.minDiameter} · 用户:{" "} + {creatorName(scheme.user)} · 日期:{" "} + {formatShortDate(scheme.create_time)} {/* 操作按钮 */} @@ -490,7 +493,7 @@ const SchemeQuery: React.FC = ({ variant="caption" className="font-medium text-gray-900" > - {scheme.user} + {creatorName(scheme.user)} diff --git a/src/components/olmap/core/useSchemeCreatorName.test.ts b/src/components/olmap/core/useSchemeCreatorName.test.ts new file mode 100644 index 0000000..c303698 --- /dev/null +++ b/src/components/olmap/core/useSchemeCreatorName.test.ts @@ -0,0 +1,31 @@ +import { resolveSchemeCreatorName } from "./useSchemeCreatorName"; + +describe("resolveSchemeCreatorName", () => { + it("shows the current viewer's display name for their own scheme", () => { + expect( + resolveSchemeCreatorName("tjwater", { + username: "tjwater", + name: "Test Account", + }), + ).toBe("Test Account"); + }); + + it("keeps the stored username for another user's scheme", () => { + expect( + resolveSchemeCreatorName("operator", { + username: "tjwater", + name: "Test Account", + }), + ).toBe("operator"); + }); + + it("falls back safely when identity data is incomplete", () => { + expect( + resolveSchemeCreatorName("tjwater", { + username: "tjwater", + name: " ", + }), + ).toBe("tjwater"); + expect(resolveSchemeCreatorName(undefined, undefined)).toBe("-"); + }); +}); diff --git a/src/components/olmap/core/useSchemeCreatorName.ts b/src/components/olmap/core/useSchemeCreatorName.ts new file mode 100644 index 0000000..6dc1dcf --- /dev/null +++ b/src/components/olmap/core/useSchemeCreatorName.ts @@ -0,0 +1,29 @@ +import { useGetIdentity } from "@refinedev/core"; +import { useCallback } from "react"; + +export interface SchemeViewerIdentity { + name?: string | null; + username?: string | null; +} + +export const resolveSchemeCreatorName = ( + schemeUsername: string | null | undefined, + viewer: SchemeViewerIdentity | null | undefined, +) => { + const username = schemeUsername?.trim(); + if (!username) return "-"; + + const viewerUsername = viewer?.username?.trim(); + const viewerName = viewer?.name?.trim(); + return viewerUsername === username && viewerName ? viewerName : username; +}; + +export const useSchemeCreatorName = () => { + const { data: viewer } = useGetIdentity(); + + return useCallback( + (schemeUsername: string | null | undefined) => + resolveSchemeCreatorName(schemeUsername, viewer), + [viewer], + ); +}; diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts index b67a3a9..e2a0580 100644 --- a/src/types/next-auth.d.ts +++ b/src/types/next-auth.d.ts @@ -7,6 +7,7 @@ declare module "next-auth" { error?: "RefreshAccessTokenError"; user?: { id?: string; + username?: string; name?: string | null; email?: string | null; image?: string | null; @@ -15,12 +16,14 @@ declare module "next-auth" { interface User { id?: string; + username?: string; } } declare module "next-auth/jwt" { interface JWT { sub?: string; + username?: string; accessToken?: string; refreshToken?: string; accessTokenExpires?: number;