Commit Graph

6 Commits

Author SHA1 Message Date
浅念 eac8da6d6c feat(ov): emergency disable channel independent of sync
P2 Task 5(海外紧急停用入口):
- ov 新增 POST /v1/internal/emergency/disable|enable(InternalSyncGuard
  HMAC,albumToken/albumId 恰好其一、都缺省 400;副本不存在 404
  ALBUM_NOT_FOUND):直写 albums_ov.emergencyDisabled(唯一 ov 本地可写
  位),独立于同步版本链——更高版本 album.updated 重放不解除(e2e 断言
  entityVersion 推进而标志保持),恢复仅经显式 enable;幂等语义
  (alreadyDisabled/wasDisabled 入审计行)
- cn 侧不感知:无对应 cn 状态位、无反向同步(契约描述文档化)
- 审计:pino child(audit=emergency 字段常驻每行)结构化日志——偏差
  登记:计划原文「写 ov 侧日志文件」落地为字段标记而非独立文件通道
  (免新增文件 transport 依赖,P5 运维按字段路由聚合);注入令牌
  EMERGENCY_AUDIT_LOGGER,测试覆写为内存目的地断言字段
- isAlbumVisibleOv 共享判定(status=active && publicEnabled &&
  !emergencyDisabled——镜像 cn 公开语义叠加本地停用位):Task 6 公开
  API / Task 7 WS subscribe 的 404 ALBUM_NOT_PUBLIC 语义单一实现,
  判定矩阵单测锁定
- 契约 [internal] 增 emergency disable/enable 操作与 schema(gen 已跑、
  redocly 通过);e2e:token/id 定位、幂等、enable 恢复、未签名 401、
  均缺省 400、未知相册 404、同步事件不解除

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-18 16:19:02 +08:00
浅念 65726702f3 feat(sync): cn event coverage, bootstrap and outbox sender
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-18 15:21:41 +08:00
浅念 3d6e9ce175 feat(ov): ov schema, entry skeleton and compose profile
P2 Task 2 ov 数据层:
- compose ov profile:mysql-ov(mysql:8.4,127.0.0.1:3309,库级
  utf8mb4_0900_bin)+ redis-ov(6381),cn 侧默认 up 不受影响
  (3307/3308 已被其它本机项目占用,端口决策 3309)
- @photo/db-ov:9 表 schema(albums_ov/photos_ov 副本、sync_inbox/
  sync_state 同步账本(水位行同表标志位)、like_batches/album_stats/
  visit_sessions/visit_requests 互动域、outbox_stat_events 回传队列);
  迁移 20260918060102_init_ov 走固化校对手决流程,information_schema
  复核全部业务表/列 utf8mb4_0900_bin(_prisma_migrations 同 cn 先例
  保持 unicode_ci)
- generator 独立 output(包内 generated/):pnpm 将两包 @prisma/client
  提升为同一 store 实例,默认 .prisma/client 输出会被双 schema 互相
  覆盖,ov 客户端独立产出经 src/index.ts 转出口
- ov 入口骨架:main-ov.ts(PORT_OV 默认 8081)+ app-ov.module.ts
  (仅管线骨架 + /v1/health,feature modules 后续任务装配);logger
  装配选项抽取至 shared/logger-options.ts 供 cn/ov 双入口共用(行为
  不变,P1 套件守护)
- test/reset-ov.ts:ov 库 truncate-if-exists 隔离工具(无 MinIO 清理)
- 根脚本:db:generate 双库齐跑,新增 db-ov:migrate/db-ov:generate/dev:ov

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-18 14:11:41 +08:00
浅念 47fb81ad31 chore(api): p1a final-review hygiene items
- contracts: add NOT_FOUND error code (framework-level 404 for unknown
  routes); Error enum + regen committed
- api: 404 default mapping in AllExceptionsFilter now NOT_FOUND (was
  ALBUM_NOT_FOUND); business 404s unchanged via explicit ApiException codes
- api: extract configureApp (prefix/CORS/x-powered-by) shared by main-cn
  bootstrap and e2e; disable x-powered-by, asserted in e2e
- api: move parseCursor/parseLimit (+page-size bounds) from
  albums.controller to shared/pagination; members/uploads/public re-pointed
- infra: bind compose published ports to 127.0.0.1 (mysql/redis/minio);
  .env.example endpoints follow suit
- e2e: unknown-route 404 NOT_FOUND + x-powered-by absent cases added
  (api e2e 120 -> 122)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-18 08:18:30 +08:00
浅念 1c48a4108b feat(api): album creation with translations and owner membership
Task 15: POST/GET /v1/albums, GET /v1/albums/{id}.
- create: single transaction writes albums + album_translations +
  album_members(owner) + audit_logs(album.create); publicToken 24-char
  [0-9a-z] with one P2002 retry; duplicate-locale input rejected as 400.
- dto: class-validator per contract AlbumCreateRequest, incl. cross-field
  rules (defaultLocale must be covered by translations; endAt>startAt when
  present; timezone string-only per P1a scope).
- get: PermissionsService.canViewAlbum gate -> 404 ALBUM_NOT_FOUND for
  non-member/nonexistent alike (anti-enumeration); admin bypass; joins
  categories (empty until Task 17).
- list: admin sees all, members see joined albums; stable createdAt DESC,
  id DESC keyset pagination via new shared/cursor.ts (encode/decode +
  typed where-builder, designed for Task 23 photo list reuse).
- e2e: services/api/test/albums.e2e-spec.ts (12 cases, seeded sessions).

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-17 21:51:50 +08:00
浅念 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