From 52cd31679a438e0ffa6d3f5e893e5f82fa4dbbb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 22 Sep 2026 16:24:37 +0800 Subject: [PATCH] refactor(api): per-source sourceSystem on user source port, retire pending-activation dead code Co-Authored-By: Claude Code --- services/api/src/auth/auth.service.ts | 2 +- .../api/src/user-sync/mock-user-source.ts | 3 ++ .../api/src/user-sync/user-source.port.ts | 5 +++ .../api/src/user-sync/user-sync.service.ts | 44 +++++++++---------- 4 files changed, 31 insertions(+), 23 deletions(-) diff --git a/services/api/src/auth/auth.service.ts b/services/api/src/auth/auth.service.ts index 11f62f7..e5cf5a3 100644 --- a/services/api/src/auth/auth.service.ts +++ b/services/api/src/auth/auth.service.ts @@ -132,7 +132,7 @@ export class AuthService { // ③ 先验密码再查状态:状态不合格与密码错误同样不可区分(同 401 同文案; // argon2 在两条路径都真实跑过,时延也一致)。 - // 哨兵哈希(P4a Task 7:同步创建的待激活账号 passwordHash='!:pending:…') + // 历史哨兵哈希行(P4a 待激活已退役;非法 argon2 编码统一按密码错误处理) // 不是合法 argon2 编码——verify 会抛错而非返回 false。此处按密码错误 // 处理并补跑等代价 dummy verify(时延与密码错误路径一致,不泄露 // 「该账号待激活」),杜绝 500。 diff --git a/services/api/src/user-sync/mock-user-source.ts b/services/api/src/user-sync/mock-user-source.ts index 6057412..da6bbd5 100644 --- a/services/api/src/user-sync/mock-user-source.ts +++ b/services/api/src/user-sync/mock-user-source.ts @@ -113,6 +113,9 @@ function encodePageCursor(watermark: number | null, page: number): string { @Injectable() export class MockUserSource implements UserSourcePort { + /** 端口 sourceSystem:mock 源命名空间(e2e 断言 'mock:ext-*' 字面量)。 */ + readonly sourceSystem = 'mock'; + private scenario: MockScenario = DEFAULT_SCENARIO; /** e2e 断言用:历次 listPage 收到的游标(null=全量形态)。 */ readonly receivedCursors: Array = []; diff --git a/services/api/src/user-sync/user-source.port.ts b/services/api/src/user-sync/user-source.port.ts index aefd147..0e19229 100644 --- a/services/api/src/user-sync/user-source.port.ts +++ b/services/api/src/user-sync/user-source.port.ts @@ -34,6 +34,11 @@ export interface UserSourcePage { /** 用户源适配器端口(mock 实现见 mock-user-source.ts;G4 后替换为真实适配器)。 */ export interface UserSourcePort { + /** 本源在 external_user_mappings.sourceSystem 的命名空间标识——映射锚点 + * (sourceSystem, externalUserId) 的组成部分(引擎红线 4)。mock='mock', + * uniservice 真实源='uniservice'(http-user-source.ts)。 */ + readonly sourceSystem: string; + /** * 拉取一页。cursor 语义见文件头;抛错 = 分页中断(引擎记 failed 批次, * 不动任何用户,水位不前进)。 diff --git a/services/api/src/user-sync/user-sync.service.ts b/services/api/src/user-sync/user-sync.service.ts index 6e72125..edc9cf3 100644 --- a/services/api/src/user-sync/user-sync.service.ts +++ b/services/api/src/user-sync/user-sync.service.ts @@ -1,5 +1,5 @@ // services/api/src/user-sync/user-sync.service.ts —— 用户同步引擎(P4a Task 7, -// A29;T30 语义全量,mock 源先行——G4 真实接口到位后仅替换 USER_SOURCE provider)。 +// A29;G4 后双源:USER_SYNC_SOURCE 装配 mock 或 http——引擎/契约不变)。 // // 红线(技术方案 3.1A,e2e 全量守护): // 1. 同步绝不写 password_hash / role / album_members / localLocked(display @@ -23,10 +23,11 @@ // → 终态:success(无坏行)/ partial(有坏行)/ failed(listPage 抛错等 // 批次级故障:error 摘要落账,会话绝不被批次故障牵连撤销)。 // -// 待激活闭环(§10):新建用户 credentialStatus=pending + 哨兵哈希 -// `!:pending:`(非法 argon2 编码,任何口令都无法通过校验——auth.service -// 对 verify 抛错按密码错误处理并跑等代价 dummy verify)。初始凭据由管理员 -// activate + reset-password 出具(Task 6 端点)。 +// 初始凭据(V1.1,2026-09-22 G4 真实接口上线):同步新建用户直接 +// credentialStatus=active、初始口令=登录账号(手机号)后 6 位(argon2 哈希, +// 不足 6 位回退 '000000');管理员可经 reset-password 轮换。P4a 的「待激活 + +// 哨兵哈希」闭环退役(auth.service 对非法哈希的等代价 dummy verify 防御保留, +// 兼容历史行)。account 关联路径仍绝不动既有凭据(红线 1 不变)。 // // department 字段:users 表无此列,忽略(端口保留对齐真实源形状,登记偏差)。 import { randomBytes } from 'node:crypto'; @@ -42,6 +43,7 @@ import { USER_SYNC_STATE_SINGLETON_ID } from '@photo/db-cn'; import { ErrorCodes } from '@photo/contracts'; import { ApiException } from '../api-exception'; import type { AuthUser } from '../auth/session.guard'; +import { hashPassword } from '../auth/password'; import { PrismaService } from '../prisma/prisma.service'; import { SyncLock } from './sync-lock'; import { @@ -50,12 +52,6 @@ import { type UserSourcePort, } from './user-source.port'; -/** 同步源系统标识(external_user_mappings.sourceSystem;G4 后换真实源标识)。 */ -export const USER_SYNC_SOURCE_SYSTEM = 'mock'; - -/** 哨兵哈希前缀:同步创建的待激活账号(非法 argon2 编码,不可被猜中登录)。 */ -export const PENDING_PASSWORD_HASH_PREFIX = '!:pending:'; - /** 定时同步间隔缺省 5 分钟(env SYNC_USERS_INTERVAL_MS 可调)。 */ const DEFAULT_SYNC_INTERVAL_MS = 300_000; @@ -86,8 +82,9 @@ interface ItemOutcome { detail?: string; } -function pendingPasswordHash(): string { - return `${PENDING_PASSWORD_HASH_PREFIX}${randomBytes(16).toString('hex')}`; +/** V1.1 初始口令:登录账号(手机号)后 6 位;缺失/空回退 '000000'。 */ +function initialPassword(loginAccount: string | undefined): string { + return loginAccount?.slice(-6) || '000000'; } /** 引擎侧批次 ID:预生成以兼作锁值(24 hex,撞库概率可忽略)。 */ @@ -95,7 +92,8 @@ function generateRunId(): string { return randomBytes(12).toString('hex'); } -/** 水位串 → 契约 int64(mock 恒数值串;非数值游标——G4 真实源——投影为 null)。 */ +/** 水位串 → 契约 int64(源水位恒数值串——mock 自增版本 / http epoch 秒; + * 非数值游标投影为 null)。 */ export function watermarkToInt(cursor: string | null): number | null { if (cursor === null || cursor === '') { return null; @@ -360,9 +358,10 @@ export class UserSyncEngine { return { bucket: 'failed', version: null, code: 'invalid_record', detail: validated.detail }; } try { + const sourceSystem = this.source.sourceSystem; const mappingWhere = { sourceSystem_externalUserId: { - sourceSystem: USER_SYNC_SOURCE_SYSTEM, + sourceSystem, externalUserId: item.externalUserId, }, }; @@ -379,11 +378,11 @@ export class UserSyncEngine { const created = await this.prisma.users.create({ data: { account: item.loginAccount ?? item.externalUserId, - passwordHash: pendingPasswordHash(), + passwordHash: await hashPassword(initialPassword(item.loginAccount)), displayName: item.displayName, role: 'staff', sourceStatus: item.sourceStatus, - credentialStatus: 'pending', + credentialStatus: 'active', }, }); await this.prisma.external_user_mappings.update({ @@ -443,7 +442,7 @@ export class UserSyncEngine { this.prisma.external_user_mappings.create({ data: { userId: existing.id, - sourceSystem: USER_SYNC_SOURCE_SYSTEM, + sourceSystem, externalUserId: item.externalUserId, sourceVersion: validated.version, }, @@ -463,21 +462,22 @@ export class UserSyncEngine { }; } - // 全新外部身份 → 建待激活用户(§10 闭环:activate + reset-password 出凭据)。 + // 全新外部身份 → 建用户;初始密码 = 手机号后 6 位(argon2 哈希)。 + // V1.1(2026-09-22):真实接口上线,账号=手机号,密码=后6位,直接 active。 const created = await this.prisma.users.create({ data: { account, - passwordHash: pendingPasswordHash(), + passwordHash: await hashPassword(initialPassword(item.loginAccount)), displayName: item.displayName, role: 'staff', sourceStatus: item.sourceStatus, - credentialStatus: 'pending', + credentialStatus: 'active', }, }); await this.prisma.external_user_mappings.create({ data: { userId: created.id, - sourceSystem: USER_SYNC_SOURCE_SYSTEM, + sourceSystem, externalUserId: item.externalUserId, sourceVersion: validated.version, },