diff --git a/backend/services/blockchain-service/src/api/controllers/internal.controller.ts b/backend/services/blockchain-service/src/api/controllers/internal.controller.ts index 8d965dce..770b0f0d 100644 --- a/backend/services/blockchain-service/src/api/controllers/internal.controller.ts +++ b/backend/services/blockchain-service/src/api/controllers/internal.controller.ts @@ -3,7 +3,7 @@ import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { AddressDerivationService } from '@/application/services/address-derivation.service'; import { MnemonicVerificationService } from '@/application/services/mnemonic-verification.service'; import { MnemonicDerivationAdapter } from '@/infrastructure/blockchain'; -import { DeriveAddressDto, VerifyMnemonicDto, VerifyMnemonicHashDto, MarkMnemonicBackupDto } from '../dto/request'; +import { DeriveAddressDto, VerifyMnemonicDto, VerifyMnemonicHashDto, MarkMnemonicBackupDto, RevokeMnemonicDto } from '../dto/request'; import { DeriveAddressResponseDto } from '../dto/response'; /** @@ -102,4 +102,12 @@ export class InternalController { message: 'Mnemonic marked as backed up', }; } + + @Post('mnemonic/revoke') + @ApiOperation({ summary: '挂失助记词' }) + @ApiResponse({ status: 200, description: '挂失结果' }) + async revokeMnemonic(@Body() dto: RevokeMnemonicDto) { + const result = await this.mnemonicVerification.revokeMnemonic(dto.accountSequence, dto.reason); + return result; + } } diff --git a/backend/services/blockchain-service/src/api/dto/request/index.ts b/backend/services/blockchain-service/src/api/dto/request/index.ts index b65ae789..d0ec70ab 100644 --- a/backend/services/blockchain-service/src/api/dto/request/index.ts +++ b/backend/services/blockchain-service/src/api/dto/request/index.ts @@ -3,3 +3,4 @@ export * from './derive-address.dto'; export * from './verify-mnemonic.dto'; export * from './verify-mnemonic-hash.dto'; export * from './mark-mnemonic-backup.dto'; +export * from './revoke-mnemonic.dto'; diff --git a/backend/services/blockchain-service/src/api/dto/request/revoke-mnemonic.dto.ts b/backend/services/blockchain-service/src/api/dto/request/revoke-mnemonic.dto.ts new file mode 100644 index 00000000..ae2c1b78 --- /dev/null +++ b/backend/services/blockchain-service/src/api/dto/request/revoke-mnemonic.dto.ts @@ -0,0 +1,15 @@ +import { IsString, IsNotEmpty, MaxLength } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class RevokeMnemonicDto { + @ApiProperty({ example: 'D2512110001', description: '账户序列号' }) + @IsString() + @IsNotEmpty() + accountSequence: string; + + @ApiProperty({ example: '助记词泄露', description: '挂失原因' }) + @IsString() + @IsNotEmpty() + @MaxLength(200) + reason: string; +} diff --git a/backend/services/blockchain-service/src/application/services/mnemonic-verification.service.ts b/backend/services/blockchain-service/src/application/services/mnemonic-verification.service.ts index 163a8e56..d166fa17 100644 --- a/backend/services/blockchain-service/src/application/services/mnemonic-verification.service.ts +++ b/backend/services/blockchain-service/src/application/services/mnemonic-verification.service.ts @@ -100,4 +100,57 @@ export class MnemonicVerificationService { }); this.logger.log(`Recovery mnemonic marked as backed up for account ${accountSequence}`); } + + /** + * 挂失助记词 + * 将 ACTIVE 状态的助记词标记为 REVOKED + */ + async revokeMnemonic(accountSequence: string, reason: string): Promise<{ success: boolean; message: string }> { + this.logger.log(`Revoking mnemonic for account ${accountSequence}, reason: ${reason}`); + + // 查找 ACTIVE 状态的助记词 + const activeRecord = await this.prisma.recoveryMnemonic.findFirst({ + where: { + accountSequence, + status: 'ACTIVE', + }, + }); + + if (!activeRecord) { + this.logger.warn(`No active mnemonic found for account ${accountSequence}`); + return { + success: false, + message: '该账户没有可挂失的助记词', + }; + } + + // 更新状态为 REVOKED + await this.prisma.recoveryMnemonic.update({ + where: { id: activeRecord.id }, + data: { + status: 'REVOKED', + revokedAt: new Date(), + revokedReason: reason, + }, + }); + + this.logger.log(`Mnemonic revoked successfully for account ${accountSequence}`); + return { + success: true, + message: '助记词已挂失', + }; + } + + /** + * 检查助记词是否已挂失 + */ + async isMnemonicRevoked(accountSequence: string): Promise { + const revokedRecord = await this.prisma.recoveryMnemonic.findFirst({ + where: { + accountSequence, + status: 'REVOKED', + }, + }); + return !!revokedRecord; + } } 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 8e7748d3..becf3cc3 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 @@ -17,7 +17,7 @@ import { import { AutoCreateAccountDto, RecoverByMnemonicDto, RecoverByPhoneDto, AutoLoginDto, SendSmsCodeDto, RegisterDto, LoginDto, BindPhoneDto, UpdateProfileDto, - BindWalletDto, SubmitKYCDto, RemoveDeviceDto, + BindWalletDto, SubmitKYCDto, RemoveDeviceDto, RevokeMnemonicDto, AutoCreateAccountResponseDto, RecoverAccountResponseDto, LoginResponseDto, UserProfileResponseDto, DeviceResponseDto, WalletStatusReadyResponseDto, WalletStatusGeneratingResponseDto, @@ -198,6 +198,14 @@ export class UserAccountController { return { message: '已标记为已备份' }; } + @Post('mnemonic/revoke') + @ApiBearerAuth() + @ApiOperation({ summary: '挂失助记词', description: '用户主动挂失助记词,挂失后该助记词将无法用于账户恢复' }) + @ApiResponse({ status: 200, description: '挂失结果' }) + async revokeMnemonic(@CurrentUser() user: CurrentUserData, @Body() dto: RevokeMnemonicDto) { + return this.userService.revokeMnemonic(user.userId, dto.reason); + } + @Post('upload-avatar') @ApiBearerAuth() @ApiOperation({ summary: '上传用户头像' }) 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 9dcbabe5..b524c6ef 100644 --- a/backend/services/identity-service/src/api/dto/request/index.ts +++ b/backend/services/identity-service/src/api/dto/request/index.ts @@ -3,3 +3,4 @@ export * from './recover-by-mnemonic.dto'; export * from './recover-by-phone.dto'; export * from './bind-phone.dto'; export * from './submit-kyc.dto'; +export * from './revoke-mnemonic.dto'; diff --git a/backend/services/identity-service/src/api/dto/request/revoke-mnemonic.dto.ts b/backend/services/identity-service/src/api/dto/request/revoke-mnemonic.dto.ts new file mode 100644 index 00000000..97d65a18 --- /dev/null +++ b/backend/services/identity-service/src/api/dto/request/revoke-mnemonic.dto.ts @@ -0,0 +1,10 @@ +import { IsString, IsNotEmpty, MaxLength } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class RevokeMnemonicDto { + @ApiProperty({ example: '助记词泄露', description: '挂失原因' }) + @IsString() + @IsNotEmpty({ message: '请填写挂失原因' }) + @MaxLength(200, { message: '挂失原因不能超过200字' }) + reason: string; +} diff --git a/backend/services/identity-service/src/application/commands/revoke-mnemonic/revoke-mnemonic.command.ts b/backend/services/identity-service/src/application/commands/revoke-mnemonic/revoke-mnemonic.command.ts new file mode 100644 index 00000000..287d8e54 --- /dev/null +++ b/backend/services/identity-service/src/application/commands/revoke-mnemonic/revoke-mnemonic.command.ts @@ -0,0 +1,7 @@ +export class RevokeMnemonicCommand { + constructor( + public readonly userId: string, + public readonly accountSequence: string, + public readonly reason: 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 ce3de2a3..9c7349f3 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 @@ -775,4 +775,33 @@ export class UserApplicationService { } } } + + // ============ 助记词挂失相关 ============ + + /** + * 挂失助记词 (POST /user/mnemonic/revoke) + * + * 用户主动挂失助记词,防止泄露后被滥用 + * 挂失后该助记词将无法用于账户恢复 + */ + async revokeMnemonic(userId: string, reason: string): Promise<{ success: boolean; message: string }> { + this.logger.log(`[REVOKE] Revoking mnemonic for user: ${userId}, reason: ${reason}`); + + // 1. 获取用户的 accountSequence + const account = await this.userRepository.findById(UserId.create(userId)); + if (!account) { + this.logger.warn(`[REVOKE] User not found: ${userId}`); + return { success: false, message: '用户不存在' }; + } + + // 2. 调用 blockchain-service 挂失助记词 + try { + const result = await this.blockchainClient.revokeMnemonic(account.accountSequence.value, reason); + this.logger.log(`[REVOKE] Mnemonic revoke result: ${JSON.stringify(result)}`); + return result; + } catch (error) { + this.logger.error(`[REVOKE] Failed to revoke mnemonic`, error); + return { success: false, message: '挂失失败,请稍后重试' }; + } + } } diff --git a/backend/services/identity-service/src/infrastructure/external/blockchain/blockchain-client.service.ts b/backend/services/identity-service/src/infrastructure/external/blockchain/blockchain-client.service.ts index 070852e8..9fa63352 100644 --- a/backend/services/identity-service/src/infrastructure/external/blockchain/blockchain-client.service.ts +++ b/backend/services/identity-service/src/infrastructure/external/blockchain/blockchain-client.service.ts @@ -165,4 +165,30 @@ export class BlockchainClientService { throw error; } } + + /** + * 挂失助记词 + */ + async revokeMnemonic(accountSequence: string, reason: string): Promise<{ success: boolean; message: string }> { + this.logger.log(`Revoking mnemonic for account ${accountSequence}, reason: ${reason}`); + + try { + const response = await firstValueFrom( + this.httpService.post<{ success: boolean; message: string }>( + `${this.blockchainServiceUrl}/internal/mnemonic/revoke`, + { accountSequence, reason }, + { + headers: { 'Content-Type': 'application/json' }, + timeout: 30000, + }, + ), + ); + + this.logger.log(`Mnemonic revoke result: success=${response.data.success}`); + return response.data; + } catch (error) { + this.logger.error('Failed to revoke mnemonic', error); + throw error; + } + } }