fix(web): remove identity simulation and stop sending X-User-ID
Product UI no longer switches sessions locally; API clients attach Bearer tokens only and LocalAuthClient stays test-only outside the prod bundle.
This commit is contained in:
+6
-59
@@ -46,7 +46,6 @@ import { FilePreviewPanel } from './components/doc/preview/FilePreviewPanel';
|
||||
import { ShieldAlert } from 'lucide-react';
|
||||
import { LoginView } from './components/auth/LoginView';
|
||||
import { FirstLoginPasswordModal } from './components/auth/FirstLoginPasswordModal';
|
||||
import { DevRoleSwitcher } from './components/dev/DevRoleSwitcher';
|
||||
import { authClient } from './api/auth';
|
||||
import { streamChat, mapErrorInfo } from './api/chatStream';
|
||||
import { streamTask, buildTaskRequest, buildResumeRequest } from './api/taskStream';
|
||||
@@ -120,7 +119,6 @@ import {
|
||||
saveUsers,
|
||||
migrateAndClearLegacyMessages,
|
||||
resolveActiveSessionForUser,
|
||||
ensurePrivateSessionsForUser,
|
||||
loadProjects,
|
||||
saveProjects,
|
||||
filterProjectSessionsForUser,
|
||||
@@ -156,7 +154,6 @@ import { subscribeTaskRunEvents, createTaskRun, cancelTaskRun, getTaskRun } from
|
||||
async function readAttachmentsAsMaterials(
|
||||
attachments: { name: string; type: string; docId?: string; fileRaw?: File | Blob; assetId?: number }[],
|
||||
projectId?: number,
|
||||
userId?: number,
|
||||
): Promise<TaskRunMaterial[]> {
|
||||
const TEXT_EXTS = new Set(['txt', 'md', 'csv', 'json', 'xml', 'yaml', 'yml', 'log']);
|
||||
const results: TaskRunMaterial[] = [];
|
||||
@@ -172,7 +169,7 @@ async function readAttachmentsAsMaterials(
|
||||
if (!projectId || projectId <= 0) {
|
||||
throw new Error(`附件 ${att.name} 需要先登记为项目资产(缺少 project_id)`);
|
||||
}
|
||||
const uploaded = await uploadProjectAsset(projectId, att.fileRaw as Blob, att.name, userId);
|
||||
const uploaded = await uploadProjectAsset(projectId, att.fileRaw as Blob, att.name);
|
||||
results.push({ filename: uploaded.filename || att.name, asset_id: uploaded.asset_id });
|
||||
continue;
|
||||
}
|
||||
@@ -643,7 +640,7 @@ export default function App() {
|
||||
|
||||
// 认证与会话状态
|
||||
const [session, setSession] = useState<LoginResult | null>(() => authClient.loadSavedSession());
|
||||
const [systemEnv, setSystemEnv] = useState<string>('dev');
|
||||
const [systemEnv, setSystemEnv] = useState<string>('');
|
||||
const [systemAuthConfig, setSystemAuthConfig] = useState<SystemAuthConfig>({
|
||||
allowRegistration: true,
|
||||
requireApproval: false,
|
||||
@@ -1249,7 +1246,7 @@ export default function App() {
|
||||
}
|
||||
|
||||
if (id.startsWith('doc-free-') || targetDoc.governanceMode === 'freeform') {
|
||||
fetchFreeformDoc(id, currentUser.id)
|
||||
fetchFreeformDoc(id)
|
||||
.then((detail) => {
|
||||
if (detail && detail.content_markdown) {
|
||||
const fullAST = markdownToBlockAST(detail.content_markdown, {
|
||||
@@ -1382,7 +1379,6 @@ export default function App() {
|
||||
currentProjectId,
|
||||
undefined,
|
||||
message.contentSha256,
|
||||
currentUser.id
|
||||
);
|
||||
resData = resp.data;
|
||||
} catch (serverErr: any) {
|
||||
@@ -1460,8 +1456,8 @@ export default function App() {
|
||||
const fetchDocs = async () => {
|
||||
try {
|
||||
const [privateSummaries, sharedSummaries] = await Promise.all([
|
||||
listFreeformDocs('my_private', currentProjectId, currentUser.id).catch(() => []),
|
||||
listFreeformDocs('project_shared', currentProjectId, currentUser.id).catch(() => []),
|
||||
listFreeformDocs('my_private', currentProjectId).catch(() => []),
|
||||
listFreeformDocs('project_shared', currentProjectId).catch(() => []),
|
||||
]);
|
||||
if (!active) return;
|
||||
const allSummaries = [...privateSummaries, ...sharedSummaries];
|
||||
@@ -2617,7 +2613,6 @@ export default function App() {
|
||||
const materials = await readAttachmentsAsMaterials(
|
||||
processedAttachments || [],
|
||||
baseTask.projectId,
|
||||
baseTask.userId,
|
||||
);
|
||||
const task: TaskRunContext = materials.length > 0 ? { ...baseTask, materials } : baseTask;
|
||||
runTaskAttempt(task, asstId, 'run', 0);
|
||||
@@ -3673,7 +3668,7 @@ export default function App() {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 用户在当前激活项目中的法定职责与权限矩阵弹窗 (支持快速模拟账号切换) */}
|
||||
{/* 用户在当前激活项目中的法定职责与权限矩阵弹窗 */}
|
||||
<UserProjectRightsModal
|
||||
isOpen={rightsModalOpen}
|
||||
onClose={() => setRightsModalOpen(false)}
|
||||
@@ -3681,22 +3676,7 @@ export default function App() {
|
||||
currentProjectTitle={sessions.find((s) => s.sessionId === activeSessionId)?.title || '当前项目'}
|
||||
deliverables={visibleDeliverables}
|
||||
activeDocId={activeDocId}
|
||||
allUsers={allUsers}
|
||||
canAssignLead={canAssignLead}
|
||||
allowDevSwitch={systemEnv === 'dev'}
|
||||
onSwitchUser={(u) => {
|
||||
const fresh = allUsers.find((x) => x.id === u.id) || u;
|
||||
setCurrentUser(fresh);
|
||||
const updatedSession: LoginResult = {
|
||||
token: session?.token || `dev-token-${fresh.id}`,
|
||||
user: fresh as any,
|
||||
mustChangePassword: false,
|
||||
env: session?.env || systemEnv,
|
||||
allowDevSwitch: session?.allowDevSwitch ?? (systemEnv === 'dev'),
|
||||
};
|
||||
setSession(updatedSession);
|
||||
authClient.saveSession(updatedSession);
|
||||
}}
|
||||
onSelectDoc={handleSelectDoc}
|
||||
onRoleChangeForDoc={handleRoleChangeForDoc}
|
||||
/>
|
||||
@@ -3758,39 +3738,6 @@ export default function App() {
|
||||
env={systemEnv}
|
||||
/>
|
||||
|
||||
{/* 研发调试快捷身份切换浮窗 (仅在 DEV 环境生效,生产与测试环境彻底剥离) */}
|
||||
<DevRoleSwitcher
|
||||
env={systemEnv}
|
||||
currentUser={currentUser}
|
||||
allUsers={allUsers}
|
||||
onSwitchUser={(newUser) => {
|
||||
const fresh = allUsers.find((u) => u.id === newUser.id) || newUser;
|
||||
setCurrentUser(fresh);
|
||||
const updatedSession: LoginResult = {
|
||||
token: session?.token || `dev-token-${fresh.id}`,
|
||||
user: fresh as any,
|
||||
mustChangePassword: false,
|
||||
env: session?.env || systemEnv,
|
||||
allowDevSwitch: session?.allowDevSwitch ?? (systemEnv === 'dev'),
|
||||
};
|
||||
setSession(updatedSession);
|
||||
authClient.saveSession(updatedSession);
|
||||
// 确保新用户对已知项目具有独立私聊会话
|
||||
const updatedSessions = ensurePrivateSessionsForUser(sessions, fresh.id);
|
||||
setSessions(updatedSessions);
|
||||
// 切换到目标用户在当前项目的私聊会话
|
||||
const nextActiveId = resolveActiveSessionForUser(
|
||||
updatedSessions,
|
||||
fresh.id,
|
||||
currentProjectId,
|
||||
activeSessionId
|
||||
);
|
||||
if (nextActiveId) {
|
||||
setActiveSessionId(nextActiveId);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 单文档可见性覆盖弹窗 */}
|
||||
<DocVisibilityModal
|
||||
isOpen={docVisibilityModalOpen}
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* 测试专用本地鉴权客户端 — 仅由单元测试 import,不进入生产 bundle 主路径。
|
||||
*/
|
||||
import {
|
||||
UserAccount,
|
||||
SystemAuthConfig,
|
||||
LoginPayload,
|
||||
LoginResult,
|
||||
RegisterPayload,
|
||||
RegisterResult,
|
||||
CreateAdminUserPayload,
|
||||
AccountStatus,
|
||||
} from '../types/auth';
|
||||
import type { AuthClientOptions } from './auth';
|
||||
|
||||
const AUTH_STORAGE_KEY_SESSION = 'smdf_auth_session';
|
||||
|
||||
export class LocalAuthClient {
|
||||
private users: Map<number, UserAccount & { password?: string }> = new Map();
|
||||
private nextId = 10;
|
||||
private config: SystemAuthConfig = { allowRegistration: true, requireApproval: false };
|
||||
private env = 'dev';
|
||||
private adminUsername = 'admin';
|
||||
private adminPassword = 'admin-secret-password-123';
|
||||
|
||||
constructor(options?: AuthClientOptions) {
|
||||
if (options?.env) this.env = options.env;
|
||||
if (options?.adminUsername) this.adminUsername = options.adminUsername;
|
||||
if (options?.adminPassword) this.adminPassword = options.adminPassword;
|
||||
if (options?.allowRegistration !== undefined) this.config.allowRegistration = options.allowRegistration;
|
||||
if (options?.requireApproval !== undefined) this.config.requireApproval = options.requireApproval;
|
||||
this.initDefaultUsers();
|
||||
}
|
||||
|
||||
private initDefaultUsers() {
|
||||
const now = new Date().toISOString();
|
||||
this.users.set(1, {
|
||||
id: 1,
|
||||
username: this.adminUsername,
|
||||
password: this.adminPassword,
|
||||
name: '系统管理员',
|
||||
roleId: 'role-admin',
|
||||
position: '系统管理员',
|
||||
department: '系统运维部',
|
||||
employeeId: 'SYS-000',
|
||||
status: 'active',
|
||||
mustChangePassword: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
|
||||
async login(payload: LoginPayload): Promise<LoginResult> {
|
||||
const trimmedUser = payload.username.trim();
|
||||
let matched: (UserAccount & { password?: string }) | undefined;
|
||||
for (const u of this.users.values()) {
|
||||
if (u.username.toLowerCase() === trimmedUser.toLowerCase()) {
|
||||
matched = u;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched || matched.password !== payload.password) {
|
||||
throw new Error('用户名或密码错误');
|
||||
}
|
||||
if (matched.status === 'pending_approval') {
|
||||
throw new Error('账号正在等待管理员审核中,暂无法登录');
|
||||
}
|
||||
const { password: _, ...safeUser } = matched;
|
||||
const result: LoginResult = {
|
||||
token: `token-${Date.now()}-${safeUser.id}`,
|
||||
user: safeUser,
|
||||
mustChangePassword: !!safeUser.mustChangePassword,
|
||||
env: this.env,
|
||||
};
|
||||
this.saveSession(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async register(payload: RegisterPayload): Promise<RegisterResult> {
|
||||
if (!this.config.allowRegistration) {
|
||||
throw new Error('系统当前已暂停自主注册,请联系系统管理员分配账号');
|
||||
}
|
||||
const trimmedUser = payload.username.trim();
|
||||
if (!trimmedUser || !payload.password || !payload.name?.trim()) {
|
||||
throw new Error('用户名、密码与姓名为必填项');
|
||||
}
|
||||
for (const u of this.users.values()) {
|
||||
if (u.username.toLowerCase() === trimmedUser.toLowerCase()) {
|
||||
throw new Error(`用户名已存在: ${trimmedUser}`);
|
||||
}
|
||||
}
|
||||
const now = new Date().toISOString();
|
||||
const status: AccountStatus = this.config.requireApproval ? 'pending_approval' : 'active';
|
||||
const newId = this.nextId++;
|
||||
this.users.set(newId, {
|
||||
id: newId,
|
||||
username: trimmedUser,
|
||||
password: payload.password,
|
||||
name: payload.name.trim(),
|
||||
roleId: 'role-guest',
|
||||
position: '访客/待指派',
|
||||
department: payload.department || '待分配部门',
|
||||
employeeId: payload.employeeId || `EXT-${newId}`,
|
||||
status,
|
||||
mustChangePassword: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
return {
|
||||
status,
|
||||
message: status === 'pending_approval' ? '注册成功,等待审核' : '注册成功',
|
||||
userId: newId,
|
||||
};
|
||||
}
|
||||
|
||||
async changePassword(userId: number, oldPass: string, newPass: string): Promise<boolean> {
|
||||
const user = this.users.get(userId);
|
||||
if (!user || user.password !== oldPass) throw new Error('原密码不正确');
|
||||
user.password = newPass;
|
||||
user.mustChangePassword = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
async updateSystemAuthConfig(newConfig: SystemAuthConfig): Promise<SystemAuthConfig> {
|
||||
this.config = { ...newConfig };
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
async updateUserStatus(userId: number, status: AccountStatus, roleId?: string, position?: string): Promise<UserAccount> {
|
||||
const user = this.users.get(userId);
|
||||
if (!user) throw new Error('用户不存在');
|
||||
user.status = status;
|
||||
if (roleId) user.roleId = roleId;
|
||||
if (position) user.position = position;
|
||||
const { password: _, ...safe } = user;
|
||||
return safe;
|
||||
}
|
||||
|
||||
async createAdminUser(payload: CreateAdminUserPayload): Promise<UserAccount> {
|
||||
const now = new Date().toISOString();
|
||||
const newId = this.nextId++;
|
||||
const user: UserAccount & { password?: string } = {
|
||||
id: newId,
|
||||
username: payload.username.trim(),
|
||||
password: payload.initialPassword,
|
||||
name: payload.name.trim(),
|
||||
roleId: payload.roleId,
|
||||
position: payload.position,
|
||||
department: payload.department || '研发中心',
|
||||
employeeId: payload.employeeId || `EMP-${newId}`,
|
||||
status: 'active',
|
||||
mustChangePassword: true,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
this.users.set(newId, user);
|
||||
const { password: _, ...safe } = user;
|
||||
return safe;
|
||||
}
|
||||
|
||||
logout() {
|
||||
if (typeof window !== 'undefined' && window.localStorage) {
|
||||
window.localStorage.removeItem(AUTH_STORAGE_KEY_SESSION);
|
||||
}
|
||||
}
|
||||
|
||||
saveSession(session: LoginResult) {
|
||||
if (typeof window !== 'undefined' && window.localStorage) {
|
||||
window.localStorage.setItem(AUTH_STORAGE_KEY_SESSION, JSON.stringify(session));
|
||||
}
|
||||
}
|
||||
|
||||
loadSavedSession(): LoginResult | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
getSessionToken(): string | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function createInMemoryAuthClient(options?: AuthClientOptions): LocalAuthClient {
|
||||
return new LocalAuthClient({ ...options, localFallback: true });
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
createInMemoryAuthClient,
|
||||
} from './auth';
|
||||
import { createInMemoryAuthClient } from './auth.local';
|
||||
|
||||
test('AuthClient: 基础登录与 .env 超级管理员鉴权', async () => {
|
||||
const client = createInMemoryAuthClient({
|
||||
@@ -23,7 +21,7 @@ test('AuthClient: 基础登录与 .env 超级管理员鉴权', async () => {
|
||||
const session = await client.login({ username: 'admin', password: 'admin-password-123' });
|
||||
assert.equal(session.user.username, 'admin');
|
||||
assert.equal(session.user.roleId, 'role-admin');
|
||||
assert.equal(session.allowDevSwitch, true);
|
||||
assert.equal(session.env, 'dev');
|
||||
assert.ok(session.token.length > 0);
|
||||
});
|
||||
|
||||
|
||||
@@ -208,174 +208,4 @@ export class AuthClient {
|
||||
}
|
||||
}
|
||||
|
||||
/** 测试专用本地鉴权客户端(不用于生产构建默认路径) */
|
||||
export class LocalAuthClient {
|
||||
private users: Map<number, UserAccount & { password?: string }> = new Map();
|
||||
private nextId = 10;
|
||||
private config: SystemAuthConfig = { allowRegistration: true, requireApproval: false };
|
||||
private env = 'dev';
|
||||
private adminUsername = 'admin';
|
||||
private adminPassword = 'admin-secret-password-123';
|
||||
|
||||
constructor(options?: AuthClientOptions) {
|
||||
if (options?.env) this.env = options.env;
|
||||
if (options?.adminUsername) this.adminUsername = options.adminUsername;
|
||||
if (options?.adminPassword) this.adminPassword = options.adminPassword;
|
||||
if (options?.allowRegistration !== undefined) this.config.allowRegistration = options.allowRegistration;
|
||||
if (options?.requireApproval !== undefined) this.config.requireApproval = options.requireApproval;
|
||||
this.initDefaultUsers();
|
||||
}
|
||||
|
||||
private initDefaultUsers() {
|
||||
const now = new Date().toISOString();
|
||||
this.users.set(1, {
|
||||
id: 1,
|
||||
username: this.adminUsername,
|
||||
password: this.adminPassword,
|
||||
name: '系统管理员',
|
||||
roleId: 'role-admin',
|
||||
position: '系统管理员',
|
||||
department: '系统运维部',
|
||||
employeeId: 'SYS-000',
|
||||
status: 'active',
|
||||
mustChangePassword: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
|
||||
async login(payload: LoginPayload): Promise<LoginResult> {
|
||||
const trimmedUser = payload.username.trim();
|
||||
let matched: (UserAccount & { password?: string }) | undefined;
|
||||
for (const u of this.users.values()) {
|
||||
if (u.username.toLowerCase() === trimmedUser.toLowerCase()) {
|
||||
matched = u;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched || matched.password !== payload.password) {
|
||||
throw new Error('用户名或密码错误');
|
||||
}
|
||||
if (matched.status === 'pending_approval') {
|
||||
throw new Error('账号正在等待管理员审核中,暂无法登录');
|
||||
}
|
||||
const { password, ...safeUser } = matched;
|
||||
const result: LoginResult = {
|
||||
token: `token-${Date.now()}-${safeUser.id}`,
|
||||
user: safeUser,
|
||||
mustChangePassword: !!safeUser.mustChangePassword,
|
||||
env: this.env,
|
||||
allowDevSwitch: this.env === 'dev',
|
||||
};
|
||||
this.saveSession(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async register(payload: RegisterPayload): Promise<RegisterResult> {
|
||||
if (!this.config.allowRegistration) {
|
||||
throw new Error('系统当前已暂停自主注册,请联系系统管理员分配账号');
|
||||
}
|
||||
const trimmedUser = payload.username.trim();
|
||||
if (!trimmedUser || !payload.password || !payload.name?.trim()) {
|
||||
throw new Error('用户名、密码与姓名为必填项');
|
||||
}
|
||||
for (const u of this.users.values()) {
|
||||
if (u.username.toLowerCase() === trimmedUser.toLowerCase()) {
|
||||
throw new Error(`用户名已存在: ${trimmedUser}`);
|
||||
}
|
||||
}
|
||||
const now = new Date().toISOString();
|
||||
const status: AccountStatus = this.config.requireApproval ? 'pending_approval' : 'active';
|
||||
const newId = this.nextId++;
|
||||
this.users.set(newId, {
|
||||
id: newId,
|
||||
username: trimmedUser,
|
||||
password: payload.password,
|
||||
name: payload.name.trim(),
|
||||
roleId: 'role-guest',
|
||||
position: '访客/待指派',
|
||||
department: payload.department || '待分配部门',
|
||||
employeeId: payload.employeeId || `EXT-${newId}`,
|
||||
status,
|
||||
mustChangePassword: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
return {
|
||||
status,
|
||||
message: status === 'pending_approval' ? '注册成功,等待审核' : '注册成功',
|
||||
userId: newId,
|
||||
};
|
||||
}
|
||||
|
||||
async changePassword(userId: number, oldPass: string, newPass: string): Promise<boolean> {
|
||||
const user = this.users.get(userId);
|
||||
if (!user || user.password !== oldPass) throw new Error('原密码不正确');
|
||||
user.password = newPass;
|
||||
user.mustChangePassword = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
async updateSystemAuthConfig(newConfig: SystemAuthConfig): Promise<SystemAuthConfig> {
|
||||
this.config = { ...newConfig };
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
async updateUserStatus(userId: number, status: AccountStatus, roleId?: string, position?: string): Promise<UserAccount> {
|
||||
const user = this.users.get(userId);
|
||||
if (!user) throw new Error('用户不存在');
|
||||
user.status = status;
|
||||
if (roleId) user.roleId = roleId;
|
||||
if (position) user.position = position;
|
||||
const { password, ...safe } = user;
|
||||
return safe;
|
||||
}
|
||||
|
||||
async createAdminUser(payload: CreateAdminUserPayload): Promise<UserAccount> {
|
||||
const now = new Date().toISOString();
|
||||
const newId = this.nextId++;
|
||||
const user: UserAccount & { password?: string } = {
|
||||
id: newId,
|
||||
username: payload.username.trim(),
|
||||
password: payload.initialPassword,
|
||||
name: payload.name.trim(),
|
||||
roleId: payload.roleId,
|
||||
position: payload.position,
|
||||
department: payload.department || '研发中心',
|
||||
employeeId: payload.employeeId || `EMP-${newId}`,
|
||||
status: 'active',
|
||||
mustChangePassword: true,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
this.users.set(newId, user);
|
||||
const { password, ...safe } = user;
|
||||
return safe;
|
||||
}
|
||||
|
||||
logout() {
|
||||
if (typeof window !== 'undefined' && window.localStorage) {
|
||||
window.localStorage.removeItem(AUTH_STORAGE_KEY_SESSION);
|
||||
}
|
||||
}
|
||||
|
||||
saveSession(session: LoginResult) {
|
||||
if (typeof window !== 'undefined' && window.localStorage) {
|
||||
window.localStorage.setItem(AUTH_STORAGE_KEY_SESSION, JSON.stringify(session));
|
||||
}
|
||||
}
|
||||
|
||||
loadSavedSession(): LoginResult | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
getSessionToken(): string | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function createInMemoryAuthClient(options?: AuthClientOptions): LocalAuthClient {
|
||||
return new LocalAuthClient({ ...options, localFallback: true });
|
||||
}
|
||||
|
||||
export const authClient = new AuthClient();
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
const AUTH_STORAGE_KEY_SESSION = 'smdf_auth_session';
|
||||
|
||||
/** Bearer token from persisted login session; no X-User-ID (Phase 0). */
|
||||
export function getBearerAuthHeaders(contentType?: string): Record<string, string> {
|
||||
const headers: Record<string, string> = {};
|
||||
if (contentType) {
|
||||
headers['Content-Type'] = contentType;
|
||||
}
|
||||
if (typeof window === 'undefined' || !window.localStorage) {
|
||||
return headers;
|
||||
}
|
||||
try {
|
||||
const raw = window.localStorage.getItem(AUTH_STORAGE_KEY_SESSION);
|
||||
if (!raw) {
|
||||
return headers;
|
||||
}
|
||||
const session = JSON.parse(raw) as { token?: string };
|
||||
if (session.token) {
|
||||
headers['Authorization'] = `Bearer ${session.token}`;
|
||||
}
|
||||
} catch {
|
||||
// ignore corrupt session
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { getBearerAuthHeaders } from './authHeaders';
|
||||
|
||||
export interface ChatSessionSummary {
|
||||
session_id: string;
|
||||
title: string;
|
||||
@@ -18,29 +20,6 @@ export interface UpsertChatSessionInput {
|
||||
|
||||
const API_BASE = '';
|
||||
|
||||
function getAuthHeaders(): HeadersInit {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (typeof window !== 'undefined' && window.localStorage) {
|
||||
try {
|
||||
const raw = window.localStorage.getItem('smdf_auth_session');
|
||||
if (raw) {
|
||||
const session = JSON.parse(raw);
|
||||
if (session.token) {
|
||||
headers['Authorization'] = `Bearer ${session.token}`;
|
||||
}
|
||||
if (session.user?.id) {
|
||||
headers['X-User-ID'] = String(session.user.id);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
export async function fetchChatSessions(
|
||||
userId: number,
|
||||
projectId?: number,
|
||||
@@ -54,7 +33,7 @@ export async function fetchChatSessions(
|
||||
params.set('project_id', String(projectId));
|
||||
}
|
||||
const res = await fetch(`${API_BASE}/api/v1/chat/sessions?${params.toString()}`, {
|
||||
headers: getAuthHeaders(),
|
||||
headers: getBearerAuthHeaders(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`获取会话列表失败: ${res.status}`);
|
||||
@@ -65,7 +44,7 @@ export async function fetchChatSessions(
|
||||
export async function upsertChatSession(input: UpsertChatSessionInput): Promise<ChatSessionSummary> {
|
||||
const res = await fetch(`${API_BASE}/api/v1/chat/sessions`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
headers: getBearerAuthHeaders('application/json'),
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
if (!res.ok) {
|
||||
|
||||
+15
-36
@@ -3,6 +3,8 @@
|
||||
* 提供单条回复保存、私密列表、详情获取、编辑保存、共享流转及回收站接口
|
||||
*/
|
||||
|
||||
import { getBearerAuthHeaders } from './authHeaders';
|
||||
|
||||
export interface FreeformDocSummary {
|
||||
doc_id: string;
|
||||
project_id: number;
|
||||
@@ -49,14 +51,10 @@ export async function saveMessageToPrivate(
|
||||
projectId: number = 1,
|
||||
title?: string,
|
||||
expectedSha256?: string,
|
||||
userId?: number
|
||||
): Promise<CreateFromMessageResponse> {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (userId) headers['X-User-ID'] = String(userId);
|
||||
|
||||
const res = await fetch('/api/v1/freeform-documents/from-message', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
headers: getBearerAuthHeaders('application/json'),
|
||||
body: JSON.stringify({
|
||||
message_id: messageId,
|
||||
project_id: projectId,
|
||||
@@ -79,17 +77,13 @@ export async function saveMessageToPrivate(
|
||||
export async function listFreeformDocs(
|
||||
space: 'my_private' | 'project_shared' | 'trash' = 'my_private',
|
||||
projectId?: number,
|
||||
userId?: number
|
||||
): Promise<FreeformDocSummary[]> {
|
||||
let url = `/api/v1/freeform-documents?space=${encodeURIComponent(space)}`;
|
||||
if (projectId) {
|
||||
url += `&project_id=${projectId}`;
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (userId) headers['X-User-ID'] = String(userId);
|
||||
|
||||
const res = await fetch(url, { headers });
|
||||
const res = await fetch(url, { headers: getBearerAuthHeaders() });
|
||||
if (!res.ok) {
|
||||
throw new Error(`获取列表失败 (${res.status})`);
|
||||
}
|
||||
@@ -100,11 +94,10 @@ export async function listFreeformDocs(
|
||||
/**
|
||||
* 获取自由文档详情与 Markdown 正文
|
||||
*/
|
||||
export async function fetchFreeformDoc(docId: string, userId?: number): Promise<FreeformDocDetail> {
|
||||
const headers: Record<string, string> = {};
|
||||
if (userId) headers['X-User-ID'] = String(userId);
|
||||
|
||||
const res = await fetch(`/api/v1/freeform-documents/${encodeURIComponent(docId)}`, { headers });
|
||||
export async function fetchFreeformDoc(docId: string): Promise<FreeformDocDetail> {
|
||||
const res = await fetch(`/api/v1/freeform-documents/${encodeURIComponent(docId)}`, {
|
||||
headers: getBearerAuthHeaders(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
if (res.status === 403) {
|
||||
throw new Error('该文档为他人私密文档,严格禁止跨用户访问');
|
||||
@@ -129,13 +122,9 @@ export async function updateFreeformDoc(
|
||||
changeSummary?: string;
|
||||
expectedVersion?: number;
|
||||
expectedSha256?: string;
|
||||
userId?: number;
|
||||
}
|
||||
): Promise<FreeformDocDetail> {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (payload.userId) {
|
||||
headers['X-User-ID'] = String(payload.userId);
|
||||
}
|
||||
const headers = getBearerAuthHeaders('application/json');
|
||||
if (payload.expectedVersion !== undefined) {
|
||||
headers['If-Match'] = `"${payload.expectedVersion}"`;
|
||||
} else if (payload.expectedSha256) {
|
||||
@@ -168,13 +157,10 @@ export async function updateFreeformDoc(
|
||||
/**
|
||||
* 一键共享至项目参考库
|
||||
*/
|
||||
export async function shareFreeformDoc(docId: string, userId?: number): Promise<void> {
|
||||
const headers: Record<string, string> = {};
|
||||
if (userId) headers['X-User-ID'] = String(userId);
|
||||
|
||||
export async function shareFreeformDoc(docId: string): Promise<void> {
|
||||
const res = await fetch(`/api/v1/freeform-documents/${encodeURIComponent(docId)}/share`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
headers: getBearerAuthHeaders(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errData = await res.json().catch(() => ({}));
|
||||
@@ -185,13 +171,10 @@ export async function shareFreeformDoc(docId: string, userId?: number): Promise<
|
||||
/**
|
||||
* 撤回共享,恢复为个人私密
|
||||
*/
|
||||
export async function unshareFreeformDoc(docId: string, userId?: number): Promise<void> {
|
||||
const headers: Record<string, string> = {};
|
||||
if (userId) headers['X-User-ID'] = String(userId);
|
||||
|
||||
export async function unshareFreeformDoc(docId: string): Promise<void> {
|
||||
const res = await fetch(`/api/v1/freeform-documents/${encodeURIComponent(docId)}/unshare`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
headers: getBearerAuthHeaders(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errData = await res.json().catch(() => ({}));
|
||||
@@ -202,17 +185,13 @@ export async function unshareFreeformDoc(docId: string, userId?: number): Promis
|
||||
/**
|
||||
* 软删除文档移至回收站
|
||||
*/
|
||||
export async function deleteFreeformDoc(docId: string, userId?: number): Promise<void> {
|
||||
const headers: Record<string, string> = {};
|
||||
if (userId) headers['X-User-ID'] = String(userId);
|
||||
|
||||
export async function deleteFreeformDoc(docId: string): Promise<void> {
|
||||
const res = await fetch(`/api/v1/freeform-documents/${encodeURIComponent(docId)}`, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
headers: getBearerAuthHeaders(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errData = await res.json().catch(() => ({}));
|
||||
throw new Error(errData.message || `删除失败 (${res.status})`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { getBearerAuthHeaders } from './authHeaders';
|
||||
|
||||
export type ProjectAsset = {
|
||||
asset_id: number;
|
||||
filename: string;
|
||||
@@ -11,17 +13,12 @@ export async function uploadProjectAsset(
|
||||
projectId: number,
|
||||
file: File | Blob,
|
||||
filename: string,
|
||||
userId?: number,
|
||||
): Promise<ProjectAsset> {
|
||||
const form = new FormData();
|
||||
form.append('file', file, filename);
|
||||
const headers: Record<string, string> = {};
|
||||
if (userId && userId > 0) {
|
||||
headers['X-User-ID'] = String(userId);
|
||||
}
|
||||
const res = await fetch(`/api/v1/projects/${projectId}/assets`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
headers: getBearerAuthHeaders(),
|
||||
body: form,
|
||||
});
|
||||
if (!res.ok) {
|
||||
@@ -31,7 +28,9 @@ export async function uploadProjectAsset(
|
||||
}
|
||||
|
||||
export async function listProjectAssets(projectId: number): Promise<ProjectAsset[]> {
|
||||
const res = await fetch(`/api/v1/projects/${projectId}/assets`);
|
||||
const res = await fetch(`/api/v1/projects/${projectId}/assets`, {
|
||||
headers: getBearerAuthHeaders(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`list assets failed: HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
@@ -7,35 +7,13 @@ import {
|
||||
ActivateWorkflowInput,
|
||||
ActivateWorkflowResult,
|
||||
} from '../types/projectWorkflow';
|
||||
import { getBearerAuthHeaders } from './authHeaders';
|
||||
|
||||
const API_BASE = '';
|
||||
|
||||
function getAuthHeaders(): HeadersInit {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (typeof window !== 'undefined' && window.localStorage) {
|
||||
try {
|
||||
const raw = window.localStorage.getItem('smdf_auth_session');
|
||||
if (raw) {
|
||||
const session = JSON.parse(raw);
|
||||
if (session.token) {
|
||||
headers['Authorization'] = `Bearer ${session.token}`;
|
||||
}
|
||||
if (session.user?.id) {
|
||||
headers['X-User-ID'] = String(session.user.id);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
export async function fetchProjects(): Promise<Project[]> {
|
||||
const res = await fetch(`${API_BASE}/api/v1/projects`, {
|
||||
headers: getAuthHeaders(),
|
||||
headers: getBearerAuthHeaders(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`获取项目列表失败: ${res.status}`);
|
||||
@@ -46,7 +24,7 @@ export async function fetchProjects(): Promise<Project[]> {
|
||||
export async function createProject(data: CreateProjectInput): Promise<Project> {
|
||||
const res = await fetch(`${API_BASE}/api/v1/projects`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
headers: getBearerAuthHeaders('application/json'),
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!res.ok) {
|
||||
@@ -58,7 +36,7 @@ export async function createProject(data: CreateProjectInput): Promise<Project>
|
||||
|
||||
export async function getProject(id: number): Promise<Project> {
|
||||
const res = await fetch(`${API_BASE}/api/v1/projects/${id}`, {
|
||||
headers: getAuthHeaders(),
|
||||
headers: getBearerAuthHeaders(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`获取项目详情失败: ${res.status}`);
|
||||
@@ -68,7 +46,7 @@ export async function getProject(id: number): Promise<Project> {
|
||||
|
||||
export async function fetchProjectWorkflows(projectId: number): Promise<ProjectWorkflow[]> {
|
||||
const res = await fetch(`${API_BASE}/api/v1/projects/${projectId}/workflows`, {
|
||||
headers: getAuthHeaders(),
|
||||
headers: getBearerAuthHeaders(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`获取项目工作流清单失败: ${res.status}`);
|
||||
@@ -82,7 +60,7 @@ export async function activateWorkflow(
|
||||
): Promise<ActivateWorkflowResult> {
|
||||
const res = await fetch(`${API_BASE}/api/v1/projects/${projectId}/active-workflow`, {
|
||||
method: 'PUT',
|
||||
headers: getAuthHeaders(),
|
||||
headers: getBearerAuthHeaders('application/json'),
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
if (!res.ok) {
|
||||
@@ -101,7 +79,7 @@ export async function activateWorkflow(
|
||||
|
||||
export async function fetchWorkflowCatalog(): Promise<WorkflowDefinition[]> {
|
||||
const res = await fetch(`${API_BASE}/api/v1/workflows`, {
|
||||
headers: getAuthHeaders(),
|
||||
headers: getBearerAuthHeaders(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`获取工作流规范目录失败: ${res.status}`);
|
||||
@@ -117,7 +95,7 @@ export async function fetchProjectLineage(
|
||||
? `${API_BASE}/api/v1/projects/${projectId}/lineage?doc_id=${encodeURIComponent(docId)}`
|
||||
: `${API_BASE}/api/v1/projects/${projectId}/lineage`;
|
||||
const res = await fetch(url, {
|
||||
headers: getAuthHeaders(),
|
||||
headers: getBearerAuthHeaders(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`获取文档血缘失败: ${res.status}`);
|
||||
@@ -128,11 +106,11 @@ export async function fetchProjectLineage(
|
||||
export async function runProjectWorkflow(
|
||||
projectId: number,
|
||||
workflowKey: string,
|
||||
payload: Record<string, any>
|
||||
): Promise<any> {
|
||||
payload: Record<string, unknown>
|
||||
): Promise<unknown> {
|
||||
const res = await fetch(`${API_BASE}/api/v1/projects/${projectId}/workflows/${workflowKey}/run`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
headers: getBearerAuthHeaders('application/json'),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Sparkles, ChevronUp, ChevronDown, Check, UserCheck } from 'lucide-react';
|
||||
import { UserProfile } from '../../types/docgov';
|
||||
|
||||
interface DevRoleSwitcherProps {
|
||||
env?: string;
|
||||
currentUser: UserProfile;
|
||||
allUsers: UserProfile[];
|
||||
onSwitchUser: (user: UserProfile) => void;
|
||||
}
|
||||
|
||||
export const DevRoleSwitcher: React.FC<DevRoleSwitcherProps> = ({
|
||||
env = 'dev',
|
||||
currentUser,
|
||||
allUsers,
|
||||
onSwitchUser,
|
||||
}) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
// 严格环境红线:非 dev 环境绝对不渲染任何 DOM 内容
|
||||
if (env !== 'dev') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: '16px',
|
||||
right: '16px',
|
||||
zIndex: 50,
|
||||
fontFamily: 'var(--font-sans)',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '280px',
|
||||
backgroundColor: 'var(--bg-panel, #131820)',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid var(--border-strong)',
|
||||
boxShadow: 'var(--shadow-lg)',
|
||||
overflow: 'hidden',
|
||||
backdropFilter: 'blur(8px)',
|
||||
}}
|
||||
>
|
||||
{/* 折叠收起顶栏 */}
|
||||
<div
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
backgroundColor: 'var(--bg-subtle, #1A212C)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
cursor: 'pointer',
|
||||
borderBottom: expanded ? '1px solid var(--border)' : 'none',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<Sparkles size={14} style={{ color: 'var(--amber-500)' }} />
|
||||
<span style={{ fontSize: '12px', fontWeight: 600, color: 'var(--text-strong)' }}>
|
||||
DEV 调试切人
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
padding: '1px 5px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'rgba(242, 169, 59, 0.15)',
|
||||
color: 'var(--amber-500)',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
DEV ONLY
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '11px', color: 'var(--text-muted)' }}>
|
||||
<span style={{ color: 'var(--brand-500)', fontWeight: 600 }}>{currentUser.name}</span>
|
||||
{expanded ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 展开的快速切换列表 */}
|
||||
{expanded && (
|
||||
<div
|
||||
style={{
|
||||
padding: '8px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '4px',
|
||||
maxHeight: '220px',
|
||||
overflowY: 'auto',
|
||||
backgroundColor: 'var(--bg-card, #151B25)',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '10px', color: 'var(--text-faint)', padding: '2px 4px' }}>
|
||||
点击一键切换身份,实测立项、指派与文档权限:
|
||||
</div>
|
||||
{allUsers.map((u) => {
|
||||
const isCurrent = u.id === currentUser.id;
|
||||
return (
|
||||
<button
|
||||
key={u.id}
|
||||
type="button"
|
||||
onClick={() => onSwitchUser(u)}
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '6px 10px',
|
||||
borderRadius: '6px',
|
||||
fontSize: '12px',
|
||||
backgroundColor: isCurrent ? 'rgba(79, 107, 255, 0.12)' : 'transparent',
|
||||
border: isCurrent ? '1px solid rgba(79, 107, 255, 0.3)' : '1px solid transparent',
|
||||
color: isCurrent ? 'var(--brand-500)' : 'var(--text)',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<UserCheck size={13} style={{ color: isCurrent ? 'var(--brand-500)' : 'var(--text-muted)' }} />
|
||||
<span style={{ fontWeight: isCurrent ? 600 : 400 }}>{u.name}</span>
|
||||
<span style={{ fontSize: '10px', color: 'var(--text-faint)' }}>({u.position})</span>
|
||||
</div>
|
||||
{isCurrent && <Check size={13} style={{ color: 'var(--brand-500)' }} />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Plus,
|
||||
Trash2,
|
||||
Check,
|
||||
UserCheck,
|
||||
Users,
|
||||
Key,
|
||||
Info,
|
||||
@@ -23,7 +22,6 @@ export interface SettingsModalProps {
|
||||
users: UserProfile[];
|
||||
currentUser: UserProfile;
|
||||
onUpdateUserRole: (userId: number, roleId: string) => void;
|
||||
onSwitchUser: (user: UserProfile) => void;
|
||||
}
|
||||
|
||||
const ALL_PERMISSION_ITEMS: {
|
||||
@@ -84,7 +82,6 @@ export const SettingsModal: FC<SettingsModalProps> = ({
|
||||
users,
|
||||
currentUser,
|
||||
onUpdateUserRole,
|
||||
onSwitchUser,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<'roles' | 'users'>('roles');
|
||||
const [selectedRoleId, setSelectedRoleId] = useState<string>(roles[0]?.id || 'role-admin');
|
||||
@@ -779,35 +776,11 @@ export const SettingsModal: FC<SettingsModalProps> = ({
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 切换登录人按钮 */}
|
||||
{!isCurrent ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
onSwitchUser(u);
|
||||
showSavedNotification();
|
||||
}}
|
||||
style={{
|
||||
padding: '6px 14px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid var(--brand-500)',
|
||||
backgroundColor: 'var(--brand-500)',
|
||||
color: '#fff',
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
}}
|
||||
>
|
||||
<UserCheck size={13} />
|
||||
<span>切换为此人登录</span>
|
||||
</button>
|
||||
) : (
|
||||
{isCurrent ? (
|
||||
<span style={{ fontSize: '12px', color: 'var(--brand-500)', fontWeight: 600, width: '120px', textAlign: 'center' }}>
|
||||
✓ 正在以此身份交互
|
||||
✓ 当前登录身份
|
||||
</span>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,10 +17,7 @@ interface UserProjectRightsModalProps {
|
||||
currentProjectTitle: string;
|
||||
deliverables: DeliverableItem[];
|
||||
activeDocId: string;
|
||||
allUsers?: UserProfile[];
|
||||
canAssignLead?: boolean;
|
||||
onSwitchUser?: (user: UserProfile) => void;
|
||||
allowDevSwitch?: boolean;
|
||||
onSelectDoc: (docId: string) => void;
|
||||
onRoleChangeForDoc?: (docId: string, role: DocPermissionRole) => void;
|
||||
}
|
||||
@@ -32,10 +29,7 @@ export const UserProjectRightsModal: FC<UserProjectRightsModalProps> = ({
|
||||
currentProjectTitle,
|
||||
deliverables,
|
||||
activeDocId,
|
||||
allUsers,
|
||||
canAssignLead = true,
|
||||
onSwitchUser,
|
||||
allowDevSwitch = true,
|
||||
onSelectDoc,
|
||||
onRoleChangeForDoc,
|
||||
}) => {
|
||||
@@ -567,33 +561,7 @@ export const UserProjectRightsModal: FC<UserProjectRightsModalProps> = ({
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{allowDevSwitch && allUsers && allUsers.length > 0 ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<span style={{ fontSize: '11px', color: 'var(--amber-400)', fontWeight: 600 }}>[DEV 调试切人]:</span>
|
||||
{allUsers.map((u) => {
|
||||
const isCurrent = u.id === currentUser.id;
|
||||
return (
|
||||
<button
|
||||
key={u.id}
|
||||
onClick={() => onSwitchUser?.(u)}
|
||||
style={{
|
||||
padding: '4px 10px',
|
||||
borderRadius: '4px',
|
||||
border: isCurrent ? '1px solid var(--brand-500)' : '1px solid var(--border)',
|
||||
backgroundColor: isCurrent ? 'var(--brand-500)' : 'var(--bg-panel, #131820)',
|
||||
color: isCurrent ? '#fff' : 'var(--text)',
|
||||
fontSize: '11px',
|
||||
fontWeight: isCurrent ? 600 : 400,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease',
|
||||
}}
|
||||
>
|
||||
{u.name} ({u.position.slice(0, 4)})
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : <div />}
|
||||
<div />
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createInMemoryAuthClient } from '../api/auth';
|
||||
import { createInMemoryAuthClient } from '../api/auth.local';
|
||||
import { DEFAULT_ROLES, hasPermission } from './rbac';
|
||||
import { UserProfile } from '../types/docgov';
|
||||
|
||||
@@ -122,22 +122,16 @@ test('全链路场景 3: 管理员开通账号首次强制改密闭环', async (
|
||||
assert.equal(freshSession.mustChangePassword, false);
|
||||
});
|
||||
|
||||
test('全链路场景 4: 环境隔离守卫(dev 允许切人,test/main 彻底禁用)', async () => {
|
||||
// DEV 环境
|
||||
test('全链路场景 4: 环境标识随登录会话返回(dev/test/main)', async () => {
|
||||
const devClient = createInMemoryAuthClient({ env: 'dev' });
|
||||
const devSession = await devClient.login({ username: 'admin', password: 'admin-secret-password-123' });
|
||||
assert.equal(devSession.env, 'dev');
|
||||
assert.equal(devSession.allowDevSwitch, true);
|
||||
|
||||
// TEST 环境
|
||||
const testClient = createInMemoryAuthClient({ env: 'test' });
|
||||
const testSession = await testClient.login({ username: 'admin', password: 'admin-secret-password-123' });
|
||||
assert.equal(testSession.env, 'test');
|
||||
assert.equal(testSession.allowDevSwitch, false);
|
||||
|
||||
// MAIN 生产环境
|
||||
const prodClient = createInMemoryAuthClient({ env: 'main' });
|
||||
const prodSession = await prodClient.login({ username: 'admin', password: 'admin-secret-password-123' });
|
||||
assert.equal(prodSession.env, 'main');
|
||||
assert.equal(prodSession.allowDevSwitch, false);
|
||||
});
|
||||
|
||||
@@ -26,7 +26,6 @@ export interface LoginResult {
|
||||
user: UserAccount;
|
||||
mustChangePassword?: boolean;
|
||||
env: string;
|
||||
allowDevSwitch: boolean;
|
||||
}
|
||||
|
||||
export interface RegisterPayload {
|
||||
|
||||
Reference in New Issue
Block a user