Files
photo-live/services/api/src/api-exception.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

29 lines
1.3 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/api-exception.ts —— 携带契约错误码的业务异常(Task 7 遗留项①)。
// 状态码表(过滤器内 STATUS_TO_ERROR_CODE)只能表达"一个 HTTP 状态 = 一个错误码",
// 无法表达同状态多错误码的契约空间(如 401 既可能是 AUTH_TOKEN_EXPIRED 也可能是
// AUTH_INVALID_CREDENTIALS,409 既可能是 MEMBER_CONFLICT 也可能是
// UPLOAD_SESSION_CONFLICT)。业务代码抛 ApiException 精确指定 ErrorCode;
// 过滤器优先识别它,状态码表仅作普通 HttpException 的兜底映射。
import { HttpException } from '@nestjs/common';
import type { ErrorCode } from '@photo/contracts';
export class ApiException extends HttpException {
constructor(
readonly apiCode: ErrorCode,
message: string,
httpStatus: number,
readonly details?: unknown,
) {
// 响应体形状 {code, message, details?}:details 缺省时不占位(保持信封干净)。
// 注意 message 须从 getResponse().message 提取(见过滤器 extractMessage),
// HttpException.message 属性在对象响应下不含业务文案。
super(
details === undefined
? { code: apiCode, message }
: { code: apiCode, message, details },
httpStatus,
);
this.name = ApiException.name;
}
}