From 2730bcb3544f0d2adcb7b7212afed5e737bd9366 Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 16 Dec 2025 17:06:28 -0800 Subject: [PATCH] =?UTF-8?q?feat(identity):=20=E5=AE=8C=E5=96=84=E8=B4=A6?= =?UTF-8?q?=E6=88=B7=E5=AE=89=E5=85=A8=E5=92=8C=E6=81=A2=E5=A4=8D=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 账户冻结/解冻功能: - POST /user/freeze: 用户主动冻结账户 - POST /user/unfreeze: 验证身份后解冻账户(支持助记词或手机号验证) - 添加 AccountUnfrozenEvent 审计事件 2. 密钥轮换功能: - POST /user/key-rotation/request: 验证助记词后请求 MPC 密钥轮换 - 添加 KeyRotationRequestedEvent 事件触发后台轮换 3. 恢复码备份功能: - POST /user/backup-codes/generate: 生成8个一次性恢复码 - POST /user/recover-by-backup-code: 使用恢复码恢复账户 - 恢复码存储在 Redis,有效期1年 - 每个恢复码只能使用一次 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../controllers/user-account.controller.ts | 60 +++ .../src/api/dto/request/freeze-account.dto.ts | 10 + .../dto/request/generate-backup-codes.dto.ts | 9 + .../src/api/dto/request/index.ts | 5 + .../dto/request/recover-by-backup-code.dto.ts | 26 ++ .../dto/request/request-key-rotation.dto.ts | 15 + .../api/dto/request/unfreeze-account.dto.ts | 24 ++ .../services/user-application.service.ts | 365 +++++++++++++++++- .../src/domain/events/index.ts | 42 ++ 9 files changed, 555 insertions(+), 1 deletion(-) create mode 100644 backend/services/identity-service/src/api/dto/request/freeze-account.dto.ts create mode 100644 backend/services/identity-service/src/api/dto/request/generate-backup-codes.dto.ts create mode 100644 backend/services/identity-service/src/api/dto/request/recover-by-backup-code.dto.ts create mode 100644 backend/services/identity-service/src/api/dto/request/request-key-rotation.dto.ts create mode 100644 backend/services/identity-service/src/api/dto/request/unfreeze-account.dto.ts diff --git a/backend/services/identity-service/src/api/controllers/user-account.controller.ts b/backend/services/identity-service/src/api/controllers/user-account.controller.ts index becf3cc3..ad72e9d9 100644 --- a/backend/services/identity-service/src/api/controllers/user-account.controller.ts +++ b/backend/services/identity-service/src/api/controllers/user-account.controller.ts @@ -18,6 +18,8 @@ import { AutoCreateAccountDto, RecoverByMnemonicDto, RecoverByPhoneDto, AutoLoginDto, SendSmsCodeDto, RegisterDto, LoginDto, BindPhoneDto, UpdateProfileDto, BindWalletDto, SubmitKYCDto, RemoveDeviceDto, RevokeMnemonicDto, + FreezeAccountDto, UnfreezeAccountDto, RequestKeyRotationDto, + GenerateBackupCodesDto, RecoverByBackupCodeDto, AutoCreateAccountResponseDto, RecoverAccountResponseDto, LoginResponseDto, UserProfileResponseDto, DeviceResponseDto, WalletStatusReadyResponseDto, WalletStatusGeneratingResponseDto, @@ -206,6 +208,64 @@ export class UserAccountController { return this.userService.revokeMnemonic(user.userId, dto.reason); } + @Post('freeze') + @ApiBearerAuth() + @ApiOperation({ summary: '冻结账户', description: '用户主动冻结自己的账户,冻结后账户将无法进行任何操作' }) + @ApiResponse({ status: 200, description: '冻结结果' }) + async freezeAccount(@CurrentUser() user: CurrentUserData, @Body() dto: FreezeAccountDto) { + return this.userService.freezeAccount(user.userId, dto.reason); + } + + @Post('unfreeze') + @ApiBearerAuth() + @ApiOperation({ summary: '解冻账户', description: '验证身份后解冻账户,支持助记词或手机号验证' }) + @ApiResponse({ status: 200, description: '解冻结果' }) + async unfreezeAccount(@CurrentUser() user: CurrentUserData, @Body() dto: UnfreezeAccountDto) { + return this.userService.unfreezeAccount({ + userId: user.userId, + verifyMethod: dto.verifyMethod, + mnemonic: dto.mnemonic, + phoneNumber: dto.phoneNumber, + smsCode: dto.smsCode, + }); + } + + @Post('key-rotation/request') + @ApiBearerAuth() + @ApiOperation({ summary: '请求密钥轮换', description: '验证当前助记词后,请求轮换 MPC 密钥对' }) + @ApiResponse({ status: 200, description: '轮换请求结果' }) + async requestKeyRotation(@CurrentUser() user: CurrentUserData, @Body() dto: RequestKeyRotationDto) { + return this.userService.requestKeyRotation({ + userId: user.userId, + currentMnemonic: dto.currentMnemonic, + reason: dto.reason, + }); + } + + @Post('backup-codes/generate') + @ApiBearerAuth() + @ApiOperation({ summary: '生成恢复码', description: '验证助记词后生成一组一次性恢复码' }) + @ApiResponse({ status: 200, description: '恢复码列表' }) + async generateBackupCodes(@CurrentUser() user: CurrentUserData, @Body() dto: GenerateBackupCodesDto) { + return this.userService.generateBackupCodes({ + userId: user.userId, + mnemonic: dto.mnemonic, + }); + } + + @Public() + @Post('recover-by-backup-code') + @ApiOperation({ summary: '使用恢复码恢复账户' }) + @ApiResponse({ status: 200, type: RecoverAccountResponseDto }) + async recoverByBackupCode(@Body() dto: RecoverByBackupCodeDto) { + return this.userService.recoverByBackupCode({ + accountSequence: dto.accountSequence, + backupCode: dto.backupCode, + newDeviceId: dto.newDeviceId, + deviceName: dto.deviceName, + }); + } + @Post('upload-avatar') @ApiBearerAuth() @ApiOperation({ summary: '上传用户头像' }) diff --git a/backend/services/identity-service/src/api/dto/request/freeze-account.dto.ts b/backend/services/identity-service/src/api/dto/request/freeze-account.dto.ts new file mode 100644 index 00000000..ad12602a --- /dev/null +++ b/backend/services/identity-service/src/api/dto/request/freeze-account.dto.ts @@ -0,0 +1,10 @@ +import { IsString, IsNotEmpty, MaxLength } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class FreezeAccountDto { + @ApiProperty({ example: '账户被盗', description: '冻结原因' }) + @IsString() + @IsNotEmpty({ message: '请填写冻结原因' }) + @MaxLength(200, { message: '冻结原因不能超过200字' }) + reason: string; +} diff --git a/backend/services/identity-service/src/api/dto/request/generate-backup-codes.dto.ts b/backend/services/identity-service/src/api/dto/request/generate-backup-codes.dto.ts new file mode 100644 index 00000000..c35e8091 --- /dev/null +++ b/backend/services/identity-service/src/api/dto/request/generate-backup-codes.dto.ts @@ -0,0 +1,9 @@ +import { IsString, IsNotEmpty } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class GenerateBackupCodesDto { + @ApiProperty({ example: 'abandon abandon ...', description: '当前助记词(验证身份用)' }) + @IsString() + @IsNotEmpty({ message: '请提供当前助记词' }) + mnemonic: string; +} diff --git a/backend/services/identity-service/src/api/dto/request/index.ts b/backend/services/identity-service/src/api/dto/request/index.ts index b524c6ef..9f0c7b36 100644 --- a/backend/services/identity-service/src/api/dto/request/index.ts +++ b/backend/services/identity-service/src/api/dto/request/index.ts @@ -4,3 +4,8 @@ export * from './recover-by-phone.dto'; export * from './bind-phone.dto'; export * from './submit-kyc.dto'; export * from './revoke-mnemonic.dto'; +export * from './freeze-account.dto'; +export * from './unfreeze-account.dto'; +export * from './request-key-rotation.dto'; +export * from './generate-backup-codes.dto'; +export * from './recover-by-backup-code.dto'; diff --git a/backend/services/identity-service/src/api/dto/request/recover-by-backup-code.dto.ts b/backend/services/identity-service/src/api/dto/request/recover-by-backup-code.dto.ts new file mode 100644 index 00000000..2998b948 --- /dev/null +++ b/backend/services/identity-service/src/api/dto/request/recover-by-backup-code.dto.ts @@ -0,0 +1,26 @@ +import { IsString, IsNotEmpty, Matches, IsOptional } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class RecoverByBackupCodeDto { + @ApiProperty({ example: 'D24121200001', description: '账户序列号' }) + @IsString() + @IsNotEmpty({ message: '请提供账户序列号' }) + @Matches(/^D\d{11}$/, { message: '账户序列号格式不正确' }) + accountSequence: string; + + @ApiProperty({ example: 'ABCD-1234-EFGH', description: '恢复码' }) + @IsString() + @IsNotEmpty({ message: '请提供恢复码' }) + @Matches(/^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/, { message: '恢复码格式不正确' }) + backupCode: string; + + @ApiProperty({ example: 'device-uuid-123', description: '新设备ID' }) + @IsString() + @IsNotEmpty() + newDeviceId: string; + + @ApiPropertyOptional({ example: 'iPhone 15 Pro', description: '新设备名称' }) + @IsString() + @IsOptional() + deviceName?: string; +} diff --git a/backend/services/identity-service/src/api/dto/request/request-key-rotation.dto.ts b/backend/services/identity-service/src/api/dto/request/request-key-rotation.dto.ts new file mode 100644 index 00000000..9269081a --- /dev/null +++ b/backend/services/identity-service/src/api/dto/request/request-key-rotation.dto.ts @@ -0,0 +1,15 @@ +import { IsString, IsNotEmpty, MaxLength } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class RequestKeyRotationDto { + @ApiProperty({ example: 'abandon abandon ...', description: '当前助记词(验证身份用)' }) + @IsString() + @IsNotEmpty({ message: '请提供当前助记词' }) + currentMnemonic: string; + + @ApiProperty({ example: '安全起见主动轮换', description: '轮换原因' }) + @IsString() + @IsNotEmpty({ message: '请填写轮换原因' }) + @MaxLength(200, { message: '轮换原因不能超过200字' }) + reason: string; +} diff --git a/backend/services/identity-service/src/api/dto/request/unfreeze-account.dto.ts b/backend/services/identity-service/src/api/dto/request/unfreeze-account.dto.ts new file mode 100644 index 00000000..e0b0b5cb --- /dev/null +++ b/backend/services/identity-service/src/api/dto/request/unfreeze-account.dto.ts @@ -0,0 +1,24 @@ +import { IsString, IsNotEmpty, IsOptional, MaxLength } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class UnfreezeAccountDto { + @ApiProperty({ example: '确认账户安全', description: '解冻验证方式: mnemonic 或 phone' }) + @IsString() + @IsNotEmpty() + verifyMethod: 'mnemonic' | 'phone'; + + @ApiPropertyOptional({ example: 'abandon abandon ...', description: '助记词 (verifyMethod=mnemonic时必填)' }) + @IsString() + @IsOptional() + mnemonic?: string; + + @ApiPropertyOptional({ example: '+8613800138000', description: '手机号 (verifyMethod=phone时必填)' }) + @IsString() + @IsOptional() + phoneNumber?: string; + + @ApiPropertyOptional({ example: '123456', description: '短信验证码 (verifyMethod=phone时必填)' }) + @IsString() + @IsOptional() + smsCode?: string; +} diff --git a/backend/services/identity-service/src/application/services/user-application.service.ts b/backend/services/identity-service/src/application/services/user-application.service.ts index a530fff9..7f7dfa5d 100644 --- a/backend/services/identity-service/src/application/services/user-application.service.ts +++ b/backend/services/identity-service/src/application/services/user-application.service.ts @@ -21,7 +21,7 @@ import { BlockchainWalletHandler } from '../event-handlers/blockchain-wallet.han import { MpcKeygenCompletedHandler } from '../event-handlers/mpc-keygen-completed.handler'; import { ApplicationError } from '@/shared/exceptions/domain.exception'; import { generateIdentity } from '@/shared/utils'; -import { MpcKeygenRequestedEvent, AccountRecoveredEvent, AccountRecoveryFailedEvent, MnemonicRevokedEvent } from '@/domain/events'; +import { MpcKeygenRequestedEvent, AccountRecoveredEvent, AccountRecoveryFailedEvent, MnemonicRevokedEvent, AccountUnfrozenEvent, KeyRotationRequestedEvent } from '@/domain/events'; import { AutoCreateAccountCommand, RecoverByMnemonicCommand, RecoverByPhoneCommand, AutoLoginCommand, RegisterCommand, LoginCommand, BindPhoneNumberCommand, @@ -914,4 +914,367 @@ export class UserApplicationService { return { success: false, message: '挂失失败,请稍后重试' }; } } + + // ============ 账户冻结/解冻相关 ============ + + /** + * 冻结账户 (POST /user/freeze) + * + * 用户主动冻结自己的账户,防止被盗用 + * 冻结后账户将无法进行任何操作 + */ + async freezeAccount(userId: string, reason: string): Promise<{ success: boolean; message: string }> { + this.logger.log(`[FREEZE] Freezing account for user: ${userId}, reason: ${reason}`); + + const account = await this.userRepository.findById(UserId.create(userId)); + if (!account) { + this.logger.warn(`[FREEZE] User not found: ${userId}`); + return { success: false, message: '用户不存在' }; + } + + try { + account.freeze(reason); + await this.userRepository.save(account); + + // 发布领域事件 (包含 UserAccountFrozenEvent) + await this.eventPublisher.publishAll(account.domainEvents); + account.clearDomainEvents(); + + this.logger.log(`[FREEZE] Account frozen successfully for user: ${userId}`); + return { success: true, message: '账户已冻结' }; + } catch (error) { + this.logger.error(`[FREEZE] Failed to freeze account`, error); + if (error instanceof Error && error.message === '账户已冻结') { + return { success: false, message: '账户已处于冻结状态' }; + } + return { success: false, message: '冻结失败,请稍后重试' }; + } + } + + /** + * 解冻账户 (POST /user/unfreeze) + * + * 需要验证身份后才能解冻 + * 支持助记词或手机号验证 + */ + async unfreezeAccount(params: { + userId: string; + verifyMethod: 'mnemonic' | 'phone'; + mnemonic?: string; + phoneNumber?: string; + smsCode?: string; + }): Promise<{ success: boolean; message: string }> { + const { userId, verifyMethod, mnemonic, phoneNumber, smsCode } = params; + this.logger.log(`[UNFREEZE] Unfreezing account for user: ${userId}, method: ${verifyMethod}`); + + const account = await this.userRepository.findById(UserId.create(userId)); + if (!account) { + this.logger.warn(`[UNFREEZE] User not found: ${userId}`); + return { success: false, message: '用户不存在' }; + } + + // 验证身份 + if (verifyMethod === 'mnemonic') { + if (!mnemonic) { + return { success: false, message: '请提供助记词' }; + } + + // 调用 blockchain-service 验证助记词 + const verifyResult = await this.blockchainClient.verifyMnemonicByAccount({ + accountSequence: account.accountSequence.value, + mnemonic, + }); + + if (!verifyResult.valid) { + this.logger.warn(`[UNFREEZE] Mnemonic verification failed for user: ${userId}`); + return { success: false, message: verifyResult.message || '助记词验证失败' }; + } + } else if (verifyMethod === 'phone') { + if (!phoneNumber || !smsCode) { + return { success: false, message: '请提供手机号和验证码' }; + } + + if (!account.phoneNumber) { + return { success: false, message: '账户未绑定手机号,请使用助记词验证' }; + } + + const phone = PhoneNumber.create(phoneNumber); + if (!account.phoneNumber.equals(phone)) { + return { success: false, message: '手机号与账户不匹配' }; + } + + const cachedCode = await this.redisService.get(`sms:unfreeze:${phone.value}`); + if (cachedCode !== smsCode) { + return { success: false, message: '验证码错误或已过期' }; + } + + // 清除验证码 + await this.redisService.delete(`sms:unfreeze:${phone.value}`); + } else { + return { success: false, message: '不支持的验证方式' }; + } + + try { + account.unfreeze(); + await this.userRepository.save(account); + + // 发布解冻事件 + await this.eventPublisher.publish(new AccountUnfrozenEvent({ + userId, + accountSequence: account.accountSequence.value, + verifyMethod, + unfrozenAt: new Date(), + })); + + this.logger.log(`[UNFREEZE] Account unfrozen successfully for user: ${userId}`); + return { success: true, message: '账户已解冻' }; + } catch (error) { + this.logger.error(`[UNFREEZE] Failed to unfreeze account`, error); + if (error instanceof Error && error.message === '账户未冻结') { + return { success: false, message: '账户未处于冻结状态' }; + } + return { success: false, message: '解冻失败,请稍后重试' }; + } + } + + // ============ 密钥轮换相关 ============ + + /** + * 请求密钥轮换 (POST /user/key-rotation/request) + * + * 用户主动请求轮换 MPC 密钥对 + * 1. 验证当前助记词 + * 2. 发布密钥轮换请求事件 + * 3. MPC 系统后台执行轮换 + */ + async requestKeyRotation(params: { + userId: string; + currentMnemonic: string; + reason: string; + }): Promise<{ success: boolean; message: string; sessionId?: string }> { + const { userId, currentMnemonic, reason } = params; + this.logger.log(`[KEY_ROTATION] Requesting key rotation for user: ${userId}, reason: ${reason}`); + + const account = await this.userRepository.findById(UserId.create(userId)); + if (!account) { + this.logger.warn(`[KEY_ROTATION] User not found: ${userId}`); + return { success: false, message: '用户不存在' }; + } + + if (!account.isActive) { + return { success: false, message: '账户已冻结或注销,无法进行密钥轮换' }; + } + + // 验证当前助记词 + const verifyResult = await this.blockchainClient.verifyMnemonicByAccount({ + accountSequence: account.accountSequence.value, + mnemonic: currentMnemonic, + }); + + if (!verifyResult.valid) { + this.logger.warn(`[KEY_ROTATION] Mnemonic verification failed for user: ${userId}`); + return { success: false, message: verifyResult.message || '助记词验证失败' }; + } + + // 生成轮换会话ID + const sessionId = crypto.randomUUID(); + + // 发布密钥轮换请求事件 + await this.eventPublisher.publish(new KeyRotationRequestedEvent({ + sessionId, + userId, + accountSequence: account.accountSequence.value, + reason, + requestedAt: new Date(), + })); + + this.logger.log(`[KEY_ROTATION] Key rotation requested for user: ${userId}, sessionId: ${sessionId}`); + return { + success: true, + message: '密钥轮换请求已提交,请等待处理完成', + sessionId, + }; + } + + // ============ 恢复码相关 ============ + + /** + * 生成恢复码 (POST /user/backup-codes/generate) + * + * 生成一组一次性恢复码,用于在丢失助记词时恢复账户 + * 每次生成会使之前的恢复码失效 + */ + async generateBackupCodes(params: { + userId: string; + mnemonic: string; + }): Promise<{ success: boolean; message: string; codes?: string[] }> { + const { userId, mnemonic } = params; + this.logger.log(`[BACKUP_CODES] Generating backup codes for user: ${userId}`); + + const account = await this.userRepository.findById(UserId.create(userId)); + if (!account) { + return { success: false, message: '用户不存在' }; + } + + if (!account.isActive) { + return { success: false, message: '账户已冻结或注销' }; + } + + // 验证助记词 + const verifyResult = await this.blockchainClient.verifyMnemonicByAccount({ + accountSequence: account.accountSequence.value, + mnemonic, + }); + + if (!verifyResult.valid) { + return { success: false, message: verifyResult.message || '助记词验证失败' }; + } + + // 生成 8 个恢复码 + const codes: string[] = []; + const hashedCodes: string[] = []; + for (let i = 0; i < 8; i++) { + const code = this.generateBackupCode(); + codes.push(code); + // 存储哈希值,不存储明文 + hashedCodes.push(await this.hashBackupCode(code)); + } + + // 存储到 Redis(带过期时间,1年) + const redisKey = `backup_codes:${account.accountSequence.value}`; + await this.redisService.set(redisKey, JSON.stringify({ + codes: hashedCodes, + generatedAt: new Date().toISOString(), + usedCount: 0, + }), 365 * 24 * 60 * 60); // 1年 + + this.logger.log(`[BACKUP_CODES] Generated 8 backup codes for user: ${userId}`); + return { + success: true, + message: '恢复码已生成,请妥善保管', + codes, + }; + } + + /** + * 使用恢复码恢复账户 (POST /user/recover-by-backup-code) + */ + async recoverByBackupCode(params: { + accountSequence: string; + backupCode: string; + newDeviceId: string; + deviceName?: string; + }): Promise { + const { accountSequence, backupCode, newDeviceId, deviceName } = params; + this.logger.log(`[BACKUP_CODES] Recovering account ${accountSequence} with backup code`); + + const account = await this.userRepository.findByAccountSequence( + AccountSequence.create(accountSequence), + ); + + if (!account) { + await this.eventPublisher.publish(new AccountRecoveryFailedEvent({ + accountSequence, + recoveryMethod: 'mnemonic', // 使用 mnemonic 作为类型,因为恢复码是备用方案 + failureReason: '账户序列号不存在', + deviceId: newDeviceId, + attemptedAt: new Date(), + })); + throw new ApplicationError('账户序列号不存在'); + } + + if (!account.isActive) { + throw new ApplicationError('账户已冻结或注销'); + } + + // 验证恢复码 + const redisKey = `backup_codes:${accountSequence}`; + const storedData = await this.redisService.get(redisKey); + + if (!storedData) { + throw new ApplicationError('未设置恢复码或恢复码已过期'); + } + + const { codes: hashedCodes, usedCount } = JSON.parse(storedData); + const hashedInput = await this.hashBackupCode(backupCode); + + const codeIndex = hashedCodes.findIndex((h: string) => h === hashedInput); + if (codeIndex === -1) { + await this.eventPublisher.publish(new AccountRecoveryFailedEvent({ + accountSequence, + recoveryMethod: 'mnemonic', + failureReason: '恢复码无效', + deviceId: newDeviceId, + attemptedAt: new Date(), + })); + throw new ApplicationError('恢复码无效'); + } + + // 标记该恢复码已使用(设为 null) + hashedCodes[codeIndex] = null; + await this.redisService.set(redisKey, JSON.stringify({ + codes: hashedCodes, + generatedAt: JSON.parse(storedData).generatedAt, + usedCount: usedCount + 1, + }), 365 * 24 * 60 * 60); + + // 恢复账户 + account.addDevice(newDeviceId, deviceName); + account.recordLogin(); + await this.userRepository.save(account); + + const tokens = await this.tokenService.generateTokenPair({ + userId: account.userId.toString(), + accountSequence: account.accountSequence.value, + deviceId: newDeviceId, + }); + + await this.eventPublisher.publishAll(account.domainEvents); + account.clearDomainEvents(); + + // 发布恢复成功事件 + await this.eventPublisher.publish(new AccountRecoveredEvent({ + userId: account.userId.toString(), + accountSequence: account.accountSequence.value, + recoveryMethod: 'mnemonic', // 使用 mnemonic 作为类型 + deviceId: newDeviceId, + deviceName, + recoveredAt: new Date(), + })); + + this.logger.log(`[BACKUP_CODES] Account ${accountSequence} recovered with backup code`); + return { + userId: account.userId.toString(), + accountSequence: account.accountSequence.value, + nickname: account.nickname, + avatarUrl: account.avatarUrl, + referralCode: account.referralCode.value, + accessToken: tokens.accessToken, + refreshToken: tokens.refreshToken, + }; + } + + /** + * 生成单个恢复码 (格式: XXXX-XXXX-XXXX) + */ + private generateBackupCode(): string { + const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // 去掉容易混淆的字符 + const segments: string[] = []; + for (let i = 0; i < 3; i++) { + let segment = ''; + for (let j = 0; j < 4; j++) { + segment += chars.charAt(Math.floor(Math.random() * chars.length)); + } + segments.push(segment); + } + return segments.join('-'); + } + + /** + * 哈希恢复码 + */ + private async hashBackupCode(code: string): Promise { + const crypto = await import('crypto'); + return crypto.createHash('sha256').update(code).digest('hex'); + } } diff --git a/backend/services/identity-service/src/domain/events/index.ts b/backend/services/identity-service/src/domain/events/index.ts index 7a13e7ac..d1e152fe 100644 --- a/backend/services/identity-service/src/domain/events/index.ts +++ b/backend/services/identity-service/src/domain/events/index.ts @@ -262,3 +262,45 @@ export class MnemonicRevokedEvent extends DomainEvent { return 'MnemonicRevoked'; } } + +/** + * 账户解冻事件 (审计日志) + */ +export class AccountUnfrozenEvent extends DomainEvent { + constructor( + public readonly payload: { + userId: string; + accountSequence: string; + verifyMethod: 'mnemonic' | 'phone'; + unfrozenAt: Date; + }, + ) { + super(); + } + + get eventType(): string { + return 'AccountUnfrozen'; + } +} + +/** + * 密钥轮换请求事件 + * 触发 MPC 系统进行密钥轮换 + */ +export class KeyRotationRequestedEvent extends DomainEvent { + constructor( + public readonly payload: { + sessionId: string; + userId: string; + accountSequence: string; + reason: string; + requestedAt: Date; + }, + ) { + super(); + } + + get eventType(): string { + return 'KeyRotationRequested'; + } +}