文件
hyb-oa/server/src/routes/confidentialDocRefs.js
T

172 行
8.8 KiB
JavaScript

const { z } = require("zod");
const { AppError } = require("../utils/AppError");
const { sendData } = require("../utils/reply");
const { buildAuditLogData } = require("../utils/audit");
const { FEATURE_FLAG_KEYS, evaluateFeatureFlag } = require("../services/featureFlags");
const UUID_SCHEMA = z.string().uuid();
const CONFIDENTIALITY_SCHEMA = z.enum(["public", "internal", "confidential", "restricted"]);
const STORAGE_TYPE_SCHEMA = z.enum(["oa_link", "nas", "internal_share", "offline_archive"]);
const QUERY_SCHEMA = z.object({ projectId: UUID_SCHEMA.optional(), contractRegistryId: UUID_SCHEMA.optional() });
const WRITE_SCHEMA = z.object({
projectId: UUID_SCHEMA.nullable().optional(),
contractRegistryId: UUID_SCHEMA.nullable().optional(),
title: z.string().trim().min(1).max(200),
documentNo: z.string().trim().max(80).nullable().optional(),
confidentialityLevel: CONFIDENTIALITY_SCHEMA.optional(),
storageType: STORAGE_TYPE_SCHEMA,
storagePath: z.string().trim().min(1).max(500),
offlineArchiveNo: z.string().trim().max(120).nullable().optional(),
ownerEmployeeId: UUID_SCHEMA.optional(),
accessRuleJson: z.record(z.string(), z.unknown()).nullable().optional(),
}).strict();
const PATCH_SCHEMA = WRITE_SCHEMA.partial().refine((value) => Object.keys(value).length > 0, { message: "empty_patch" });
function buildValidationError(details, statusCode = 400) {
return new AppError({ code: "VALID_001", message: "参数校验失败", statusCode, details });
}
function parseBody(schema, body) {
const parsed = schema.safeParse(body ?? {});
if (!parsed.success) throw buildValidationError(parsed.error.flatten());
return parsed.data;
}
async function ensureFeatureEnabled(fastify, request) {
const gate = await evaluateFeatureFlag({ prisma: fastify.prisma, config: fastify.config, flagKey: FEATURE_FLAG_KEYS.confidentialDocRefs, departmentId: request.employee.department_id ?? null, strictDepartment: false });
if (!gate.enabled) throw buildValidationError({ featureFlag: "confidential_doc_refs_v1_disabled", reason: gate.reason }, 409);
}
function canManage(fastify, employee) {
return fastify.hasPermission(employee, "project.manage") || fastify.hasPermission(employee, "project.view_all") || fastify.hasPermission(employee, "system.settings");
}
async function writeAudit(fastify, request, action, targetId, extra = {}) {
try {
await fastify.prisma.auditLog.create({ data: buildAuditLogData(request, { actorId: request.employee.id, action, targetType: "confidential_document_ref", targetId, extra }) });
} catch (error) {
request.log.error({ err: error, action, targetId }, "confidential doc ref audit failed");
}
}
function mapRow(item) {
return {
id: item.id,
projectId: item.project_id ?? null,
contractRegistryId: item.contract_registry_id ?? null,
title: item.title,
documentNo: item.document_no ?? null,
confidentialityLevel: item.confidentiality_level,
storageType: item.storage_type,
storagePath: item.storage_path,
offlineArchiveNo: item.offline_archive_no ?? null,
ownerEmployeeId: item.owner_employee_id,
accessRuleJson: item.access_rule_json ?? null,
createdAt: item.created_at?.toISOString?.() ?? null,
updatedAt: item.updated_at?.toISOString?.() ?? null,
owner: item.owner ? { id: item.owner.id, employeeNo: item.owner.employee_no, name: item.owner.name } : null,
project: item.project ? { id: item.project.id, projectNo: item.project.project_no, name: item.project.name } : null,
contractRegistry: item.contract_registry ? { id: item.contract_registry.id, contractNo: item.contract_registry.contract_no } : null,
};
}
async function confidentialDocRefsRoutes(fastify) {
fastify.get("/confidential-doc-refs", { preHandler: fastify.authenticate }, async (request, reply) => {
await ensureFeatureEnabled(fastify, request);
const parsed = QUERY_SCHEMA.safeParse(request.query ?? {});
if (!parsed.success) throw buildValidationError(parsed.error.flatten());
const rows = await fastify.prisma.confidentialDocumentRef.findMany({
where: {
AND: [
parsed.data.projectId ? { project_id: parsed.data.projectId } : {},
parsed.data.contractRegistryId ? { contract_registry_id: parsed.data.contractRegistryId } : {},
canManage(fastify, request.employee)
? {}
: {
OR: [
{ owner_employee_id: request.employee.id },
{ project: { owner_employee_id: request.employee.id } },
{ project: { manager_employee_id: request.employee.id } },
{ project: { members: { some: { employee_id: request.employee.id, left_at: null } } } },
{ contract_registry: { owner_employee_id: request.employee.id } },
],
},
],
},
orderBy: { updated_at: "desc" },
include: {
owner: { select: { id: true, employee_no: true, name: true } },
project: { select: { id: true, project_no: true, name: true } },
contract_registry: { select: { id: true, contract_no: true } },
},
});
if (rows.some((item) => ["confidential", "restricted"].includes(item.confidentiality_level))) {
await writeAudit(fastify, request, "CONFIDENTIAL_DOC_REF_VIEW", rows[0]?.id ?? request.employee.id, { count: rows.length, query: parsed.data });
}
return sendData(reply, request, { items: rows.map(mapRow) });
});
fastify.post("/confidential-doc-refs", { preHandler: fastify.authenticate }, async (request, reply) => {
await ensureFeatureEnabled(fastify, request);
if (!canManage(fastify, request.employee)) {
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "project.manage" } });
}
const body = parseBody(WRITE_SCHEMA, request.body);
const created = await fastify.prisma.confidentialDocumentRef.create({
data: {
project_id: body.projectId ?? null,
contract_registry_id: body.contractRegistryId ?? null,
title: body.title,
document_no: body.documentNo ?? null,
confidentiality_level: body.confidentialityLevel ?? "internal",
storage_type: body.storageType,
storage_path: body.storagePath,
offline_archive_no: body.offlineArchiveNo ?? null,
owner_employee_id: body.ownerEmployeeId ?? request.employee.id,
access_rule_json: body.accessRuleJson ?? null,
},
include: {
owner: { select: { id: true, employee_no: true, name: true } },
project: { select: { id: true, project_no: true, name: true } },
contract_registry: { select: { id: true, contract_no: true } },
},
});
await writeAudit(fastify, request, "CONFIDENTIAL_DOC_REF_CREATED", created.id, { confidentialityLevel: created.confidentiality_level });
return reply.code(201).send({ requestId: request.id, data: { item: mapRow(created) } });
});
fastify.patch("/confidential-doc-refs/:id", { preHandler: fastify.authenticate }, async (request, reply) => {
await ensureFeatureEnabled(fastify, request);
if (!canManage(fastify, request.employee)) {
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "project.manage" } });
}
const refId = UUID_SCHEMA.parse(request.params?.id);
const body = parseBody(PATCH_SCHEMA, request.body);
const updated = await fastify.prisma.confidentialDocumentRef.update({
where: { id: refId },
data: {
...(body.projectId !== undefined ? { project_id: body.projectId ?? null } : {}),
...(body.contractRegistryId !== undefined ? { contract_registry_id: body.contractRegistryId ?? null } : {}),
...(body.title !== undefined ? { title: body.title } : {}),
...(body.documentNo !== undefined ? { document_no: body.documentNo ?? null } : {}),
...(body.confidentialityLevel !== undefined ? { confidentiality_level: body.confidentialityLevel } : {}),
...(body.storageType !== undefined ? { storage_type: body.storageType } : {}),
...(body.storagePath !== undefined ? { storage_path: body.storagePath } : {}),
...(body.offlineArchiveNo !== undefined ? { offline_archive_no: body.offlineArchiveNo ?? null } : {}),
...(body.ownerEmployeeId !== undefined ? { owner_employee_id: body.ownerEmployeeId } : {}),
...(body.accessRuleJson !== undefined ? { access_rule_json: body.accessRuleJson ?? null } : {}),
},
include: {
owner: { select: { id: true, employee_no: true, name: true } },
project: { select: { id: true, project_no: true, name: true } },
contract_registry: { select: { id: true, contract_no: true } },
},
});
await writeAudit(fastify, request, "CONFIDENTIAL_DOC_REF_UPDATED", updated.id, { fields: Object.keys(body) });
return sendData(reply, request, { item: mapRow(updated) });
});
}
module.exports = confidentialDocRefsRoutes;