fix(auth): unify scheme creator display

This commit is contained in:
2026-07-30 14:21:20 +08:00
parent 1d7e07174f
commit 8836549697
17 changed files with 106 additions and 18 deletions
+1
View File
@@ -146,6 +146,7 @@ const App = (props: React.PropsWithChildren<AppProps>) => {
const { user } = data; const { user } = data;
return { return {
id: user.id, id: user.id,
username: user.username,
name: user.name, name: user.name,
avatar: user.image, avatar: user.image,
}; };
+10
View File
@@ -66,6 +66,7 @@ const authOptions: NextAuthOptions = {
profile(profile) { profile(profile) {
return { return {
id: profile.sub, id: profile.sub,
username: profile.preferred_username,
name: profile.name ?? profile.preferred_username, name: profile.name ?? profile.preferred_username,
email: profile.email, email: profile.email,
image: Avatar.src, image: Avatar.src,
@@ -79,6 +80,12 @@ const authOptions: NextAuthOptions = {
if (profile?.sub) { if (profile?.sub) {
token.sub = 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) {
if (account.access_token) { if (account.access_token) {
@@ -104,6 +111,9 @@ const authOptions: NextAuthOptions = {
if (session.user && token.sub) { if (session.user && token.sub) {
session.user.id = token.sub; session.user.id = token.sub;
} }
if (session.user && token.username) {
session.user.username = token.username;
}
if (token.accessToken) { if (token.accessToken) {
session.accessToken = token.accessToken; session.accessToken = token.accessToken;
} }
@@ -42,6 +42,7 @@ import {
BurstSchemeRecord, BurstSchemeRecord,
} from "./types"; } from "./types";
import { FLOW_DISPLAY_UNIT, toM3h } from "@utils/units"; import { FLOW_DISPLAY_UNIT, toM3h } from "@utils/units";
import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName";
interface Props { interface Props {
onViewResult: (result: BurstLocationResult) => void; onViewResult: (result: BurstLocationResult) => void;
@@ -86,6 +87,7 @@ const SchemeQuery: React.FC<Props> = ({
const simulationBurstIdsByName = queryState.simulationBurstIdsByName ?? {}; const simulationBurstIdsByName = queryState.simulationBurstIdsByName ?? {};
const [internalSchemes, setInternalSchemes] = useState<BurstSchemeRecord[]>([]); const [internalSchemes, setInternalSchemes] = useState<BurstSchemeRecord[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const creatorName = useSchemeCreatorName();
const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes; const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes;
const setSchemes = onSchemesChange || setInternalSchemes; const setSchemes = onSchemesChange || setInternalSchemes;
const sortedSchemes = useMemo( const sortedSchemes = useMemo(
@@ -526,7 +528,7 @@ const SchemeQuery: React.FC<Props> = ({
: :
</Typography> </Typography>
<Typography variant="caption" className="font-medium text-gray-900"> <Typography variant="caption" className="font-medium text-gray-900">
{scheme.username || "-"} {creatorName(scheme.username)}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
@@ -21,7 +21,7 @@ const scheme: SchemeRecord = {
id: 17, id: 17,
schemeName: "burst-report-demo", schemeName: "burst-report-demo",
type: "burst_analysis", type: "burst_analysis",
user: "operator", username: "operator",
create_time: "2026-07-30T08:00:00+08:00", create_time: "2026-07-30T08:00:00+08:00",
startTime: "2026-07-30T09:00:00+08:00", startTime: "2026-07-30T09:00:00+08:00",
schemeDetail: { schemeDetail: {
@@ -255,7 +255,7 @@ const ReportDocument: React.FC<ReportDocumentProps> = ({
{[ {[
["管网", NETWORK_NAME], ["管网", NETWORK_NAME],
["方案名称", scheme.schemeName], ["方案名称", scheme.schemeName],
["方案创建人", scheme.user || "未记录"], ["方案创建人", scheme.username || "未记录"],
["方案创建时间", formatDateTime(scheme.create_time)], ["方案创建时间", formatDateTime(scheme.create_time)],
["模拟开始时间", formatDateTime(scheme.startTime)], ["模拟开始时间", formatDateTime(scheme.startTime)],
["模拟持续时间", formatDuration(duration)], ["模拟持续时间", formatDuration(duration)],
@@ -18,7 +18,7 @@ jest.mock("./SchemeQuery", () => ({
id, id,
schemeName, schemeName,
type: "burst_analysis", type: "burst_analysis",
user: "operator", username: "operator",
create_time: "2026-07-30T08:00:00+08:00", create_time: "2026-07-30T08:00:00+08:00",
startTime: "2026-07-30T09:00:00+08:00", startTime: "2026-07-30T09:00:00+08:00",
schemeDetail: { schemeDetail: {
@@ -56,6 +56,7 @@ import {
getPipeDiameterDisplay, getPipeDiameterDisplay,
type PipeDiameterMap, type PipeDiameterMap,
} from "./schemePipeDiameters"; } from "./schemePipeDiameters";
import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName";
interface SchemeQueryProps { interface SchemeQueryProps {
schemes?: SchemeRecord[]; schemes?: SchemeRecord[];
@@ -113,6 +114,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
const [internalSchemes, setInternalSchemes] = useState<SchemeRecord[]>([]); const [internalSchemes, setInternalSchemes] = useState<SchemeRecord[]>([]);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const creatorName = useSchemeCreatorName();
const [mapContainer, setMapContainer] = useState<HTMLElement | null>(null); // 地图容器元素 const [mapContainer, setMapContainer] = useState<HTMLElement | null>(null); // 地图容器元素
const [pipeDiametersByScheme, setPipeDiametersByScheme] = useState< const [pipeDiametersByScheme, setPipeDiametersByScheme] = useState<
Record<number, PipeDiameterMap> Record<number, PipeDiameterMap>
@@ -177,7 +179,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
id: item.scheme_id, id: item.scheme_id,
schemeName: item.scheme_name, schemeName: item.scheme_name,
type: item.scheme_type, type: item.scheme_type,
user: item.username, username: item.username,
create_time: item.create_time, create_time: item.create_time,
startTime: item.scheme_start_time, startTime: item.scheme_start_time,
schemeDetail: item.scheme_detail, schemeDetail: item.scheme_detail,
@@ -723,7 +725,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
variant="caption" variant="caption"
className="font-medium text-gray-900" className="font-medium text-gray-900"
> >
{scheme.user} {creatorName(scheme.username)}
</Typography> </Typography>
</Box> </Box>
<Box className="flex items-center gap-2"> <Box className="flex items-center gap-2">
@@ -11,7 +11,7 @@ export interface SchemeRecord {
id: number; id: number;
schemeName: string; schemeName: string;
type: string; type: string;
user: string; username: string;
create_time: string; create_time: string;
startTime: string; startTime: string;
// 详情信息 // 详情信息
@@ -40,6 +40,7 @@ import Feature from "ol/Feature";
import { bbox, featureCollection } from "@turf/turf"; import { bbox, featureCollection } from "@turf/turf";
import Timeline from "@components/olmap/core/Controls/Timeline"; import Timeline from "@components/olmap/core/Controls/Timeline";
import { ContaminantSchemaItem, ContaminantSchemeRecord } from "./types"; import { ContaminantSchemaItem, ContaminantSchemeRecord } from "./types";
import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName";
interface SchemeQueryProps { interface SchemeQueryProps {
schemes?: ContaminantSchemeRecord[]; schemes?: ContaminantSchemeRecord[];
@@ -101,6 +102,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
>([]); >([]);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [mapContainer, setMapContainer] = useState<HTMLElement | null>(null); const [mapContainer, setMapContainer] = useState<HTMLElement | null>(null);
const creatorName = useSchemeCreatorName();
const { open } = useNotification(); const { open } = useNotification();
const map = useMap(); const map = useMap();
@@ -242,7 +244,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
id: item.scheme_id, id: item.scheme_id,
schemeName: item.scheme_name, schemeName: item.scheme_name,
type: item.scheme_type, type: item.scheme_type,
user: item.username, username: item.username,
create_time: item.create_time, create_time: item.create_time,
startTime: item.scheme_start_time, startTime: item.scheme_start_time,
schemeDetail: item.scheme_detail, schemeDetail: item.scheme_detail,
@@ -598,7 +600,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
variant="caption" variant="caption"
className="font-medium text-gray-900" className="font-medium text-gray-900"
> >
{scheme.user} {creatorName(scheme.username)}
</Typography> </Typography>
</Box> </Box>
<Box className="flex items-center gap-2"> <Box className="flex items-center gap-2">
@@ -9,7 +9,7 @@ export interface ContaminantSchemeRecord {
id: number; id: number;
schemeName: string; schemeName: string;
type: string; type: string;
user: string; username: string;
create_time: string; create_time: string;
startTime: string; startTime: string;
schemeDetail?: ContaminantSchemeDetail; schemeDetail?: ContaminantSchemeDetail;
@@ -26,6 +26,7 @@ import { NETWORK_NAME, config } from "@config/config";
import { useControllableObjectState } from "@components/olmap/core/useControllableState"; import { useControllableObjectState } from "@components/olmap/core/useControllableState";
import { LeakageResultDetail, LeakageSchemeRecord } from "./types"; import { LeakageResultDetail, LeakageSchemeRecord } from "./types";
import { FLOW_DISPLAY_UNIT, toM3h } from "@utils/units"; import { FLOW_DISPLAY_UNIT, toM3h } from "@utils/units";
import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName";
interface Props { interface Props {
onViewResult: (result: LeakageResultDetail) => void; onViewResult: (result: LeakageResultDetail) => void;
@@ -63,6 +64,7 @@ const SchemeQuery: React.FC<Props> = ({
const { queryAll, queryDate, expandedId } = queryState; const { queryAll, queryDate, expandedId } = queryState;
const [internalSchemes, setInternalSchemes] = useState<LeakageSchemeRecord[]>([]); const [internalSchemes, setInternalSchemes] = useState<LeakageSchemeRecord[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const creatorName = useSchemeCreatorName();
const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes; const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes;
const setSchemes = onSchemesChange || setInternalSchemes; const setSchemes = onSchemesChange || setInternalSchemes;
const sortedSchemes = useMemo( const sortedSchemes = useMemo(
@@ -265,7 +267,7 @@ const SchemeQuery: React.FC<Props> = ({
: :
</Typography> </Typography>
<Typography variant="caption" className="font-medium text-gray-900"> <Typography variant="caption" className="font-medium text-gray-900">
{scheme.username || "-"} {creatorName(scheme.username)}
</Typography> </Typography>
</Box> </Box>
<Box className="grid grid-cols-[78px_1fr] items-center gap-x-2"> <Box className="grid grid-cols-[78px_1fr] items-center gap-x-2">
@@ -42,6 +42,7 @@ import { bbox, featureCollection } from "@turf/turf";
import Timeline from "@components/olmap/core/Controls/Timeline"; import Timeline from "@components/olmap/core/Controls/Timeline";
import { SchemeRecord, SchemaItem } from "./types"; import { SchemeRecord, SchemaItem } from "./types";
import { FLOW_DISPLAY_UNIT } from "@utils/units"; import { FLOW_DISPLAY_UNIT } from "@utils/units";
import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName";
interface SchemeQueryProps { interface SchemeQueryProps {
schemes?: SchemeRecord[]; schemes?: SchemeRecord[];
@@ -100,6 +101,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]); const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
const [mapContainer, setMapContainer] = useState<HTMLElement | null>(null); const [mapContainer, setMapContainer] = useState<HTMLElement | null>(null);
const creatorName = useSchemeCreatorName();
const { open } = useNotification(); const { open } = useNotification();
const map = useMap(); const map = useMap();
@@ -290,7 +292,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
id: item.scheme_id, id: item.scheme_id,
schemeName: item.scheme_name, schemeName: item.scheme_name,
type: item.scheme_type, type: item.scheme_type,
user: item.username, username: item.username,
create_time: item.create_time, create_time: item.create_time,
startTime: item.scheme_start_time, startTime: item.scheme_start_time,
schemeDetail: item.scheme_detail, schemeDetail: item.scheme_detail,
@@ -479,7 +481,8 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
variant="caption" variant="caption"
className="text-gray-500 block" className="text-gray-500 block"
> >
: {scheme.user} · : {formatTime(scheme.create_time)} : {creatorName(scheme.username)} · :{" "}
{formatTime(scheme.create_time)}
</Typography> </Typography>
</Box> </Box>
<Box className="flex gap-1 ml-2"> <Box className="flex gap-1 ml-2">
@@ -579,7 +582,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
: :
</Typography> </Typography>
<Typography variant="caption" className="font-medium text-gray-900"> <Typography variant="caption" className="font-medium text-gray-900">
{scheme.user} {creatorName(scheme.username)}
</Typography> </Typography>
</Box> </Box>
@@ -9,7 +9,7 @@ export interface SchemeRecord {
id: number; id: number;
schemeName: string; schemeName: string;
type: string; type: string;
user: string; username: string;
create_time: string; create_time: string;
startTime: string; startTime: string;
// 详情信息 // 详情信息
@@ -37,6 +37,7 @@ import VectorSource from "ol/source/Vector";
import { Style, Icon, Circle, Fill, Stroke } from "ol/style"; import { Style, Icon, Circle, Fill, Stroke } from "ol/style";
import Feature, { FeatureLike } from "ol/Feature"; import Feature, { FeatureLike } from "ol/Feature";
import { bbox, featureCollection } from "@turf/turf"; import { bbox, featureCollection } from "@turf/turf";
import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName";
interface SchemeRecord { interface SchemeRecord {
id: number; id: number;
@@ -96,6 +97,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
const { queryAll, queryDate, expandedId } = queryState; const { queryAll, queryDate, expandedId } = queryState;
const [internalSchemes, setInternalSchemes] = useState<SchemeRecord[]>([]); const [internalSchemes, setInternalSchemes] = useState<SchemeRecord[]>([]);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const creatorName = useSchemeCreatorName();
const { open } = useNotification(); const { open } = useNotification();
const map = useMap(); const map = useMap();
@@ -399,8 +401,9 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
variant="caption" variant="caption"
className="text-gray-500 block" className="text-gray-500 block"
> >
: {scheme.minDiameter} · : {scheme.user} · : {scheme.minDiameter} · :{" "}
: {formatShortDate(scheme.create_time)} {creatorName(scheme.user)} · :{" "}
{formatShortDate(scheme.create_time)}
</Typography> </Typography>
</Box> </Box>
{/* 操作按钮 */} {/* 操作按钮 */}
@@ -490,7 +493,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
variant="caption" variant="caption"
className="font-medium text-gray-900" className="font-medium text-gray-900"
> >
{scheme.user} {creatorName(scheme.user)}
</Typography> </Typography>
</Box> </Box>
<Box className="flex items-center gap-2"> <Box className="flex items-center gap-2">
@@ -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("-");
});
});
@@ -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<SchemeViewerIdentity>();
return useCallback(
(schemeUsername: string | null | undefined) =>
resolveSchemeCreatorName(schemeUsername, viewer),
[viewer],
);
};
+3
View File
@@ -7,6 +7,7 @@ declare module "next-auth" {
error?: "RefreshAccessTokenError"; error?: "RefreshAccessTokenError";
user?: { user?: {
id?: string; id?: string;
username?: string;
name?: string | null; name?: string | null;
email?: string | null; email?: string | null;
image?: string | null; image?: string | null;
@@ -15,12 +16,14 @@ declare module "next-auth" {
interface User { interface User {
id?: string; id?: string;
username?: string;
} }
} }
declare module "next-auth/jwt" { declare module "next-auth/jwt" {
interface JWT { interface JWT {
sub?: string; sub?: string;
username?: string;
accessToken?: string; accessToken?: string;
refreshToken?: string; refreshToken?: string;
accessTokenExpires?: number; accessTokenExpires?: number;