diff --git a/web/src/App.tsx b/web/src/App.tsx index dc260e8c..d342f3d5 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -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 { 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(() => authClient.loadSavedSession()); - const [systemEnv, setSystemEnv] = useState('dev'); + const [systemEnv, setSystemEnv] = useState(''); const [systemAuthConfig, setSystemAuthConfig] = useState({ 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() { } /> - {/* 用户在当前激活项目中的法定职责与权限矩阵弹窗 (支持快速模拟账号切换) */} + {/* 用户在当前激活项目中的法定职责与权限矩阵弹窗 */} 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 环境生效,生产与测试环境彻底剥离) */} - { - 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); - } - }} - /> - {/* 单文档可见性覆盖弹窗 */} = 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 { + 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 { + 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 { + 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 { + this.config = { ...newConfig }; + return { ...this.config }; + } + + async updateUserStatus(userId: number, status: AccountStatus, roleId?: string, position?: string): Promise { + 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 { + 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 }); +} diff --git a/web/src/api/auth.test.ts b/web/src/api/auth.test.ts index 31a7a874..0efb65f4 100644 --- a/web/src/api/auth.test.ts +++ b/web/src/api/auth.test.ts @@ -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); }); diff --git a/web/src/api/auth.ts b/web/src/api/auth.ts index b00d0bf4..f6866521 100644 --- a/web/src/api/auth.ts +++ b/web/src/api/auth.ts @@ -208,174 +208,4 @@ export class AuthClient { } } -/** 测试专用本地鉴权客户端(不用于生产构建默认路径) */ -export class LocalAuthClient { - private users: Map = 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 { - 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 { - 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 { - 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 { - this.config = { ...newConfig }; - return { ...this.config }; - } - - async updateUserStatus(userId: number, status: AccountStatus, roleId?: string, position?: string): Promise { - 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 { - 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(); diff --git a/web/src/api/authHeaders.ts b/web/src/api/authHeaders.ts new file mode 100644 index 00000000..69a2fe72 --- /dev/null +++ b/web/src/api/authHeaders.ts @@ -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 { + const headers: Record = {}; + 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; +} diff --git a/web/src/api/chatSessions.ts b/web/src/api/chatSessions.ts index ae03d1a2..05bd7b46 100644 --- a/web/src/api/chatSessions.ts +++ b/web/src/api/chatSessions.ts @@ -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 = { - '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 { 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) { diff --git a/web/src/api/freeformDoc.ts b/web/src/api/freeformDoc.ts index 67fc0289..bce78dd5 100644 --- a/web/src/api/freeformDoc.ts +++ b/web/src/api/freeformDoc.ts @@ -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 { - const headers: Record = { '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 { let url = `/api/v1/freeform-documents?space=${encodeURIComponent(space)}`; if (projectId) { url += `&project_id=${projectId}`; } - const headers: Record = {}; - 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 { - const headers: Record = {}; - 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 { + 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 { - const headers: Record = { '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 { - const headers: Record = {}; - if (userId) headers['X-User-ID'] = String(userId); - +export async function shareFreeformDoc(docId: string): Promise { 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 { - const headers: Record = {}; - if (userId) headers['X-User-ID'] = String(userId); - +export async function unshareFreeformDoc(docId: string): Promise { 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 { - const headers: Record = {}; - if (userId) headers['X-User-ID'] = String(userId); - +export async function deleteFreeformDoc(docId: string): Promise { 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})`); } } - diff --git a/web/src/api/projectAssets.ts b/web/src/api/projectAssets.ts index 587425d6..d69694f5 100644 --- a/web/src/api/projectAssets.ts +++ b/web/src/api/projectAssets.ts @@ -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 { const form = new FormData(); form.append('file', file, filename); - const headers: Record = {}; - 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 { - 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}`); } diff --git a/web/src/api/projectWorkflow.ts b/web/src/api/projectWorkflow.ts index 794679cd..5264b682 100644 --- a/web/src/api/projectWorkflow.ts +++ b/web/src/api/projectWorkflow.ts @@ -7,35 +7,13 @@ import { ActivateWorkflowInput, ActivateWorkflowResult, } from '../types/projectWorkflow'; +import { getBearerAuthHeaders } from './authHeaders'; const API_BASE = ''; -function getAuthHeaders(): HeadersInit { - const headers: Record = { - '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 { 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 { export async function createProject(data: CreateProjectInput): Promise { 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 export async function getProject(id: number): Promise { 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 { export async function fetchProjectWorkflows(projectId: number): Promise { 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 { 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 { 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 -): Promise { + payload: Record +): Promise { 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) { diff --git a/web/src/components/dev/DevRoleSwitcher.tsx b/web/src/components/dev/DevRoleSwitcher.tsx deleted file mode 100644 index 1a06d0dc..00000000 --- a/web/src/components/dev/DevRoleSwitcher.tsx +++ /dev/null @@ -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 = ({ - env = 'dev', - currentUser, - allUsers, - onSwitchUser, -}) => { - const [expanded, setExpanded] = useState(false); - - // 严格环境红线:非 dev 环境绝对不渲染任何 DOM 内容 - if (env !== 'dev') { - return null; - } - - return ( -
-
- {/* 折叠收起顶栏 */} -
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', - }} - > -
- - - DEV 调试切人 - - - DEV ONLY - -
- -
- {currentUser.name} - {expanded ? : } -
-
- - {/* 展开的快速切换列表 */} - {expanded && ( -
-
- 点击一键切换身份,实测立项、指派与文档权限: -
- {allUsers.map((u) => { - const isCurrent = u.id === currentUser.id; - return ( - - ); - })} -
- )} -
-
- ); -}; diff --git a/web/src/components/user/SettingsModal.tsx b/web/src/components/user/SettingsModal.tsx index 9bea8bbf..bd085967 100644 --- a/web/src/components/user/SettingsModal.tsx +++ b/web/src/components/user/SettingsModal.tsx @@ -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 = ({ users, currentUser, onUpdateUserRole, - onSwitchUser, }) => { const [activeTab, setActiveTab] = useState<'roles' | 'users'>('roles'); const [selectedRoleId, setSelectedRoleId] = useState(roles[0]?.id || 'role-admin'); @@ -779,35 +776,11 @@ export const SettingsModal: FC = ({ - {/* 切换登录人按钮 */} - {!isCurrent ? ( - - ) : ( + {isCurrent ? ( - ✓ 正在以此身份交互 + ✓ 当前登录身份 - )} + ) : null} ); diff --git a/web/src/components/user/UserProjectRightsModal.tsx b/web/src/components/user/UserProjectRightsModal.tsx index 56545870..cfe83bdc 100644 --- a/web/src/components/user/UserProjectRightsModal.tsx +++ b/web/src/components/user/UserProjectRightsModal.tsx @@ -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 = ({ currentProjectTitle, deliverables, activeDocId, - allUsers, canAssignLead = true, - onSwitchUser, - allowDevSwitch = true, onSelectDoc, onRoleChangeForDoc, }) => { @@ -567,33 +561,7 @@ export const UserProjectRightsModal: FC = ({ justifyContent: 'space-between', }} > - {allowDevSwitch && allUsers && allUsers.length > 0 ? ( -
- [DEV 调试切人]: - {allUsers.map((u) => { - const isCurrent = u.id === currentUser.id; - return ( - - ); - })} -
- ) :
} +