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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user