feat(identity-service): 添加密码设置和短信验证功能

- 添加 bcrypt 依赖用于密码哈希
- 添加 passwordHash 字段到 UserAccount 模型
- 添加 VerifySmsCodeCommand 和 SetPasswordCommand
- 添加 VerifySmsCodeDto 和 SetPasswordDto
- 添加数据库迁移 add_password_hash

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-19 03:23:36 -08:00
parent 2662409d80
commit 9452d14962
9 changed files with 108 additions and 0 deletions

View File

@ -25,6 +25,7 @@
"@prisma/client": "^5.7.0",
"@scure/bip32": "^1.3.2",
"@scure/bip39": "^1.2.1",
"bcrypt": "^6.0.0",
"bech32": "^2.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
@ -43,6 +44,7 @@
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/bcrypt": "^6.0.0",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/jsonwebtoken": "^9.0.0",
@ -2689,6 +2691,16 @@
"@babel/types": "^7.28.2"
}
},
"node_modules/@types/bcrypt": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz",
"integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/body-parser": {
"version": "1.19.6",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
@ -3898,6 +3910,20 @@
"baseline-browser-mapping": "dist/cli.js"
}
},
"node_modules/bcrypt": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
"integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^8.3.0",
"node-gyp-build": "^4.8.4"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/bech32": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz",
@ -8357,6 +8383,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"license": "MIT",
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/node-emoji": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",
@ -8387,6 +8422,17 @@
}
}
},
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"license": "MIT",
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",

View File

@ -44,6 +44,7 @@
"@prisma/client": "^5.7.0",
"@scure/bip32": "^1.3.2",
"@scure/bip39": "^1.2.1",
"bcrypt": "^6.0.0",
"bech32": "^2.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
@ -62,6 +63,7 @@
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/bcrypt": "^6.0.0",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/jsonwebtoken": "^9.0.0",

View File

@ -0,0 +1,2 @@
-- AlterTable: Add password_hash column to user_accounts
ALTER TABLE "user_accounts" ADD COLUMN "password_hash" VARCHAR(100);

View File

@ -12,6 +12,7 @@ model UserAccount {
accountSequence String @unique @map("account_sequence") @db.VarChar(12) // 格式: D + YYMMDD + 5位序号
phoneNumber String? @unique @map("phone_number") @db.VarChar(20)
passwordHash String? @map("password_hash") @db.VarChar(100) // bcrypt 哈希密码
nickname String @db.VarChar(100)
avatarUrl String? @map("avatar_url") @db.Text

View File

@ -9,3 +9,5 @@ export * from './unfreeze-account.dto';
export * from './request-key-rotation.dto';
export * from './generate-backup-codes.dto';
export * from './recover-by-backup-code.dto';
export * from './verify-sms-code.dto';
export * from './set-password.dto';

View File

@ -0,0 +1,16 @@
import { IsString, MinLength, MaxLength, Matches } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class SetPasswordDto {
@ApiProperty({
example: 'Abc123456',
description: '登录密码6-20位包含字母和数字',
})
@IsString()
@MinLength(6, { message: '密码长度至少6位' })
@MaxLength(20, { message: '密码长度不能超过20位' })
@Matches(/^(?=.*[A-Za-z])(?=.*\d).+$/, {
message: '密码需包含字母和数字',
})
password: string;
}

View File

@ -0,0 +1,23 @@
import { IsString, Matches, IsIn } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class VerifySmsCodeDto {
@ApiProperty({ example: '13800138000', description: '手机号' })
@IsString()
@Matches(/^1[3-9]\d{9}$/, { message: '手机号格式错误' })
phoneNumber: string;
@ApiProperty({ example: '123456', description: '6位验证码' })
@IsString()
@Matches(/^\d{6}$/, { message: '验证码格式错误' })
smsCode: string;
@ApiProperty({
example: 'REGISTER',
description: '验证码类型',
enum: ['REGISTER', 'LOGIN', 'BIND', 'RECOVER'],
})
@IsString()
@IsIn(['REGISTER', 'LOGIN', 'BIND', 'RECOVER'], { message: '无效的验证码类型' })
type: string;
}

View File

@ -157,6 +157,21 @@ export class MarkMnemonicBackedUpCommand {
constructor(public readonly userId: string) {}
}
export class VerifySmsCodeCommand {
constructor(
public readonly phoneNumber: string,
public readonly smsCode: string,
public readonly type: 'REGISTER' | 'LOGIN' | 'BIND' | 'RECOVER',
) {}
}
export class SetPasswordCommand {
constructor(
public readonly userId: string,
public readonly password: string,
) {}
}
// ============ Results ============
// 钱包状态

View File

@ -3,6 +3,7 @@ export interface UserAccountEntity {
userId: bigint;
accountSequence: string; // 格式: D + YYMMDD + 5位序号
phoneNumber: string | null;
passwordHash: string | null; // bcrypt 哈希密码
nickname: string;
avatarUrl: string | null;
inviterSequence: string | null; // 格式: D + YYMMDD + 5位序号