[Snow Team] express-types: auto-commit work

This commit is contained in:
yml
2026-05-21 22:57:48 +08:00
parent c72396ca15
commit 0e6bf88beb
10 changed files with 639 additions and 463 deletions
+89
View File
@@ -13,6 +13,7 @@
"playwright": "1.42.1" "playwright": "1.42.1"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^5.0.6",
"@types/node": "^25.6.0", "@types/node": "^25.6.0",
"@types/pg": "^8.20.0", "@types/pg": "^8.20.0",
"tsx": "^4.22.3", "tsx": "^4.22.3",
@@ -461,6 +462,59 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@types/body-parser": {
"version": "1.19.6",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/connect": "*",
"@types/node": "*"
}
},
"node_modules/@types/connect": {
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/express": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
"integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^5.0.0",
"@types/serve-static": "^2"
}
},
"node_modules/@types/express-serve-static-core": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz",
"integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*",
"@types/send": "*"
}
},
"node_modules/@types/http-errors": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "25.6.0", "version": "25.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
@@ -483,6 +537,41 @@
"pg-types": "^2.2.0" "pg-types": "^2.2.0"
} }
}, },
"node_modules/@types/qs": {
"version": "6.15.1",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
"integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/send": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
"integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/serve-static": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
"integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/http-errors": "*",
"@types/node": "*"
}
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+1
View File
@@ -23,6 +23,7 @@
"playwright": "1.42.1" "playwright": "1.42.1"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^5.0.6",
"@types/node": "^25.6.0", "@types/node": "^25.6.0",
"@types/pg": "^8.20.0", "@types/pg": "^8.20.0",
"tsx": "^4.22.3", "tsx": "^4.22.3",
+2 -1
View File
@@ -1,3 +1,4 @@
import type { Request, Response, NextFunction } from "express";
import express from "express"; import express from "express";
import process from "node:process"; import process from "node:process";
@@ -92,7 +93,7 @@ export function createApp({
app.use("/api/v1/claim", claimsRouter); app.use("/api/v1/claim", claimsRouter);
app.use("/api/v1/admin", adminRouter); app.use("/api/v1/admin", adminRouter);
app.use((err, _req, res, _next) => { app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => {
sendRouteError(res, err, "服务内部错误", "[global]"); sendRouteError(res, err, "服务内部错误", "[global]");
}); });
+12 -4
View File
@@ -1,6 +1,11 @@
import type { Request, Response, NextFunction } from "express";
import { createRequestId, logInfo } from "../utils/logger.js"; import { createRequestId, logInfo } from "../utils/logger.js";
export function accessLogMiddleware(req, res, next) { export function accessLogMiddleware(
req: Request,
res: Response,
next: NextFunction
): void {
const startedAt = Date.now(); const startedAt = Date.now();
const requestId = resolveRequestId(req); const requestId = resolveRequestId(req);
@@ -32,12 +37,15 @@ export function accessLogMiddleware(req, res, next) {
next(); next();
} }
function resolveRequestId(req) { function resolveRequestId(req: Request): string {
const fromHeader = String(req.headers["x-request-id"] || "").trim(); const fromHeader = String(req.headers["x-request-id"] || "").trim();
return fromHeader || createRequestId("req"); return fromHeader || createRequestId("req");
} }
function shouldSkipAccessLog(originalUrl, statusCode) { function shouldSkipAccessLog(originalUrl: string, statusCode: number): boolean {
const pathname = String(originalUrl || "").split("?")[0]; const pathname = String(originalUrl || "").split("?")[0];
return ["/health", "/health/live", "/health/ready"].includes(pathname) && Number(statusCode) < 400; return (
["/health", "/health/live", "/health/ready"].includes(pathname) &&
Number(statusCode) < 400
);
} }
+11 -2
View File
@@ -1,7 +1,16 @@
export function corsMiddleware(req, res, next) { import type { Request, Response, NextFunction } from "express";
export function corsMiddleware(
req: Request,
res: Response,
next: NextFunction
): void {
res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS"); res.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, PUT, PATCH, DELETE, OPTIONS"
);
if (req.method === "OPTIONS") { if (req.method === "OPTIONS") {
res.sendStatus(204); res.sendStatus(204);
+10 -23
View File
@@ -1,23 +1,10 @@
import type { Request, Response, NextFunction } from 'express'
import { requireAdminRole, verifyAdminSessionToken } from '../../services/admin/admin-auth-service.js' import { requireAdminRole, verifyAdminSessionToken } from '../../services/admin/admin-auth-service.js'
import { writeAdminAuditLog } from '../../services/admin/admin-audit-service.js' import { writeAdminAuditLog } from '../../services/admin/admin-audit-service.js'
import { buildSuccessPayload, createHttpError, sendRouteError } from '../../utils/http.js' import { buildSuccessPayload, createHttpError, sendRouteError } from '../../utils/http.js'
import { logWarn } from '../../utils/logger.js' import { logWarn } from '../../utils/logger.js'
import type { AdminSession } from '../../services/admin/admin-auth-service.js' import type { AdminSession } from '../../services/admin/admin-auth-service.js'
type NextFunction = (error?: unknown) => void
type ExpressResponse = {
json: (body: unknown) => unknown
sendFile: (path: string) => unknown
}
export type AdminExpressRequest = {
headers: { authorization?: string | string[] }
body?: any
params?: Record<string, string | undefined>
query?: Record<string, unknown>
adminSession?: AdminSession | null
[key: string]: unknown
}
type AdminAuditPayload = { type AdminAuditPayload = {
action?: string action?: string
targetType?: string targetType?: string
@@ -29,7 +16,7 @@ type AdminJsonHandlerOptions = {
successMessage?: string successMessage?: string
errorMessage?: string errorMessage?: string
scope?: string scope?: string
audit?: (req: AdminExpressRequest, data: unknown) => AdminAuditPayload | null | undefined audit?: (req: Request, data: unknown) => AdminAuditPayload | null | undefined
} }
type AdminFileHandlerOptions = { type AdminFileHandlerOptions = {
@@ -38,10 +25,10 @@ type AdminFileHandlerOptions = {
} }
export function createJsonHandler( export function createJsonHandler(
action: (req: AdminExpressRequest, res: ExpressResponse) => unknown | Promise<unknown>, action: (req: Request, res: Response) => unknown | Promise<unknown>,
{ successMessage = 'ok', errorMessage, scope, audit }: AdminJsonHandlerOptions = {}, { successMessage = 'ok', errorMessage, scope, audit }: AdminJsonHandlerOptions = {},
) { ) {
return async (req, res) => { return async (req: Request, res: Response): Promise<void> => {
try { try {
const data = await action(req, res) const data = await action(req, res)
await recordAdminAudit(req.adminSession, req, data, audit) await recordAdminAudit(req.adminSession, req, data, audit)
@@ -53,10 +40,10 @@ export function createJsonHandler(
} }
export function createFileHandler( export function createFileHandler(
action: (req: AdminExpressRequest, res: ExpressResponse) => string | Promise<string>, action: (req: Request, res: Response) => string | Promise<string>,
{ errorMessage, scope }: AdminFileHandlerOptions = {}, { errorMessage, scope }: AdminFileHandlerOptions = {},
) { ) {
return async (req, res) => { return async (req: Request, res: Response): Promise<void> => {
try { try {
const filePath = await action(req, res) const filePath = await action(req, res)
res.sendFile(filePath) res.sendFile(filePath)
@@ -66,7 +53,7 @@ export function createFileHandler(
} }
} }
export async function requireAdminSession(req: AdminExpressRequest, res: ExpressResponse, next: NextFunction) { export async function requireAdminSession(req: Request, res: Response, next: NextFunction): Promise<void> {
try { try {
req.adminSession = await verifyAdminSessionToken(extractBearerToken(req)) req.adminSession = await verifyAdminSessionToken(extractBearerToken(req))
next() next()
@@ -76,7 +63,7 @@ export async function requireAdminSession(req: AdminExpressRequest, res: Express
} }
export function requireAdminRoles(allowedRoles: string[]) { export function requireAdminRoles(allowedRoles: string[]) {
return (req, res, next) => { return (req: Request, res: Response, next: NextFunction): void => {
try { try {
requireAdminRole(req.adminSession, allowedRoles) requireAdminRole(req.adminSession, allowedRoles)
next() next()
@@ -88,7 +75,7 @@ export function requireAdminRoles(allowedRoles: string[]) {
async function recordAdminAudit( async function recordAdminAudit(
session: AdminSession | null | undefined, session: AdminSession | null | undefined,
req: AdminExpressRequest, req: Request,
data: unknown, data: unknown,
audit: AdminJsonHandlerOptions['audit'], audit: AdminJsonHandlerOptions['audit'],
) { ) {
@@ -114,7 +101,7 @@ async function recordAdminAudit(
} }
} }
export function extractBearerToken(req: AdminExpressRequest) { export function extractBearerToken(req: Request): string {
const authorization = String(req.headers.authorization || '').trim() const authorization = String(req.headers.authorization || '').trim()
const matched = authorization.match(/^Bearer\s+(.+)$/i) const matched = authorization.match(/^Bearer\s+(.+)$/i)
+246 -204
View File
@@ -1,4 +1,4 @@
import { Router } from 'express' import { Router } from "express";
import { import {
closeAdminTask, closeAdminTask,
@@ -14,116 +14,125 @@ import {
releaseAdminTaskInventory, releaseAdminTaskInventory,
returnNumberAdminTaskKuaishouCloudFulfillment, returnNumberAdminTaskKuaishouCloudFulfillment,
retryAdminTask, retryAdminTask,
} from '../../services/admin/admin-write-service.js' } from "../../services/admin/admin-write-service.js";
import { import {
getAdminTaskDetail, getAdminTaskDetail,
getAdminTaskScreenshotPath, getAdminTaskScreenshotPath,
getAdminTasks, getAdminTasks,
} from '../../services/admin/admin-read-service.js' } from "../../services/admin/admin-read-service.js";
import { createFileHandler, createJsonHandler, requireAdminRoles } from './shared.js' import {
import type { AdminExpressRequest } from './shared.js' createFileHandler,
createJsonHandler,
requireAdminRoles,
} from "./shared.js";
import type { Request } from "express";
import type { import type {
AdminTaskKuaishouCloudDispatchRouteBody, AdminTaskKuaishouCloudDispatchRouteBody,
AdminTaskManualDispatchRouteBody, AdminTaskManualDispatchRouteBody,
AdminTaskRouteParams, AdminTaskRouteParams,
AdminTaskRouteQuery, AdminTaskRouteQuery,
} from '../../types/admin-route-inputs.js' } from "../../types/admin-route-inputs.js";
import type { import type {
AdminTaskActionResponse, AdminTaskActionResponse,
AdminTaskBindingReleaseResponse, AdminTaskBindingReleaseResponse,
AdminTaskManualDispatchResponse, AdminTaskManualDispatchResponse,
} from '../../types/admin-write-models.js' } from "../../types/admin-write-models.js";
const router = Router() const router = Router();
function getTaskId(req: AdminExpressRequest): string | undefined { function getTaskId(req: Request): string | undefined {
return (req.params as AdminTaskRouteParams).taskId return (req.params as AdminTaskRouteParams).taskId;
} }
function getBindingId(req: AdminExpressRequest): string | undefined { function getBindingId(req: Request): string | undefined {
return (req.params as AdminTaskRouteParams).bindingId return (req.params as AdminTaskRouteParams).bindingId;
} }
router.get('/tasks', createJsonHandler( router.get(
(req) => getAdminTasks( "/tasks",
req.query as AdminTaskRouteQuery, createJsonHandler(
req.adminSession || null, (req) =>
), getAdminTasks(req.query as AdminTaskRouteQuery, req.adminSession || null),
{ {
successMessage: 'ok', successMessage: "ok",
errorMessage: '读取任务列表失败', errorMessage: "读取任务列表失败",
scope: '[admin/tasks]', scope: "[admin/tasks]",
}, }
)) )
);
router.get('/tasks/:taskId', createJsonHandler( router.get(
(req) => getAdminTaskDetail( "/tasks/:taskId",
getTaskId(req), createJsonHandler(
req.adminSession || null, (req) => getAdminTaskDetail(getTaskId(req), req.adminSession || null),
),
{ {
successMessage: 'ok', successMessage: "ok",
errorMessage: '读取任务详情失败', errorMessage: "读取任务详情失败",
scope: '[admin/tasks/:taskId]', scope: "[admin/tasks/:taskId]",
}, }
)) )
);
router.get('/tasks/:taskId/screenshot', createFileHandler( router.get(
(req) => getAdminTaskScreenshotPath( "/tasks/:taskId/screenshot",
getTaskId(req), createFileHandler(
req.adminSession || null, (req) =>
), getAdminTaskScreenshotPath(getTaskId(req), req.adminSession || null),
{ {
errorMessage: '读取任务截图失败', errorMessage: "读取任务截图失败",
scope: '[admin/tasks/:taskId/screenshot]', scope: "[admin/tasks/:taskId/screenshot]",
}, }
)) )
);
router.post('/tasks/:taskId/retry', requireAdminRoles(['admin', 'operator']), createJsonHandler( router.post(
(req) => retryAdminTask(getTaskId(req)), "/tasks/:taskId/retry",
{ requireAdminRoles(["admin", "operator"]),
successMessage: '任务已重试', createJsonHandler((req) => retryAdminTask(getTaskId(req)), {
errorMessage: '重试任务失败', successMessage: "任务已重试",
scope: '[admin/tasks/:taskId/retry]', errorMessage: "重试任务失败",
}, scope: "[admin/tasks/:taskId/retry]",
)) })
);
router.post('/tasks/:taskId/release-inventory', requireAdminRoles(['admin']), createJsonHandler( router.post(
(req) => releaseAdminTaskInventory(getTaskId(req)), "/tasks/:taskId/release-inventory",
{ requireAdminRoles(["admin"]),
successMessage: '库存凭据已释放', createJsonHandler((req) => releaseAdminTaskInventory(getTaskId(req)), {
errorMessage: '释放库存凭据失败', successMessage: "库存凭据已释放",
scope: '[admin/tasks/:taskId/release-inventory]', errorMessage: "释放库存凭据失败",
scope: "[admin/tasks/:taskId/release-inventory]",
audit: (req, data) => { audit: (req, data) => {
const result = data as AdminTaskActionResponse const result = data as AdminTaskActionResponse;
return { return {
action: 'task_release_inventory', action: "task_release_inventory",
targetType: 'task', targetType: "task",
targetId: String(getTaskId(req)), targetId: String(getTaskId(req)),
data: { data: {
taskId: result.task.taskId, taskId: result.task.taskId,
taskNo: result.task.taskNo, taskNo: result.task.taskNo,
inventoryItemId: result.task.inventoryItemId, inventoryItemId: result.task.inventoryItemId,
}, },
} };
}, },
}, })
)) );
router.post('/tasks/:taskId/inventory-bindings/:bindingId/release', requireAdminRoles(['admin']), createJsonHandler( router.post(
(req) => releaseAdminTaskInventoryBinding( "/tasks/:taskId/inventory-bindings/:bindingId/release",
getTaskId(req), requireAdminRoles(["admin"]),
getBindingId(req), createJsonHandler(
), (req) =>
releaseAdminTaskInventoryBinding(getTaskId(req), getBindingId(req)),
{ {
successMessage: '库存绑定已释放', successMessage: "库存绑定已释放",
errorMessage: '释放库存绑定失败', errorMessage: "释放库存绑定失败",
scope: '[admin/tasks/:taskId/inventory-bindings/:bindingId/release]', scope: "[admin/tasks/:taskId/inventory-bindings/:bindingId/release]",
audit: (req, data) => { audit: (req, data) => {
const result = data as AdminTaskBindingReleaseResponse const result = data as AdminTaskBindingReleaseResponse;
return { return {
action: 'task_release_inventory_binding', action: "task_release_inventory_binding",
targetType: 'task_inventory_binding', targetType: "task_inventory_binding",
targetId: String(getBindingId(req)), targetId: String(getBindingId(req)),
data: { data: {
taskId: result.task.taskId, taskId: result.task.taskId,
@@ -131,75 +140,80 @@ router.post('/tasks/:taskId/inventory-bindings/:bindingId/release', requireAdmin
bindingId: result.bindingId, bindingId: result.bindingId,
inventoryItemId: result.inventoryItemId, inventoryItemId: result.inventoryItemId,
}, },
};
},
} }
}, )
}, );
))
router.post('/tasks/:taskId/regenerate-claim-link', requireAdminRoles(['admin', 'operator', 'support']), createJsonHandler( router.post(
(req) => regenerateAdminTaskClaimLink( "/tasks/:taskId/regenerate-claim-link",
getTaskId(req), requireAdminRoles(["admin", "operator", "support"]),
req.adminSession || null, createJsonHandler(
), (req) =>
regenerateAdminTaskClaimLink(getTaskId(req), req.adminSession || null),
{ {
successMessage: '领取链接已重新生成', successMessage: "领取链接已重新生成",
errorMessage: '重新生成领取链接失败', errorMessage: "重新生成领取链接失败",
scope: '[admin/tasks/:taskId/regenerate-claim-link]', scope: "[admin/tasks/:taskId/regenerate-claim-link]",
audit: (req, data) => { audit: (req, data) => {
const result = data as AdminTaskActionResponse const result = data as AdminTaskActionResponse;
return { return {
action: 'task_regenerate_claim_link', action: "task_regenerate_claim_link",
targetType: 'task', targetType: "task",
targetId: String(getTaskId(req)), targetId: String(getTaskId(req)),
data: { data: {
taskId: result.task.taskId, taskId: result.task.taskId,
taskNo: result.task.taskNo, taskNo: result.task.taskNo,
primaryClaimTokenId: result.task.primaryClaimTokenId, primaryClaimTokenId: result.task.primaryClaimTokenId,
}, },
};
},
} }
}, )
}, );
))
router.post('/tasks/:taskId/support-confirm-role', requireAdminRoles(['admin', 'operator', 'support']), createJsonHandler( router.post(
(req) => confirmAdminTaskAssistedRole( "/tasks/:taskId/support-confirm-role",
getTaskId(req), requireAdminRoles(["admin", "operator", "support"]),
req.adminSession || null, createJsonHandler(
), (req) =>
confirmAdminTaskAssistedRole(getTaskId(req), req.adminSession || null),
{ {
successMessage: '角色已确认', successMessage: "角色已确认",
errorMessage: '客服确认角色失败', errorMessage: "客服确认角色失败",
scope: '[admin/tasks/:taskId/support-confirm-role]', scope: "[admin/tasks/:taskId/support-confirm-role]",
audit: (req, data) => { audit: (req, data) => {
const result = data as AdminTaskActionResponse const result = data as AdminTaskActionResponse;
return { return {
action: 'task_support_confirm_role', action: "task_support_confirm_role",
targetType: 'task', targetType: "task",
targetId: String(getTaskId(req)), targetId: String(getTaskId(req)),
data: { data: {
taskId: result.task.taskId, taskId: result.task.taskId,
taskNo: result.task.taskNo, taskNo: result.task.taskNo,
status: result.task.status, status: result.task.status,
}, },
};
},
} }
}, )
}, );
))
router.post('/tasks/:taskId/support-redeem', requireAdminRoles(['admin', 'operator', 'support']), createJsonHandler( router.post(
(req) => redeemAdminTaskAssisted( "/tasks/:taskId/support-redeem",
getTaskId(req), requireAdminRoles(["admin", "operator", "support"]),
req.adminSession || null, createJsonHandler(
), (req) => redeemAdminTaskAssisted(getTaskId(req), req.adminSession || null),
{ {
successMessage: '兑换任务已启动', successMessage: "兑换任务已启动",
errorMessage: '客服发起兑换失败', errorMessage: "客服发起兑换失败",
scope: '[admin/tasks/:taskId/support-redeem]', scope: "[admin/tasks/:taskId/support-redeem]",
audit: (req, data) => { audit: (req, data) => {
const result = data as AdminTaskActionResponse const result = data as AdminTaskActionResponse;
return { return {
action: 'task_support_redeem', action: "task_support_redeem",
targetType: 'task', targetType: "task",
targetId: String(getTaskId(req)), targetId: String(getTaskId(req)),
data: { data: {
taskId: result.task.taskId, taskId: result.task.taskId,
@@ -207,70 +221,77 @@ router.post('/tasks/:taskId/support-redeem', requireAdminRoles(['admin', 'operat
status: result.task.status, status: result.task.status,
deliveryStatus: result.task.deliveryStatus, deliveryStatus: result.task.deliveryStatus,
}, },
};
},
} }
}, )
}, );
))
router.post('/tasks/:taskId/close', requireAdminRoles(['admin', 'operator', 'support']), createJsonHandler( router.post(
(req) => closeAdminTask(getTaskId(req)), "/tasks/:taskId/close",
{ requireAdminRoles(["admin", "operator", "support"]),
successMessage: '任务已关闭', createJsonHandler((req) => closeAdminTask(getTaskId(req)), {
errorMessage: '关闭任务失败', successMessage: "任务已关闭",
scope: '[admin/tasks/:taskId/close]', errorMessage: "关闭任务失败",
scope: "[admin/tasks/:taskId/close]",
audit: (req, data) => { audit: (req, data) => {
const result = data as AdminTaskActionResponse const result = data as AdminTaskActionResponse;
return { return {
action: 'task_closed', action: "task_closed",
targetType: 'task', targetType: "task",
targetId: String(getTaskId(req)), targetId: String(getTaskId(req)),
data: { data: {
taskId: result.task.taskId, taskId: result.task.taskId,
taskNo: result.task.taskNo, taskNo: result.task.taskNo,
status: result.task.status, status: result.task.status,
}, },
} };
}, },
}, })
)) );
router.post('/tasks/:taskId/mark-manual-review', requireAdminRoles(['admin', 'operator']), createJsonHandler( router.post(
(req) => markAdminTaskManualReview(getTaskId(req)), "/tasks/:taskId/mark-manual-review",
{ requireAdminRoles(["admin", "operator"]),
successMessage: '任务已转人工处理', createJsonHandler((req) => markAdminTaskManualReview(getTaskId(req)), {
errorMessage: '标记人工处理失败', successMessage: "任务已转人工处理",
scope: '[admin/tasks/:taskId/mark-manual-review]', errorMessage: "标记人工处理失败",
scope: "[admin/tasks/:taskId/mark-manual-review]",
audit: (req, data) => { audit: (req, data) => {
const result = data as AdminTaskActionResponse const result = data as AdminTaskActionResponse;
return { return {
action: 'task_mark_manual_review', action: "task_mark_manual_review",
targetType: 'task', targetType: "task",
targetId: String(getTaskId(req)), targetId: String(getTaskId(req)),
data: { data: {
taskId: result.task.taskId, taskId: result.task.taskId,
taskNo: result.task.taskNo, taskNo: result.task.taskNo,
status: result.task.status, status: result.task.status,
}, },
} };
}, },
}, })
)) );
router.post('/tasks/:taskId/complete-manual-dispatch', requireAdminRoles(['admin', 'operator']), createJsonHandler( router.post(
(req) => completeAdminTaskManualDispatch( "/tasks/:taskId/complete-manual-dispatch",
requireAdminRoles(["admin", "operator"]),
createJsonHandler(
(req) =>
completeAdminTaskManualDispatch(
getTaskId(req), getTaskId(req),
req.body as AdminTaskManualDispatchRouteBody, req.body as AdminTaskManualDispatchRouteBody,
req.adminSession || null, req.adminSession || null
), ),
{ {
successMessage: '人工履约结果已回写', successMessage: "人工履约结果已回写",
errorMessage: '回写人工履约结果失败', errorMessage: "回写人工履约结果失败",
scope: '[admin/tasks/:taskId/complete-manual-dispatch]', scope: "[admin/tasks/:taskId/complete-manual-dispatch]",
audit: (req, data) => { audit: (req, data) => {
const result = data as AdminTaskManualDispatchResponse const result = data as AdminTaskManualDispatchResponse;
return { return {
action: 'task_complete_manual_dispatch', action: "task_complete_manual_dispatch",
targetType: 'task', targetType: "task",
targetId: String(getTaskId(req)), targetId: String(getTaskId(req)),
data: { data: {
taskId: result.task.taskId, taskId: result.task.taskId,
@@ -279,113 +300,134 @@ router.post('/tasks/:taskId/complete-manual-dispatch', requireAdminRoles(['admin
deliveryStatus: result.task.deliveryStatus, deliveryStatus: result.task.deliveryStatus,
resultCode: result.task.resultCode, resultCode: result.task.resultCode,
}, },
};
},
} }
}, )
}, );
))
router.post('/tasks/:taskId/kuaishou-cloud/prepare', requireAdminRoles(['admin', 'operator']), createJsonHandler( router.post(
(req) => prepareAdminTaskKuaishouCloudFulfillment( "/tasks/:taskId/kuaishou-cloud/prepare",
requireAdminRoles(["admin", "operator"]),
createJsonHandler(
(req) =>
prepareAdminTaskKuaishouCloudFulfillment(
getTaskId(req), getTaskId(req),
req.adminSession || null, req.adminSession || null
), ),
{ {
successMessage: '绑定资源已准备完成', successMessage: "绑定资源已准备完成",
errorMessage: '准备绑定资源失败', errorMessage: "准备绑定资源失败",
scope: '[admin/tasks/:taskId/kuaishou-cloud/prepare]', scope: "[admin/tasks/:taskId/kuaishou-cloud/prepare]",
audit: (req, data) => { audit: (req, data) => {
const result = data as AdminTaskActionResponse const result = data as AdminTaskActionResponse;
return { return {
action: 'task_kuaishou_cloud_prepare', action: "task_kuaishou_cloud_prepare",
targetType: 'task', targetType: "task",
targetId: String(getTaskId(req)), targetId: String(getTaskId(req)),
data: { data: {
taskId: result.task.taskId, taskId: result.task.taskId,
taskNo: result.task.taskNo, taskNo: result.task.taskNo,
status: result.task.status, status: result.task.status,
}, },
};
},
} }
}, )
}, );
))
router.post('/tasks/:taskId/kuaishou-cloud/refresh-role-info', requireAdminRoles(['admin', 'operator', 'support']), createJsonHandler( router.post(
(req) => refreshAdminTaskKuaishouCloudRoleInfo( "/tasks/:taskId/kuaishou-cloud/refresh-role-info",
requireAdminRoles(["admin", "operator", "support"]),
createJsonHandler(
(req) =>
refreshAdminTaskKuaishouCloudRoleInfo(
getTaskId(req), getTaskId(req),
req.adminSession || null, req.adminSession || null
), ),
{ {
successMessage: '角色信息已刷新', successMessage: "角色信息已刷新",
errorMessage: '刷新角色信息失败', errorMessage: "刷新角色信息失败",
scope: '[admin/tasks/:taskId/kuaishou-cloud/refresh-role-info]', scope: "[admin/tasks/:taskId/kuaishou-cloud/refresh-role-info]",
audit: (req, data) => { audit: (req, data) => {
const result = data as AdminTaskActionResponse const result = data as AdminTaskActionResponse;
return { return {
action: 'task_kuaishou_cloud_role_info_refreshed', action: "task_kuaishou_cloud_role_info_refreshed",
targetType: 'task', targetType: "task",
targetId: String(getTaskId(req)), targetId: String(getTaskId(req)),
data: { data: {
taskId: result.task.taskId, taskId: result.task.taskId,
taskNo: result.task.taskNo, taskNo: result.task.taskNo,
status: result.task.status, status: result.task.status,
}, },
};
},
} }
}, )
}, );
))
router.post('/tasks/:taskId/kuaishou-cloud/dispatch', requireAdminRoles(['admin', 'operator']), createJsonHandler( router.post(
(req) => dispatchAdminTaskKuaishouCloudFulfillment( "/tasks/:taskId/kuaishou-cloud/dispatch",
requireAdminRoles(["admin", "operator"]),
createJsonHandler(
(req) =>
dispatchAdminTaskKuaishouCloudFulfillment(
getTaskId(req), getTaskId(req),
req.body as AdminTaskKuaishouCloudDispatchRouteBody, req.body as AdminTaskKuaishouCloudDispatchRouteBody,
req.adminSession || null, req.adminSession || null
), ),
{ {
successMessage: '已完成绑定确认并发货', successMessage: "已完成绑定确认并发货",
errorMessage: '执行发货失败', errorMessage: "执行发货失败",
scope: '[admin/tasks/:taskId/kuaishou-cloud/dispatch]', scope: "[admin/tasks/:taskId/kuaishou-cloud/dispatch]",
audit: (req, data) => { audit: (req, data) => {
const result = data as AdminTaskActionResponse const result = data as AdminTaskActionResponse;
const body = req.body as AdminTaskKuaishouCloudDispatchRouteBody const body = req.body as AdminTaskKuaishouCloudDispatchRouteBody;
return { return {
action: 'task_kuaishou_cloud_dispatch', action: "task_kuaishou_cloud_dispatch",
targetType: 'task', targetType: "task",
targetId: String(getTaskId(req)), targetId: String(getTaskId(req)),
data: { data: {
taskId: result.task.taskId, taskId: result.task.taskId,
taskNo: result.task.taskNo, taskNo: result.task.taskNo,
status: result.task.status, status: result.task.status,
deliveryStatus: result.task.deliveryStatus, deliveryStatus: result.task.deliveryStatus,
ticketCodeProvided: Boolean(String(body.ticketCode || '').trim()), ticketCodeProvided: Boolean(String(body.ticketCode || "").trim()),
},
};
}, },
} }
}, )
}, );
))
router.post('/tasks/:taskId/kuaishou-cloud/return-number', requireAdminRoles(['admin', 'operator']), createJsonHandler( router.post(
(req) => returnNumberAdminTaskKuaishouCloudFulfillment( "/tasks/:taskId/kuaishou-cloud/return-number",
requireAdminRoles(["admin", "operator"]),
createJsonHandler(
(req) =>
returnNumberAdminTaskKuaishouCloudFulfillment(
getTaskId(req), getTaskId(req),
req.adminSession || null, req.adminSession || null
), ),
{ {
successMessage: '号码已退还', successMessage: "号码已退还",
errorMessage: '退还号码失败', errorMessage: "退还号码失败",
scope: '[admin/tasks/:taskId/kuaishou-cloud/return-number]', scope: "[admin/tasks/:taskId/kuaishou-cloud/return-number]",
audit: (req, data) => { audit: (req, data) => {
const result = data as AdminTaskActionResponse const result = data as AdminTaskActionResponse;
return { return {
action: 'task_kuaishou_cloud_return_number', action: "task_kuaishou_cloud_return_number",
targetType: 'task', targetType: "task",
targetId: String(getTaskId(req)), targetId: String(getTaskId(req)),
data: { data: {
taskId: result.task.taskId, taskId: result.task.taskId,
taskNo: result.task.taskNo, taskNo: result.task.taskNo,
status: result.task.status, status: result.task.status,
}, },
};
},
} }
}, )
}, );
))
export default router export default router;
+4 -4
View File
@@ -56,7 +56,7 @@ router.post('/users', createJsonHandler(
)) ))
router.post('/users/:userId/role', createJsonHandler( router.post('/users/:userId/role', createJsonHandler(
(req) => updateManagedAdminUserRole(req.params.userId, req.body, req.adminSession), (req) => updateManagedAdminUserRole(String(req.params.userId), req.body, req.adminSession),
{ {
successMessage: '用户角色已更新', successMessage: '用户角色已更新',
errorMessage: '更新用户角色失败', errorMessage: '更新用户角色失败',
@@ -77,7 +77,7 @@ router.post('/users/:userId/role', createJsonHandler(
)) ))
router.post('/users/:userId/status', createJsonHandler( router.post('/users/:userId/status', createJsonHandler(
(req) => updateManagedAdminUserStatus(req.params.userId, req.body, req.adminSession), (req) => updateManagedAdminUserStatus(String(req.params.userId), req.body, req.adminSession),
{ {
successMessage: '用户状态已更新', successMessage: '用户状态已更新',
errorMessage: '更新用户状态失败', errorMessage: '更新用户状态失败',
@@ -98,7 +98,7 @@ router.post('/users/:userId/status', createJsonHandler(
)) ))
router.post('/users/:userId/inventory-groups', createJsonHandler( router.post('/users/:userId/inventory-groups', createJsonHandler(
(req) => updateManagedAdminUserInventoryGroups(req.params.userId, req.body, req.adminSession), (req) => updateManagedAdminUserInventoryGroups(String(req.params.userId), req.body, req.adminSession),
{ {
successMessage: '用户库存组已更新', successMessage: '用户库存组已更新',
errorMessage: '更新用户库存组失败', errorMessage: '更新用户库存组失败',
@@ -119,7 +119,7 @@ router.post('/users/:userId/inventory-groups', createJsonHandler(
)) ))
router.post('/users/:userId/reset-password', createJsonHandler( router.post('/users/:userId/reset-password', createJsonHandler(
(req) => resetManagedAdminUserPassword(req.params.userId, req.body), (req) => resetManagedAdminUserPassword(String(req.params.userId), req.body),
{ {
successMessage: '用户密码已重置', successMessage: '用户密码已重置',
errorMessage: '重置用户密码失败', errorMessage: '重置用户密码失败',
+12
View File
@@ -0,0 +1,12 @@
import type { AdminSession } from '../services/admin/admin-auth-service.js'
declare global {
namespace Express {
interface Request {
adminSession?: AdminSession | null
requestId?: string
}
}
}
export {}
+139 -112
View File
@@ -1,33 +1,34 @@
import { logError, logWarn } from './logger.js' import type { Request, Response } from "express";
import { logError, logWarn } from "./logger.js";
const RESPONSE_TIME_ZONE = 'Asia/Shanghai' const RESPONSE_TIME_ZONE = "Asia/Shanghai";
export type HttpErrorLike = { export type HttpErrorLike = {
statusCode?: number statusCode?: number;
errorCode?: string errorCode?: string;
cause?: unknown cause?: unknown;
context?: unknown context?: unknown;
[key: string]: unknown [key: string]: unknown;
} };
type CreateHttpErrorOptions = { type CreateHttpErrorOptions = {
statusCode?: number statusCode?: number;
errorCode?: string errorCode?: string;
cause?: unknown cause?: unknown;
context?: unknown context?: unknown;
} };
export function buildSuccessPayload(data, msg = 'ok') { export function buildSuccessPayload(data: unknown, msg: string = "ok") {
return { return {
code: 0, code: 0,
msg, msg,
time: Math.floor(Date.now() / 1000), time: Math.floor(Date.now() / 1000),
data: normalizeResponseDateTime(data), data: normalizeResponseDateTime(data),
} };
} }
export function buildErrorPayload(error, fallbackMessage) { export function buildErrorPayload(error: unknown, fallbackMessage: string) {
const classification = classifyRouteError(error) const classification = classifyRouteError(error);
return { return {
code: 1, code: 1,
@@ -35,214 +36,240 @@ export function buildErrorPayload(error, fallbackMessage) {
errorCode: classification.errorCode, errorCode: classification.errorCode,
time: Math.floor(Date.now() / 1000), time: Math.floor(Date.now() / 1000),
data: null, data: null,
} };
} }
export function sendRouteError(res, error, fallbackMessage, scope) { export function sendRouteError(
const classification = classifyRouteError(error) res: Response,
const logger = classification.statusCode >= 500 ? logError : logWarn error: unknown,
fallbackMessage: string,
scope: string
): void {
const classification = classifyRouteError(error);
const logger = classification.statusCode >= 500 ? logError : logWarn;
logger(scope, fallbackMessage, { logger(scope, fallbackMessage, {
statusCode: classification.statusCode, statusCode: classification.statusCode,
errorCode: classification.errorCode, errorCode: classification.errorCode,
error, error,
}) });
res.status(classification.statusCode).json(buildErrorPayload(error, fallbackMessage)) res
.status(classification.statusCode)
.json(buildErrorPayload(error, fallbackMessage));
} }
export function buildNotFoundPayload(req) { export function buildNotFoundPayload(req: Request) {
return { return {
code: 1, code: 1,
msg: `未实现接口: ${req.method} ${req.originalUrl}`, msg: `未实现接口: ${req.method} ${req.originalUrl}`,
time: Math.floor(Date.now() / 1000), time: Math.floor(Date.now() / 1000),
data: null, data: null,
} };
} }
export function createHttpError(message: string, options: CreateHttpErrorOptions = {}): Error & HttpErrorLike { export function createHttpError(
const { statusCode = 500, errorCode = '', cause, context } = options message: string,
const error = new Error(message) as Error & HttpErrorLike options: CreateHttpErrorOptions = {}
error.statusCode = statusCode ): Error & HttpErrorLike {
error.errorCode = errorCode || defaultErrorCodeForStatus(statusCode) const { statusCode = 500, errorCode = "", cause, context } = options;
const error = new Error(message) as Error & HttpErrorLike;
error.statusCode = statusCode;
error.errorCode = errorCode || defaultErrorCodeForStatus(statusCode);
if (typeof cause !== 'undefined') { if (typeof cause !== "undefined") {
error.cause = cause error.cause = cause;
} }
if (typeof context !== 'undefined') { if (typeof context !== "undefined") {
error.context = context error.context = context;
} }
return error return error;
} }
function classifyRouteError(error) { function classifyRouteError(error: unknown): {
if (error && typeof error === 'object') { statusCode: number;
const currentError = error as HttpErrorLike errorCode: string;
const statusCode = Number(currentError.statusCode) } {
const errorCode = String(currentError.errorCode || '').trim() if (error && typeof error === "object") {
const currentError = error as HttpErrorLike;
const statusCode = Number(currentError.statusCode);
const errorCode = String(currentError.errorCode || "").trim();
if (Number.isInteger(statusCode) && statusCode >= 400 && statusCode < 600) { if (Number.isInteger(statusCode) && statusCode >= 400 && statusCode < 600) {
return { return {
statusCode, statusCode,
errorCode: errorCode || defaultErrorCodeForStatus(statusCode), errorCode: errorCode || defaultErrorCodeForStatus(statusCode),
} };
} }
} }
const message = error instanceof Error ? error.message : String(error || '') const message = error instanceof Error ? error.message : String(error || "");
if (message.includes('缺少') || message.includes('无效') || message.includes('验签失败')) { if (
return { statusCode: 400, errorCode: 'invalid_request' } message.includes("缺少") ||
message.includes("无效") ||
message.includes("验签失败")
) {
return { statusCode: 400, errorCode: "invalid_request" };
} }
if (message.includes('不存在')) { if (message.includes("不存在")) {
return { statusCode: 404, errorCode: 'not_found' } return { statusCode: 404, errorCode: "not_found" };
} }
if (message.includes('已关闭')) { if (message.includes("已关闭")) {
return { statusCode: 410, errorCode: 'gone' } return { statusCode: 410, errorCode: "gone" };
} }
if (message.includes('不能') || message.includes('冲突')) { if (message.includes("不能") || message.includes("冲突")) {
return { statusCode: 409, errorCode: 'conflict' } return { statusCode: 409, errorCode: "conflict" };
} }
if ( if (
message.includes('OCR') || message.includes("OCR") ||
message.includes('uv sync') || message.includes("uv sync") ||
message.includes('spawn uv ENOENT') || message.includes("spawn uv ENOENT") ||
message.includes("Executable doesn't exist") || message.includes("Executable doesn't exist") ||
message.includes('please run the following command to download new browsers') message.includes(
"please run the following command to download new browsers"
)
) { ) {
return { statusCode: 503, errorCode: 'dependency_unavailable' } return { statusCode: 503, errorCode: "dependency_unavailable" };
} }
return { statusCode: 500, errorCode: 'internal_error' } return { statusCode: 500, errorCode: "internal_error" };
} }
function defaultErrorCodeForStatus(statusCode) { function defaultErrorCodeForStatus(statusCode: number): string {
if (statusCode === 400) { if (statusCode === 400) {
return 'invalid_request' return "invalid_request";
} }
if (statusCode === 401) { if (statusCode === 401) {
return 'unauthorized' return "unauthorized";
} }
if (statusCode === 403) { if (statusCode === 403) {
return 'forbidden' return "forbidden";
} }
if (statusCode === 404) { if (statusCode === 404) {
return 'not_found' return "not_found";
} }
if (statusCode === 409) { if (statusCode === 409) {
return 'conflict' return "conflict";
} }
if (statusCode === 410) { if (statusCode === 410) {
return 'gone' return "gone";
} }
if (statusCode === 503) { if (statusCode === 503) {
return 'dependency_unavailable' return "dependency_unavailable";
} }
return 'internal_error' return "internal_error";
} }
function normalizeResponseDateTime(value) { function normalizeResponseDateTime(value: unknown): unknown {
if (value instanceof Date) { if (value instanceof Date) {
return formatResponseDateTime(value.toISOString()) return formatResponseDateTime(value.toISOString());
} }
if (Array.isArray(value)) { if (Array.isArray(value)) {
return value.map((item) => normalizeResponseDateTime(item)) return value.map((item) => normalizeResponseDateTime(item));
} }
if (value && typeof value === 'object') { if (value && typeof value === "object") {
return Object.fromEntries( return Object.fromEntries(
Object.entries(value).map(([key, currentValue]) => [key, normalizeResponseDateTime(currentValue)]), Object.entries(value).map(([key, currentValue]) => [
) key,
normalizeResponseDateTime(currentValue),
])
);
} }
if (isIsoDateTimeString(value)) { if (isIsoDateTimeString(value)) {
return formatResponseDateTime(value) return formatResponseDateTime(value as string);
} }
return value return value;
} }
function isIsoDateTimeString(value) { function isIsoDateTimeString(value: unknown): boolean {
return ( return (
typeof value === 'string' && typeof value === "string" &&
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/.test(value.trim()) /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/.test(
value.trim()
) )
);
} }
function formatResponseDateTime(value) { function formatResponseDateTime(value: string): string {
const date = new Date(value) const date = new Date(value);
if (Number.isNaN(date.getTime())) { if (Number.isNaN(date.getTime())) {
return String(value || '').replace('T', ' ').replace('Z', '') return String(value || "")
.replace("T", " ")
.replace("Z", "");
} }
const parts = new Intl.DateTimeFormat('zh-CN', { const parts = new Intl.DateTimeFormat("zh-CN", {
timeZone: RESPONSE_TIME_ZONE, timeZone: RESPONSE_TIME_ZONE,
hour12: false, hour12: false,
year: 'numeric', year: "numeric",
month: '2-digit', month: "2-digit",
day: '2-digit', day: "2-digit",
hour: '2-digit', hour: "2-digit",
minute: '2-digit', minute: "2-digit",
second: '2-digit', second: "2-digit",
}).formatToParts(date) }).formatToParts(date);
const tokens = Object.fromEntries( const tokens = Object.fromEntries(
parts parts
.filter((item) => item.type !== 'literal') .filter((item) => item.type !== "literal")
.map((item) => [item.type, item.value]), .map((item) => [item.type, item.value])
) );
return `${tokens.year}-${tokens.month}-${tokens.day} ${tokens.hour}:${tokens.minute}:${tokens.second}` return `${tokens.year}-${tokens.month}-${tokens.day} ${tokens.hour}:${tokens.minute}:${tokens.second}`;
} }
type RouteHandlerOptions = { type RouteHandlerOptions = {
successMessage?: string successMessage?: string;
errorMessage?: string errorMessage?: string;
scope?: string scope?: string;
} };
type RouteFileHandlerOptions = { type RouteFileHandlerOptions = {
errorMessage?: string errorMessage?: string;
scope?: string scope?: string;
} };
export function createRouteHandler( export function createRouteHandler(
action: (req: any, res: any) => unknown | Promise<unknown>, action: (req: Request, res: Response) => unknown | Promise<unknown>,
{ successMessage = 'ok', errorMessage, scope }: RouteHandlerOptions = {}, { successMessage = "ok", errorMessage, scope }: RouteHandlerOptions = {}
) { ) {
return async (req, res) => { return async (req: Request, res: Response): Promise<void> => {
try { try {
const data = await action(req, res) const data = await action(req, res);
res.json(buildSuccessPayload(data, successMessage)) res.json(buildSuccessPayload(data, successMessage));
} catch (error) { } catch (error) {
sendRouteError(res, error, errorMessage, scope) sendRouteError(res, error, errorMessage, scope);
}
} }
};
} }
export function createRouteFileHandler( export function createRouteFileHandler(
action: (req: any, res: any) => string | Promise<string>, action: (req: Request, res: Response) => string | Promise<string>,
{ errorMessage, scope }: RouteFileHandlerOptions = {}, { errorMessage, scope }: RouteFileHandlerOptions = {}
) { ) {
return async (req, res) => { return async (req: Request, res: Response): Promise<void> => {
try { try {
const filePath = await action(req, res) const filePath = await action(req, res);
res.sendFile(filePath) res.sendFile(filePath);
} catch (error) { } catch (error) {
sendRouteError(res, error, errorMessage, scope) sendRouteError(res, error, errorMessage, scope);
}
} }
};
} }