refactor(api): per-source sourceSystem on user source port, retire pending-activation dead code
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -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<string | null> = [];
|
||||
|
||||
@@ -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 批次,
|
||||
* 不动任何用户,水位不前进)。
|
||||
|
||||
@@ -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:<random>`(非法 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,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user