Add a login-visible user directory API and admin/self profile update paths so project duty selectors stop falling back to "用户 N", and so users can persist display fields with an admin-edit tip.
238 lines
8.7 KiB
TypeScript
238 lines
8.7 KiB
TypeScript
import { useEffect, type Dispatch, type MutableRefObject, type SetStateAction } from 'react';
|
|
import { authClient } from '../api/auth';
|
|
import { fetchChatSessions } from '../api/chatSessions';
|
|
import { fetchProjects } from '../api/projectWorkflow';
|
|
import { loginMemberSyncIds, RequestEpoch, seedViewerMembership } from '../data/initFanout';
|
|
import { mergeSessionUser } from '../lib/mergeSessionUser';
|
|
import { mapServerProjectToProjectInfo, mergeProjectLists } from '../lib/projectAdapter';
|
|
import { mapServerSessionToProjectSession, mergeSessionLists } from '../lib/sessionAdapter';
|
|
import type { LoginResult, SystemAuthConfig } from '../types/auth';
|
|
import type { UserProfile } from '../types/docgov';
|
|
import type { ProjectInfo, ProjectSession } from '../types/workbench';
|
|
|
|
export interface AuthenticatedWorkspaceSyncInput {
|
|
allUsers: UserProfile[];
|
|
setAllUsers: Dispatch<SetStateAction<UserProfile[]>>;
|
|
setCurrentUser: Dispatch<SetStateAction<UserProfile>>;
|
|
setSession: Dispatch<SetStateAction<LoginResult | null>>;
|
|
setSessionValidated: Dispatch<SetStateAction<boolean | null>>;
|
|
setSystemEnv: Dispatch<SetStateAction<string>>;
|
|
setSystemAuthConfig: Dispatch<SetStateAction<SystemAuthConfig>>;
|
|
setCapabilities: Dispatch<SetStateAction<string[]>>;
|
|
session: LoginResult | null;
|
|
sessionValidated: boolean | null;
|
|
currentUser: UserProfile;
|
|
currentProjectId: number;
|
|
syncProjectMembersFromServer: (projectId: number, creatorId: number) => Promise<unknown>;
|
|
setProjects: Dispatch<SetStateAction<ProjectInfo[]>>;
|
|
setProjectSyncNotice: Dispatch<SetStateAction<string | null>>;
|
|
projectMembersModalOpen: boolean;
|
|
projectsRef: MutableRefObject<ProjectInfo[]>;
|
|
setSessions: Dispatch<SetStateAction<ProjectSession[]>>;
|
|
setSessionSyncNotice: Dispatch<SetStateAction<string | null>>;
|
|
sessionEpochRef: MutableRefObject<RequestEpoch>;
|
|
}
|
|
|
|
/**
|
|
* 已登录工作台的启动同步:校验会话、拉项目、按需同步当前项目成员、拉会话列表。
|
|
* 首次校验故意只跑一次,沿用入口组件原来的空依赖,避免用户目录更新把登录态重放一遍。
|
|
*/
|
|
export function useAuthenticatedWorkspaceSync(input: AuthenticatedWorkspaceSyncInput): void {
|
|
const {
|
|
allUsers,
|
|
setAllUsers,
|
|
setCurrentUser,
|
|
setSession,
|
|
setSessionValidated,
|
|
setSystemEnv,
|
|
setSystemAuthConfig,
|
|
setCapabilities,
|
|
session,
|
|
sessionValidated,
|
|
currentUser,
|
|
currentProjectId,
|
|
syncProjectMembersFromServer,
|
|
setProjects,
|
|
setProjectSyncNotice,
|
|
projectMembersModalOpen,
|
|
projectsRef,
|
|
setSessions,
|
|
setSessionSyncNotice,
|
|
sessionEpochRef,
|
|
} = input;
|
|
|
|
useEffect(() => {
|
|
authClient.getSystemAuthConfig().then(setSystemAuthConfig).catch(() => {});
|
|
const saved = authClient.loadSavedSession();
|
|
if (saved) {
|
|
setSession(saved);
|
|
if (saved.env) setSystemEnv(saved.env);
|
|
setCapabilities(saved.capabilities ?? []);
|
|
const match = allUsers.find((u) => u.id === saved.user.id);
|
|
setCurrentUser(mergeSessionUser(saved.user, match));
|
|
void authClient
|
|
.fetchMe()
|
|
.then((me) => {
|
|
setCapabilities(me.capabilities ?? []);
|
|
setCurrentUser(mergeSessionUser(me.user, match));
|
|
// Rewrite session from /auth/me so a polluted local mustChangePassword
|
|
// (nested user flag) cannot survive refresh after a successful change.
|
|
const nextSession = {
|
|
...saved,
|
|
user: { ...saved.user, ...me.user, mustChangePassword: me.mustChangePassword },
|
|
mustChangePassword: me.mustChangePassword,
|
|
env: me.env || saved.env,
|
|
capabilities: me.capabilities ?? saved.capabilities ?? [],
|
|
};
|
|
setSession(nextSession);
|
|
authClient.saveSession(nextSession);
|
|
setSessionValidated(true);
|
|
})
|
|
.catch(() => {
|
|
authClient.clearLocalSession();
|
|
setSession(null);
|
|
setCapabilities([]);
|
|
setSessionValidated(false);
|
|
});
|
|
} else {
|
|
setSessionValidated(false);
|
|
}
|
|
// 与原入口一致:只在挂载时恢复本地会话。
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
// 会话校验通过后拉取全员轻量目录(普通用户可用,修复负责人选择器「用户 N」兜底)。
|
|
// 独立 effect + 不依赖 allUsers,避免与项目同步 effect 循环触发。
|
|
useEffect(() => {
|
|
if (!session || sessionValidated !== true) return;
|
|
let cancelled = false;
|
|
void (async () => {
|
|
try {
|
|
const directory = await authClient.fetchUserDirectory();
|
|
if (cancelled) return;
|
|
setAllUsers(
|
|
directory.map((u) => ({
|
|
id: u.id,
|
|
name: u.name,
|
|
employeeId: u.employeeId || '',
|
|
department: u.department || '',
|
|
position: u.position || '',
|
|
roleId: u.roleId,
|
|
avatar: u.avatar,
|
|
})),
|
|
);
|
|
} catch (err) {
|
|
if (cancelled) return;
|
|
console.warn('[users] 用户目录同步失败,继续使用本地缓存:', err);
|
|
}
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [session, sessionValidated, setAllUsers]);
|
|
|
|
useEffect(() => {
|
|
if (!session || sessionValidated !== true) {
|
|
setProjectSyncNotice(null);
|
|
return;
|
|
}
|
|
|
|
let cancelled = false;
|
|
void (async () => {
|
|
try {
|
|
const serverProjects = await fetchProjects();
|
|
if (cancelled) return;
|
|
setProjects((prev) => {
|
|
const mapped = (serverProjects ?? []).map((project) =>
|
|
seedViewerMembership(
|
|
mapServerProjectToProjectInfo(
|
|
project,
|
|
allUsers,
|
|
prev.find((item) => item.id === project.id)
|
|
),
|
|
currentUser.id,
|
|
currentUser.name
|
|
)
|
|
);
|
|
return mergeProjectLists(prev, mapped);
|
|
});
|
|
const syncIds = loginMemberSyncIds(
|
|
(serverProjects ?? []).map((project) => project.id),
|
|
currentProjectId
|
|
);
|
|
await Promise.all(
|
|
syncIds.map((id) => {
|
|
const project = serverProjects.find((item) => item.id === id);
|
|
if (!project) return Promise.resolve(null);
|
|
return syncProjectMembersFromServer(project.id, project.creator_user_id);
|
|
})
|
|
);
|
|
setProjectSyncNotice(null);
|
|
} catch (err) {
|
|
if (cancelled) return;
|
|
console.warn('[projects] 服务端项目列表同步失败,继续使用本地缓存:', err);
|
|
setProjectSyncNotice('项目列表暂用本地缓存:无法连接服务端,新建项目可能无法在其他设备同步。');
|
|
}
|
|
})();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [
|
|
session,
|
|
sessionValidated,
|
|
allUsers,
|
|
currentUser.id,
|
|
currentUser.name,
|
|
currentProjectId,
|
|
syncProjectMembersFromServer,
|
|
setProjects,
|
|
setProjectSyncNotice,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (!projectMembersModalOpen || !currentProjectId || !session || sessionValidated !== true) return;
|
|
const project = projectsRef.current.find((item) => item.id === currentProjectId);
|
|
if (!project) return;
|
|
void syncProjectMembersFromServer(project.id, project.creatorId);
|
|
}, [projectMembersModalOpen, currentProjectId, session, sessionValidated, syncProjectMembersFromServer, projectsRef]);
|
|
|
|
useEffect(() => {
|
|
if (!session || sessionValidated !== true || !currentUser.id) {
|
|
setSessionSyncNotice(null);
|
|
return;
|
|
}
|
|
|
|
const abort = new AbortController();
|
|
const epoch = sessionEpochRef.current.next();
|
|
void (async () => {
|
|
try {
|
|
const serverSessions = await fetchChatSessions(currentUser.id, undefined, 100, abort.signal);
|
|
if (abort.signal.aborted || !sessionEpochRef.current.isCurrent(epoch)) return;
|
|
const knownProjects = projectsRef.current;
|
|
setSessions((prev) => {
|
|
const mapped = (serverSessions ?? []).map((item) =>
|
|
mapServerSessionToProjectSession(
|
|
item,
|
|
knownProjects,
|
|
prev.find((sessionItem) => sessionItem.sessionId === item.session_id)
|
|
)
|
|
);
|
|
return mergeSessionLists(prev, mapped);
|
|
});
|
|
setSessionSyncNotice(null);
|
|
} catch (err) {
|
|
if (abort.signal.aborted || !sessionEpochRef.current.isCurrent(epoch)) return;
|
|
console.warn('[sessions] 服务端会话列表同步失败,继续使用本地缓存:', err);
|
|
setSessionSyncNotice(
|
|
'会话列表暂用本地缓存:无法连接服务端,新建会话可能无法在其他设备同步。'
|
|
);
|
|
}
|
|
})();
|
|
|
|
return () => {
|
|
abort.abort();
|
|
};
|
|
}, [session, sessionValidated, currentUser.id, setSessions, setSessionSyncNotice, sessionEpochRef, projectsRef]);
|
|
}
|