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;
return {
id: user.id,
username: user.username,
name: user.name,
avatar: user.image,
};
+10
View File
@@ -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;
}
@@ -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<Props> = ({
const simulationBurstIdsByName = queryState.simulationBurstIdsByName ?? {};
const [internalSchemes, setInternalSchemes] = useState<BurstSchemeRecord[]>([]);
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<Props> = ({
:
</Typography>
<Typography variant="caption" className="font-medium text-gray-900">
{scheme.username || "-"}
{creatorName(scheme.username)}
</Typography>
</Box>
</Box>
@@ -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: {
@@ -255,7 +255,7 @@ const ReportDocument: React.FC<ReportDocumentProps> = ({
{[
["管网", NETWORK_NAME],
["方案名称", scheme.schemeName],
["方案创建人", scheme.user || "未记录"],
["方案创建人", scheme.username || "未记录"],
["方案创建时间", formatDateTime(scheme.create_time)],
["模拟开始时间", formatDateTime(scheme.startTime)],
["模拟持续时间", formatDuration(duration)],
@@ -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: {
@@ -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<SchemeQueryProps> = ({
const [internalSchemes, setInternalSchemes] = useState<SchemeRecord[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const creatorName = useSchemeCreatorName();
const [mapContainer, setMapContainer] = useState<HTMLElement | null>(null); // 地图容器元素
const [pipeDiametersByScheme, setPipeDiametersByScheme] = useState<
Record<number, PipeDiameterMap>
@@ -177,7 +179,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
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<SchemeQueryProps> = ({
variant="caption"
className="font-medium text-gray-900"
>
{scheme.user}
{creatorName(scheme.username)}
</Typography>
</Box>
<Box className="flex items-center gap-2">
@@ -11,7 +11,7 @@ export interface SchemeRecord {
id: number;
schemeName: string;
type: string;
user: string;
username: string;
create_time: string;
startTime: string;
// 详情信息
@@ -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<SchemeQueryProps> = ({
>([]);
const [loading, setLoading] = useState<boolean>(false);
const [mapContainer, setMapContainer] = useState<HTMLElement | null>(null);
const creatorName = useSchemeCreatorName();
const { open } = useNotification();
const map = useMap();
@@ -242,7 +244,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
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<SchemeQueryProps> = ({
variant="caption"
className="font-medium text-gray-900"
>
{scheme.user}
{creatorName(scheme.username)}
</Typography>
</Box>
<Box className="flex items-center gap-2">
@@ -9,7 +9,7 @@ export interface ContaminantSchemeRecord {
id: number;
schemeName: string;
type: string;
user: string;
username: string;
create_time: string;
startTime: string;
schemeDetail?: ContaminantSchemeDetail;
@@ -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<Props> = ({
const { queryAll, queryDate, expandedId } = queryState;
const [internalSchemes, setInternalSchemes] = useState<LeakageSchemeRecord[]>([]);
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<Props> = ({
:
</Typography>
<Typography variant="caption" className="font-medium text-gray-900">
{scheme.username || "-"}
{creatorName(scheme.username)}
</Typography>
</Box>
<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 { 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<SchemeQueryProps> = ({
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
const [mapContainer, setMapContainer] = useState<HTMLElement | null>(null);
const creatorName = useSchemeCreatorName();
const { open } = useNotification();
const map = useMap();
@@ -290,7 +292,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
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<SchemeQueryProps> = ({
variant="caption"
className="text-gray-500 block"
>
: {scheme.user} · : {formatTime(scheme.create_time)}
: {creatorName(scheme.username)} · :{" "}
{formatTime(scheme.create_time)}
</Typography>
</Box>
<Box className="flex gap-1 ml-2">
@@ -579,7 +582,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
:
</Typography>
<Typography variant="caption" className="font-medium text-gray-900">
{scheme.user}
{creatorName(scheme.username)}
</Typography>
</Box>
@@ -9,7 +9,7 @@ export interface SchemeRecord {
id: number;
schemeName: string;
type: string;
user: string;
username: string;
create_time: string;
startTime: string;
// 详情信息
@@ -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<SchemeQueryProps> = ({
const { queryAll, queryDate, expandedId } = queryState;
const [internalSchemes, setInternalSchemes] = useState<SchemeRecord[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const creatorName = useSchemeCreatorName();
const { open } = useNotification();
const map = useMap();
@@ -399,8 +401,9 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
variant="caption"
className="text-gray-500 block"
>
: {scheme.minDiameter} · : {scheme.user} ·
: {formatShortDate(scheme.create_time)}
: {scheme.minDiameter} · :{" "}
{creatorName(scheme.user)} · :{" "}
{formatShortDate(scheme.create_time)}
</Typography>
</Box>
{/* 操作按钮 */}
@@ -490,7 +493,7 @@ const SchemeQuery: React.FC<SchemeQueryProps> = ({
variant="caption"
className="font-medium text-gray-900"
>
{scheme.user}
{creatorName(scheme.user)}
</Typography>
</Box>
<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";
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;