feat(identity/blockchain): 添加助记词挂失功能
后端: - blockchain-service: 新增 revokeMnemonic() 方法和 POST /internal/mnemonic/revoke API - identity-service: 新增 POST /user/mnemonic/revoke 用户端API - 挂失后助记词状态变为 REVOKED,无法用于账户恢复 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6fb18c6ef2
commit
d3e680ea14
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<boolean> {
|
||||
const revokedRecord = await this.prisma.recoveryMnemonic.findFirst({
|
||||
where: {
|
||||
accountSequence,
|
||||
status: 'REVOKED',
|
||||
},
|
||||
});
|
||||
return !!revokedRecord;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: '上传用户头像' })
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
export class RevokeMnemonicCommand {
|
||||
constructor(
|
||||
public readonly userId: string,
|
||||
public readonly accountSequence: string,
|
||||
public readonly reason: string,
|
||||
) {}
|
||||
}
|
||||
|
|
@ -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: '挂失失败,请稍后重试' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue