/** * MSW Handlers * * 仲裁 ARB-004 §4:MSW 拦截所有 GraphQL/HTTP 请求 * 端点: * - POST /api/auth/login - mock 登录 * - POST /api/admin/graphql - admin 命名空间 GraphQL * - GET /ws - WebSocket(mock-socket 在 use-websocket.ts 中处理) */ import { graphql, http, HttpResponse } from "msw"; import { mockCurrentUser, mockUsers, mockRoles, mockPermissions, mockViewports, mockOrganization, mockClasses, mockTeachers, mockStudents, mockAuditLogs, mockDashboard, mockSystemSettings, mockSchools, mockGrades, mockGradeInsights, mockDepartments, mockAcademicYears, mockAuditOverviewStats, mockAuditTrend, mockDataChangeLogs, mockLoginLogs, mockAnnouncements, mockInvitationCodes, mockFiles, mockFileStats, mockAiProviders, mockAiUsage, mockSecurityPolicy, mockFileUploadConfig, mockNotificationConfig, mockBrandConfig, genId, } from "./fixtures"; import type { UserViewModel, RoleViewModel, ViewportConfigViewModel, PaginatedResult, ListFilter, } from "@/types/view-models"; const BASE_URL = ""; // 内存中可变副本(支持 mock 增删改) const db = { users: [...mockUsers] as UserViewModel[], roles: [...mockRoles] as RoleViewModel[], viewports: [...mockViewports] as ViewportConfigViewModel[], permissions: [...mockPermissions], organization: [...mockOrganization], classes: [...mockClasses], teachers: [...mockTeachers], students: [...mockStudents], auditLogs: [...mockAuditLogs], systemSettings: { ...mockSystemSettings }, schools: [...mockSchools], grades: [...mockGrades], departments: [...mockDepartments], academicYears: [...mockAcademicYears], announcements: [...mockAnnouncements], invitationCodes: [...mockInvitationCodes], files: [...mockFiles], aiProviders: [...mockAiProviders], securityPolicy: { ...mockSecurityPolicy }, fileUploadConfig: { ...mockFileUploadConfig }, notificationConfig: { ...mockNotificationConfig }, brandConfig: { ...mockBrandConfig }, }; function paginate( items: T[], filter: Partial, ): PaginatedResult { const page = filter.page ?? 1; const pageSize = filter.pageSize ?? 20; const start = (page - 1) * pageSize; const end = start + pageSize; const sliced = items.slice(start, end); return { items: sliced, total: items.length, page, pageSize, hasNext: end < items.length, }; } function recordAudit( action: string, resourceType: string, resourceId: string, afterState?: string, ): void { db.auditLogs.unshift({ id: genId("al"), eventId: genId("evt"), occurredAt: Date.now(), actorUserId: mockCurrentUser.id, actorName: mockCurrentUser.name, action, resourceType, resourceId, ip: "127.0.0.1", userAgent: "MSW-Mock", beforeState: null, afterState: afterState ?? null, traceId: genId("trace"), }); } export const handlers = [ // ─── 认证 ──────────────────────────────────────────── http.post(`${BASE_URL}/api/auth/login`, async ({ request }) => { const body = (await request.json()) as { email: string; password: string }; if (body.email === "admin@edu.test" && body.password === "admin123") { return HttpResponse.json({ success: true, data: { user: mockCurrentUser, tokens: { accessToken: "mock-jwt-token-admin-001" }, }, }); } return HttpResponse.json( { success: false, error: { message: "邮箱或密码错误" }, }, { status: 401 }, ); }), // ─── GraphQL admin 命名空间 ────────────────────────── graphql.operation(({ query, variables }) => { const operationName = (query.match(/^\s*(query|mutation)\s+(\w+)/) || [])[2] ?? ""; // ── currentUser ── if (operationName === "CurrentUser") { return HttpResponse.json({ data: { currentUser: mockCurrentUser } }); } // ── adminUsers ── if (operationName === "AdminUsers") { const filter = (variables.filter ?? {}) as Partial; let items = [...db.users]; if (filter.search) { const q = filter.search.toLowerCase(); items = items.filter( (u) => u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q), ); } if (filter.status && filter.status !== "all") { items = items.filter((u) => u.status === filter.status); } const result = paginate(items, filter); return HttpResponse.json({ data: { adminUsers: result } }); } // ── adminUser ── if (operationName === "AdminUser") { const id = variables.id as string; const user = db.users.find((u) => u.id === id) ?? null; return HttpResponse.json({ data: { adminUser: user } }); } // ── createUser ── if (operationName === "CreateUser") { const input = variables.input as { email: string; name: string; password: string; roleIds?: string[]; dataScope?: string; organizationId?: string; }; const roles = db.roles.filter((r) => input.roleIds?.includes(r.id)); const newUser: UserViewModel = { id: genId("u"), email: input.email, name: input.name, roles, status: "active", dataScope: (input.dataScope as UserViewModel["dataScope"]) ?? "SCHOOL", organizationId: input.organizationId ?? null, schoolName: "Edu 实验学校", lastLoginAt: null, createdAt: Date.now(), updatedAt: Date.now(), }; db.users.push(newUser); recordAudit( "USER_CREATE", "iam.user", newUser.id, JSON.stringify({ email: newUser.email, name: newUser.name }), ); return HttpResponse.json({ data: { createUser: { id: newUser.id, email: newUser.email, name: newUser.name, status: newUser.status, }, }, }); } // ── updateUser ── if (operationName === "UpdateUser") { const id = variables.id as string; const input = variables.input as { name?: string; email?: string; roleIds?: string[]; dataScope?: string; organizationId?: string; }; const user = db.users.find((u) => u.id === id); if (!user) { return HttpResponse.json( { errors: [{ message: "用户不存在" }] }, { status: 404 }, ); } if (input.name) user.name = input.name; if (input.email) user.email = input.email; if (input.roleIds) user.roles = db.roles.filter((r) => input.roleIds!.includes(r.id)); if (input.dataScope) user.dataScope = input.dataScope as UserViewModel["dataScope"]; if (input.organizationId !== undefined) user.organizationId = input.organizationId; user.updatedAt = Date.now(); recordAudit("USER_UPDATE", "iam.user", user.id, JSON.stringify(input)); return HttpResponse.json({ data: { updateUser: { id: user.id, email: user.email, name: user.name, status: user.status, }, }, }); } // ── toggleUserStatus ── if (operationName === "ToggleUserStatus") { const id = variables.id as string; const newStatus = variables.status as UserViewModel["status"]; const user = db.users.find((u) => u.id === id); if (!user) { return HttpResponse.json( { errors: [{ message: "用户不存在" }] }, { status: 404 }, ); } const before = user.status; user.status = newStatus; user.updatedAt = Date.now(); recordAudit( "USER_TOGGLE_STATUS", "iam.user", user.id, JSON.stringify({ before, after: newStatus }), ); return HttpResponse.json({ data: { toggleUserStatus: { id: user.id, status: user.status } }, }); } // ── adminRoles ── if (operationName === "AdminRoles") { return HttpResponse.json({ data: { adminRoles: db.roles } }); } // ── createRole ── if (operationName === "CreateRole") { const input = variables.input as { name: string; code: string; description?: string; dataScope?: string; }; const newRole: RoleViewModel = { id: genId("r"), name: input.name, code: input.code, description: input.description ?? "", permissions: [], userCount: 0, dataScope: (input.dataScope as RoleViewModel["dataScope"]) ?? "SCHOOL", isSystem: false, createdAt: Date.now(), updatedAt: Date.now(), }; db.roles.push(newRole); recordAudit( "ROLE_CREATE", "iam.role", newRole.id, JSON.stringify({ name: newRole.name, code: newRole.code }), ); return HttpResponse.json({ data: { createRole: { id: newRole.id, name: newRole.name, code: newRole.code, }, }, }); } // ── updateRolePermissions ── if (operationName === "UpdateRolePermissions") { const roleId = variables.roleId as string; const codes = variables.permissionCodes as string[]; const role = db.roles.find((r) => r.id === roleId); if (!role) { return HttpResponse.json( { errors: [{ message: "角色不存在" }] }, { status: 404 }, ); } role.permissions = db.permissions.filter((p) => codes.includes(p.code)); role.updatedAt = Date.now(); recordAudit( "ROLE_UPDATE", "iam.role", role.id, JSON.stringify({ permissionCodes: codes }), ); return HttpResponse.json({ data: { updateRolePermissions: { id: role.id, permissions: role.permissions.map((p) => ({ id: p.id, code: p.code, })), }, }, }); } // ── adminPermissions ── if (operationName === "AdminPermissions") { return HttpResponse.json({ data: { adminPermissions: db.permissions } }); } // ── adminViewports ── if (operationName === "AdminViewports") { const scope = variables.scope as string | undefined; const items = scope ? db.viewports.filter((v) => v.scope === scope) : db.viewports; return HttpResponse.json({ data: { adminViewports: items } }); } // ── updateViewport ── if (operationName === "UpdateViewport") { const id = variables.id as string; const input = variables.input as { label?: string; sortOrder?: number; isVisible?: boolean; requiredPermission?: string | null; }; const vp = db.viewports.find((v) => v.id === id); if (!vp) { return HttpResponse.json( { errors: [{ message: "视口不存在" }] }, { status: 404 }, ); } if (input.label !== undefined) vp.label = input.label; if (input.sortOrder !== undefined) vp.sortOrder = input.sortOrder; if (input.isVisible !== undefined) vp.isVisible = input.isVisible; if (input.requiredPermission !== undefined) vp.requiredPermission = input.requiredPermission; recordAudit( "VIEWPORT_UPDATE", "iam.viewport", vp.id, JSON.stringify(input), ); return HttpResponse.json({ data: { updateViewport: { id: vp.id, key: vp.key, label: vp.label, sortOrder: vp.sortOrder, isVisible: vp.isVisible, }, }, }); } // ── adminOrganization ── if (operationName === "AdminOrganization") { const parentId = variables.parentId as string | null | undefined; const items = parentId === undefined || parentId === null ? db.organization.filter((n) => n.parentId === null) : db.organization.filter((n) => n.parentId === parentId); return HttpResponse.json({ data: { adminOrganization: items } }); } // ── adminClasses ── if (operationName === "AdminClasses") { const filter = (variables.filter ?? {}) as Partial; let items = [...db.classes]; if (filter.search) { const q = filter.search.toLowerCase(); items = items.filter( (c) => c.name.toLowerCase().includes(q) || c.gradeName.toLowerCase().includes(q), ); } if (filter.schoolId) { // mock:单一学校 } const result = paginate(items, filter); return HttpResponse.json({ data: { adminClasses: result } }); } // ── adminTeachers ── if (operationName === "AdminTeachers") { const filter = (variables.filter ?? {}) as Partial; let items = [...db.teachers]; if (filter.search) { const q = filter.search.toLowerCase(); items = items.filter( (t) => t.name.toLowerCase().includes(q) || t.email.toLowerCase().includes(q), ); } if (filter.status && filter.status !== "all") { items = items.filter((t) => t.status === filter.status); } const result = paginate(items, filter); return HttpResponse.json({ data: { adminTeachers: result } }); } // ── adminStudents ── if (operationName === "AdminStudents") { const filter = (variables.filter ?? {}) as Partial; let items = [...db.students]; if (filter.search) { const q = filter.search.toLowerCase(); items = items.filter( (s) => s.name.toLowerCase().includes(q) || s.email.toLowerCase().includes(q), ); } if (filter.status && filter.status !== "all") { items = items.filter((s) => s.status === filter.status); } const result = paginate(items, filter); return HttpResponse.json({ data: { adminStudents: result } }); } // ── auditLogs ── if (operationName === "AuditLogs") { const filter = (variables.filter ?? {}) as Partial & { action?: string; actorUserId?: string; }; let items = [...db.auditLogs]; if (filter.search) { const q = filter.search.toLowerCase(); items = items.filter( (l) => l.actorName.toLowerCase().includes(q) || l.action.toLowerCase().includes(q) || l.resourceType.toLowerCase().includes(q), ); } if (filter.action) { items = items.filter((l) => l.action === filter.action); } if (filter.actorUserId) { items = items.filter((l) => l.actorUserId === filter.actorUserId); } const result = paginate(items, filter); return HttpResponse.json({ data: { auditLogs: result } }); } // ── adminDashboard ── if (operationName === "AdminDashboard") { return HttpResponse.json({ data: { adminDashboard: mockDashboard } }); } // ── systemSettings ── if (operationName === "SystemSettings") { return HttpResponse.json({ data: { systemSettings: db.systemSettings } }); } // ── updateSystemSettings ── if (operationName === "UpdateSystemSettings") { const input = variables.input as Partial; Object.assign(db.systemSettings, input); recordAudit( "SYSTEM_SETTINGS_UPDATE", "admin.system", "settings", JSON.stringify(input), ); return HttpResponse.json({ data: { updateSystemSettings: { schoolName: db.systemSettings.schoolName, schoolCode: db.systemSettings.schoolCode, academicYear: db.systemSettings.academicYear, semester: db.systemSettings.semester, }, }, }); } // ── auditOverviewStats ── if (operationName === "AuditOverviewStats") { return HttpResponse.json({ data: { auditOverviewStats: mockAuditOverviewStats }, }); } // ── auditTrend ── if (operationName === "AuditTrend") { return HttpResponse.json({ data: { auditTrend: mockAuditTrend } }); } // ── dataChangeLogs ── if (operationName === "DataChangeLogs") { return HttpResponse.json({ data: { dataChangeLogs: paginate(mockDataChangeLogs, variables.filter ?? {}), }, }); } // ── loginLogs ── if (operationName === "LoginLogs") { return HttpResponse.json({ data: { loginLogs: paginate(mockLoginLogs, variables.filter ?? {}) }, }); } // ── adminSchools ── if (operationName === "AdminSchools") { return HttpResponse.json({ data: { adminSchools: db.schools } }); } // ── adminGrades ── if (operationName === "AdminGrades") { const schoolId = variables.schoolId; const grades = schoolId ? db.grades.filter((g) => g.schoolId === schoolId) : db.grades; return HttpResponse.json({ data: { adminGrades: grades } }); } // ── adminGradeInsights ── if (operationName === "AdminGradeInsights") { return HttpResponse.json({ data: { adminGradeInsights: mockGradeInsights }, }); } // ── adminDepartments ── if (operationName === "AdminDepartments") { const schoolId = variables.schoolId; const depts = schoolId ? db.departments.filter((d) => d.schoolId === schoolId) : db.departments; return HttpResponse.json({ data: { adminDepartments: depts } }); } // ── adminAcademicYears ── if (operationName === "AdminAcademicYears") { return HttpResponse.json({ data: { adminAcademicYears: db.academicYears }, }); } // ── adminAnnouncements ── if (operationName === "AdminAnnouncements") { return HttpResponse.json({ data: { adminAnnouncements: paginate( db.announcements, variables.filter ?? {}, ), }, }); } // ── createAnnouncement ── if (operationName === "CreateAnnouncement") { const input = variables.input; const newAnn = { id: genId("ann"), title: input.title, content: input.content, status: "draft" as const, audience: input.audience ?? ["all"], authorName: mockCurrentUser.name, isPinned: false, readCount: 0, totalCount: 0, publishedAt: null, createdAt: Date.now(), updatedAt: Date.now(), }; db.announcements.unshift(newAnn); recordAudit("ANNOUNCEMENT_CREATE", "announcement", newAnn.id); return HttpResponse.json({ data: { createAnnouncement: { id: newAnn.id, title: newAnn.title, status: newAnn.status, }, }, }); } // ── publishAnnouncement ── if (operationName === "PublishAnnouncement") { const ann = db.announcements.find((a) => a.id === variables.id); if (ann) { ann.status = "published"; ann.publishedAt = Date.now(); ann.updatedAt = Date.now(); recordAudit("ANNOUNCEMENT_PUBLISH", "announcement", ann.id); } return HttpResponse.json({ data: { publishAnnouncement: { id: variables.id, status: "published" }, }, }); } // ── archiveAnnouncement ── if (operationName === "ArchiveAnnouncement") { const ann = db.announcements.find((a) => a.id === variables.id); if (ann) { ann.status = "archived"; ann.updatedAt = Date.now(); recordAudit("ANNOUNCEMENT_ARCHIVE", "announcement", ann.id); } return HttpResponse.json({ data: { archiveAnnouncement: { id: variables.id, status: "archived" } }, }); } // ── toggleAnnouncementPin ── if (operationName === "ToggleAnnouncementPin") { const ann = db.announcements.find((a) => a.id === variables.id); if (ann) { ann.isPinned = !ann.isPinned; ann.updatedAt = Date.now(); } return HttpResponse.json({ data: { toggleAnnouncementPin: { id: variables.id, isPinned: ann?.isPinned ?? false, }, }, }); } // ── adminInvitationCodes ── if (operationName === "AdminInvitationCodes") { return HttpResponse.json({ data: { adminInvitationCodes: paginate( db.invitationCodes, variables.filter ?? {}, ), }, }); } // ── generateInvitationCodes ── if (operationName === "GenerateInvitationCodes") { const input = variables.input; const count = Math.min(input.count ?? 10, 1000); const batchId = genId("bat"); const charset = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; const codes: string[] = []; for (let i = 0; i < count; i++) { let code = ""; for (let j = 0; j < 8; j++) { code += charset[Math.floor(Math.random() * charset.length)]; } codes.push(code); db.invitationCodes.unshift({ id: genId("ic"), code, status: "unused", roleId: input.roleId ?? null, roleName: null, classId: input.classId ?? null, className: null, email: input.email ?? null, batchId, usedBy: null, usedByName: null, usedAt: null, createdAt: Date.now(), expiresAt: input.expiresAt ?? null, }); } recordAudit("INVITATION_CODE_GENERATE", "invitation_code", batchId); return HttpResponse.json({ data: { generateInvitationCodes: { codes, batchId } }, }); } // ── deleteInvitationCodes ── if (operationName === "DeleteInvitationCodes") { const ids = variables.ids as string[]; db.invitationCodes = db.invitationCodes.filter( (ic) => !ids.includes(ic.id), ); recordAudit("INVITATION_CODE_DELETE", "invitation_code", ids.join(",")); return HttpResponse.json({ data: { deleteInvitationCodes: true } }); } // ── adminFiles ── if (operationName === "AdminFiles") { return HttpResponse.json({ data: { adminFiles: paginate(db.files, variables.filter ?? {}) }, }); } // ── adminFileStats ── if (operationName === "AdminFileStats") { return HttpResponse.json({ data: { adminFileStats: mockFileStats } }); } // ── deleteFiles ── if (operationName === "DeleteFiles") { const ids = variables.ids as string[]; db.files = db.files.filter((f) => !ids.includes(f.id)); recordAudit("FILE_DELETE", "file", ids.join(",")); return HttpResponse.json({ data: { deleteFiles: true } }); } // ── aiProviders ── if (operationName === "AiProviders") { return HttpResponse.json({ data: { aiProviders: db.aiProviders } }); } // ── updateAiProvider ── if (operationName === "UpdateAiProvider") { const provider = db.aiProviders.find((p) => p.id === variables.id); if (provider) { Object.assign(provider, variables.input); } return HttpResponse.json({ data: { updateAiProvider: { id: variables.id, isActive: provider?.isActive ?? false, }, }, }); } // ── aiUsage ── if (operationName === "AiUsage") { return HttpResponse.json({ data: { aiUsage: mockAiUsage } }); } // ── 未匹配的 operation ── return HttpResponse.json( { errors: [{ message: `未实现的 GraphQL operation: ${operationName}` }] }, { status: 501 }, ); }), ];