diff --git a/apps/server/src/routes/api/$.ts b/apps/server/src/routes/api/$.ts index 5246b3d..3b493eb 100644 --- a/apps/server/src/routes/api/$.ts +++ b/apps/server/src/routes/api/$.ts @@ -16,6 +16,7 @@ const handler = new OpenAPIHandler(router, { info: { title: name, version, + description: 'UX 授权服务 OpenAPI 文档:设备授权、任务解密、摘要加密与报告签名打包接口。', }, }, docsPath: '/docs', diff --git a/apps/server/src/server/api/contracts/crypto.contract.ts b/apps/server/src/server/api/contracts/crypto.contract.ts index 052e48f..80fece0 100644 --- a/apps/server/src/server/api/contracts/crypto.contract.ts +++ b/apps/server/src/server/api/contracts/crypto.contract.ts @@ -2,65 +2,103 @@ import { oc } from '@orpc/contract' import { z } from 'zod' export const encryptDeviceInfo = oc + .route({ + method: 'POST', + path: '/crypto/encrypt-device-info', + operationId: 'encryptDeviceInfo', + summary: '生成设备授权密文', + description: + '按 deviceId 查询已注册设备,使用设备记录中的 platformPublicKey 对 {licence, fingerprint} 做 RSA-OAEP 加密,返回 Base64 密文。', + tags: ['Crypto'], + }) .input( z.object({ - deviceId: z.string().min(1), + deviceId: z.string().min(1).describe('设备 ID'), }), ) .output( z.object({ - encrypted: z.string(), + encrypted: z.string().describe('Base64 密文(用于设备授权二维码)'), }), ) export const decryptTask = oc + .route({ + method: 'POST', + path: '/crypto/decrypt-task', + operationId: 'decryptTask', + summary: '解密任务二维码', + description: + '按 deviceId 查询已注册设备,使用 UTF-8 编码下的 licence 与 fingerprint 直接拼接(无分隔符)后取 SHA256 作为 AES-256-GCM 密钥解密任务密文。', + tags: ['Crypto'], + }) .input( z.object({ - deviceId: z.string().min(1), - encryptedData: z.string().min(1), + deviceId: z.string().min(1).describe('设备 ID'), + encryptedData: z.string().min(1).describe('任务二维码中的 Base64 密文'), }), ) .output( z.object({ - taskId: z.string(), - enterpriseId: z.string(), - orgName: z.string(), - inspectionId: z.string(), - inspectionPerson: z.string(), - issuedAt: z.number(), + taskId: z.string().describe('任务 ID'), + enterpriseId: z.string().describe('企业 ID'), + orgName: z.string().describe('单位名称'), + inspectionId: z.string().describe('检查 ID'), + inspectionPerson: z.string().describe('检查人'), + issuedAt: z.number().describe('任务发布时间戳(毫秒)'), }), ) export const encryptSummary = oc + .route({ + method: 'POST', + path: '/crypto/encrypt-summary', + operationId: 'encryptSummary', + summary: '加密摘要二维码内容', + description: '按 deviceId 查询已注册设备,使用 HKDF-SHA256 + AES-256-GCM 加密摘要信息,返回二维码 JSON 字符串。', + tags: ['Crypto'], + }) .input( z.object({ - deviceId: z.string().min(1), - taskId: z.string().min(1), - enterpriseId: z.string().min(1), - inspectionId: z.string().min(1), - summary: z.string().min(1), + deviceId: z.string().min(1).describe('设备 ID'), + taskId: z.string().min(1).describe('任务 ID'), + enterpriseId: z.string().min(1).describe('企业 ID'), + inspectionId: z.string().min(1).describe('检查 ID'), + summary: z.string().min(1).describe('摘要明文'), }), ) .output( z.object({ - qrContent: z.string(), + qrContent: z.string().describe('二维码内容 JSON:{"taskId":"...","encrypted":"..."}'), }), ) export const signAndPackReport = oc + .route({ + method: 'POST', + path: '/crypto/sign-and-pack-report', + operationId: 'signAndPackReport', + summary: '签名并打包报告 ZIP', + description: + '接收原始 ZIP(multipart/form-data 文件字段 rawZip),由 UX 生成 summary.json、manifest.json、signature.asc,并返回 signedZipBase64。', + tags: ['Crypto', 'Report'], + }) .input( z.object({ - deviceId: z.string().min(1), - taskId: z.string().min(1), - enterpriseId: z.string().min(1), - inspectionId: z.string().min(1), - summary: z.string().min(1), - rawZip: z.file().mime(['application/zip', 'application/x-zip-compressed']), + deviceId: z.string().min(1).describe('设备 ID'), + taskId: z.string().min(1).describe('任务 ID'), + enterpriseId: z.string().min(1).describe('企业 ID'), + inspectionId: z.string().min(1).describe('检查 ID'), + summary: z.string().min(1).describe('检查摘要明文'), + rawZip: z + .file() + .mime(['application/zip', 'application/x-zip-compressed']) + .describe('原始报告 ZIP 文件(multipart/form-data 字段)'), }), ) .output( z.object({ - deviceSignature: z.string(), - signedZipBase64: z.string(), + deviceSignature: z.string().describe('设备签名(HMAC-SHA256 Base64)'), + signedZipBase64: z.string().describe('签名后 ZIP 的 Base64 编码'), }), ) diff --git a/apps/server/src/server/api/contracts/device.contract.ts b/apps/server/src/server/api/contracts/device.contract.ts index def7a63..666b72c 100644 --- a/apps/server/src/server/api/contracts/device.contract.ts +++ b/apps/server/src/server/api/contracts/device.contract.ts @@ -2,29 +2,45 @@ import { oc } from '@orpc/contract' import { z } from 'zod' const deviceOutput = z.object({ - id: z.string(), - licence: z.string(), - fingerprint: z.string(), - platformPublicKey: z.string(), - pgpPublicKey: z.string().nullable(), - createdAt: z.date(), - updatedAt: z.date(), + id: z.string().describe('设备主键 ID'), + licence: z.string().describe('设备授权码 licence'), + fingerprint: z.string().describe('UX 计算并持久化的设备指纹'), + platformPublicKey: z.string().describe('平台公钥(Base64,SPKI DER)'), + pgpPublicKey: z.string().nullable().describe('设备 OpenPGP 公钥(ASCII armored)'), + createdAt: z.date().describe('记录创建时间'), + updatedAt: z.date().describe('记录更新时间'), }) export const register = oc + .route({ + method: 'POST', + path: '/device/register', + operationId: 'deviceRegister', + summary: '注册设备', + description: '注册 licence 与平台公钥,指纹由 UX 本机计算,返回设备信息。', + tags: ['Device'], + }) .input( z.object({ - licence: z.string().min(1), - platformPublicKey: z.string().min(1), + licence: z.string().min(1).describe('设备授权码 licence'), + platformPublicKey: z.string().min(1).describe('平台公钥(Base64,SPKI DER)'), }), ) .output(deviceOutput) export const get = oc + .route({ + method: 'POST', + path: '/device/get', + operationId: 'deviceGet', + summary: '查询设备', + description: '按 id 或 licence 查询设备信息。', + tags: ['Device'], + }) .input( z.object({ - id: z.string().optional(), - licence: z.string().optional(), + id: z.string().optional().describe('设备 ID,与 licence 二选一'), + licence: z.string().optional().describe('设备授权码,与 id 二选一'), }), ) .output(deviceOutput) diff --git a/apps/server/src/server/api/contracts/task.contract.ts b/apps/server/src/server/api/contracts/task.contract.ts index edf5cf4..5fc3dc4 100644 --- a/apps/server/src/server/api/contracts/task.contract.ts +++ b/apps/server/src/server/api/contracts/task.contract.ts @@ -2,46 +2,70 @@ import { oc } from '@orpc/contract' import { z } from 'zod' const taskOutput = z.object({ - id: z.string(), - deviceId: z.string(), - taskId: z.string(), - enterpriseId: z.string().nullable(), - orgName: z.string().nullable(), - inspectionId: z.string().nullable(), - inspectionPerson: z.string().nullable(), - issuedAt: z.date().nullable(), - status: z.enum(['pending', 'in_progress', 'done']), - createdAt: z.date(), - updatedAt: z.date(), + id: z.string().describe('任务记录 ID'), + deviceId: z.string().describe('设备 ID'), + taskId: z.string().describe('任务业务 ID'), + enterpriseId: z.string().nullable().describe('企业 ID'), + orgName: z.string().nullable().describe('单位名称'), + inspectionId: z.string().nullable().describe('检查 ID'), + inspectionPerson: z.string().nullable().describe('检查人'), + issuedAt: z.date().nullable().describe('任务发布时间(ISO date-time;由毫秒时间戳转换后存储)'), + status: z.enum(['pending', 'in_progress', 'done']).describe('任务状态'), + createdAt: z.date().describe('记录创建时间'), + updatedAt: z.date().describe('记录更新时间'), }) export const save = oc + .route({ + method: 'POST', + path: '/task/save', + operationId: 'taskSave', + summary: '保存任务', + description: '保存解密后的任务信息到 UX 数据库。', + tags: ['Task'], + }) .input( z.object({ - deviceId: z.string().min(1), - taskId: z.string().min(1), - enterpriseId: z.string().optional(), - orgName: z.string().optional(), - inspectionId: z.string().optional(), - inspectionPerson: z.string().optional(), - issuedAt: z.number().optional(), + deviceId: z.string().min(1).describe('设备 ID'), + taskId: z.string().min(1).describe('任务 ID'), + enterpriseId: z.string().optional().describe('企业 ID'), + orgName: z.string().optional().describe('单位名称'), + inspectionId: z.string().optional().describe('检查 ID'), + inspectionPerson: z.string().optional().describe('检查人'), + issuedAt: z.number().optional().describe('任务发布时间戳(毫秒)'), }), ) .output(taskOutput) export const list = oc + .route({ + method: 'POST', + path: '/task/list', + operationId: 'taskList', + summary: '查询任务列表', + description: '按设备 ID 查询任务列表。', + tags: ['Task'], + }) .input( z.object({ - deviceId: z.string().min(1), + deviceId: z.string().min(1).describe('设备 ID'), }), ) .output(z.array(taskOutput)) export const updateStatus = oc + .route({ + method: 'POST', + path: '/task/update-status', + operationId: 'taskUpdateStatus', + summary: '更新任务状态', + description: '按记录 ID 更新任务状态。', + tags: ['Task'], + }) .input( z.object({ - id: z.string().min(1), - status: z.enum(['pending', 'in_progress', 'done']), + id: z.string().min(1).describe('任务记录 ID'), + status: z.enum(['pending', 'in_progress', 'done']).describe('目标状态'), }), ) .output(taskOutput)