Files
photo-live/services/api/src/request-id.middleware.ts
T
浅念 368b1d5b47 feat(api): argon2 password, idempotent admin seed, api-exception infra
Task 12 + Task 7 quality-review carryovers:

Part A - password hashing & controlled initial admin:
- src/auth/password.ts: hashPassword/verifyPassword over @node-rs/argon2
  (napi prebuilt, argon2id default); empty plain rejected at the door
- src/scripts/seed-admin.ts: idempotent admin seed (ADMIN_ACCOUNT/
  ADMIN_PASSWORD env, walk-up root .env load); existing account -> skip,
  never overwrites the password; no audit_logs (system op, no actor)
- pnpm seed script (build + node dist/scripts/seed-admin.js);
  .env.example dev defaults admin/admin-secret-change-me
- tests: test/password.spec.ts (5), test/seed.spec.ts real-DB idempotency (4)

Part B - Task 7 carryovers (all 8):
- ApiException(apiCode, message, httpStatus, details?) recognized first by
  the filter; unmapped-status rule: >=500 -> INTERNAL_ERROR + generic
  message, <500 -> VALIDATION_FAILED keeping original status (warn-logged)
- 5xx/INTERNAL_ERROR responses forced to generic message (leak fix)
- pino redact adds req.headers.cookie / res.headers.set-cookie
- x-request-id echo capped at 128 chars (fresh UUID beyond, no log)
- LOG_LEVEL validated against pino levels at module wiring -> fail fast
- shared/ constants (API_PREFIX), request-with-id, log-level (+spec)
- jest ts-jest reuses tsconfig.json; unit testMatch includes test/*.spec.ts
- build:watch script (no concurrently dep; dev = build:watch + node --watch)

api: 35 unit + 10 e2e tests green; root lint/typecheck/test green.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-17 20:54:30 +08:00

41 lines
1.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// services/api/src/request-id.middleware.ts
// 请求 ID 约定:优先透传上游 x-request-id(网关/前端可携带),否则生成 UUID v4;
// 挂到 req.id(pino-http 日志字段)并写回响应头 x-request-id,便于全链路排障。
// 长度上限(Task 7 遗留项④):上游值 >128 字符时视为滥用/异常,静默换成新 UUID
//(不透传也不告警——日志与响应头由本函数统一写回,不会出现半截 ID)。
// genReqId 与本中间件共用 resolveRequestId,先后顺序不影响结果。
import { randomUUID } from 'node:crypto';
import type { ServerResponse } from 'node:http';
import type { NestMiddleware } from '@nestjs/common';
import type { ServerRequestWithId } from './shared/request-with-id';
/** 透传上游 x-request-id 的长度上限(UUID v4 为 36 字符,128 已十分宽裕)。 */
export const MAX_REQUEST_ID_LENGTH = 128;
export function resolveRequestId(
req: ServerRequestWithId,
res: ServerResponse,
): string {
if (req.id) {
return req.id;
}
const header = req.headers['x-request-id'];
const fromHeader = Array.isArray(header) ? header[0] : header;
const id =
typeof fromHeader === 'string' &&
fromHeader.trim() !== '' &&
fromHeader.length <= MAX_REQUEST_ID_LENGTH
? fromHeader
: randomUUID();
req.id = id;
res.setHeader('x-request-id', id);
return id;
}
export class RequestIdMiddleware implements NestMiddleware {
use(req: ServerRequestWithId, res: ServerResponse, next: () => void): void {
resolveRequestId(req, res);
next();
}
}