172 行
8.8 KiB
JavaScript
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;
|