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:
parent
2662409d80
commit
9452d14962
|
|
@ -25,6 +25,7 @@
|
||||||
"@prisma/client": "^5.7.0",
|
"@prisma/client": "^5.7.0",
|
||||||
"@scure/bip32": "^1.3.2",
|
"@scure/bip32": "^1.3.2",
|
||||||
"@scure/bip39": "^1.2.1",
|
"@scure/bip39": "^1.2.1",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
"bech32": "^2.0.0",
|
"bech32": "^2.0.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
|
|
@ -43,6 +44,7 @@
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
"@nestjs/schematics": "^10.0.0",
|
"@nestjs/schematics": "^10.0.0",
|
||||||
"@nestjs/testing": "^10.0.0",
|
"@nestjs/testing": "^10.0.0",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.2",
|
||||||
"@types/jsonwebtoken": "^9.0.0",
|
"@types/jsonwebtoken": "^9.0.0",
|
||||||
|
|
@ -2689,6 +2691,16 @@
|
||||||
"@babel/types": "^7.28.2"
|
"@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": {
|
"node_modules/@types/body-parser": {
|
||||||
"version": "1.19.6",
|
"version": "1.19.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||||
|
|
@ -3898,6 +3910,20 @@
|
||||||
"baseline-browser-mapping": "dist/cli.js"
|
"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": {
|
"node_modules/bech32": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz",
|
||||||
|
|
@ -8357,6 +8383,15 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/node-emoji": {
|
||||||
"version": "1.11.0",
|
"version": "1.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",
|
"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": {
|
"node_modules/node-int64": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@
|
||||||
"@prisma/client": "^5.7.0",
|
"@prisma/client": "^5.7.0",
|
||||||
"@scure/bip32": "^1.3.2",
|
"@scure/bip32": "^1.3.2",
|
||||||
"@scure/bip39": "^1.2.1",
|
"@scure/bip39": "^1.2.1",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
"bech32": "^2.0.0",
|
"bech32": "^2.0.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
|
|
@ -62,6 +63,7 @@
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
"@nestjs/schematics": "^10.0.0",
|
"@nestjs/schematics": "^10.0.0",
|
||||||
"@nestjs/testing": "^10.0.0",
|
"@nestjs/testing": "^10.0.0",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.2",
|
||||||
"@types/jsonwebtoken": "^9.0.0",
|
"@types/jsonwebtoken": "^9.0.0",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable: Add password_hash column to user_accounts
|
||||||
|
ALTER TABLE "user_accounts" ADD COLUMN "password_hash" VARCHAR(100);
|
||||||
|
|
@ -12,6 +12,7 @@ model UserAccount {
|
||||||
accountSequence String @unique @map("account_sequence") @db.VarChar(12) // 格式: D + YYMMDD + 5位序号
|
accountSequence String @unique @map("account_sequence") @db.VarChar(12) // 格式: D + YYMMDD + 5位序号
|
||||||
|
|
||||||
phoneNumber String? @unique @map("phone_number") @db.VarChar(20)
|
phoneNumber String? @unique @map("phone_number") @db.VarChar(20)
|
||||||
|
passwordHash String? @map("password_hash") @db.VarChar(100) // bcrypt 哈希密码
|
||||||
nickname String @db.VarChar(100)
|
nickname String @db.VarChar(100)
|
||||||
avatarUrl String? @map("avatar_url") @db.Text
|
avatarUrl String? @map("avatar_url") @db.Text
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,5 @@ export * from './unfreeze-account.dto';
|
||||||
export * from './request-key-rotation.dto';
|
export * from './request-key-rotation.dto';
|
||||||
export * from './generate-backup-codes.dto';
|
export * from './generate-backup-codes.dto';
|
||||||
export * from './recover-by-backup-code.dto';
|
export * from './recover-by-backup-code.dto';
|
||||||
|
export * from './verify-sms-code.dto';
|
||||||
|
export * from './set-password.dto';
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -157,6 +157,21 @@ export class MarkMnemonicBackedUpCommand {
|
||||||
constructor(public readonly userId: string) {}
|
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 ============
|
// ============ Results ============
|
||||||
|
|
||||||
// 钱包状态
|
// 钱包状态
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ export interface UserAccountEntity {
|
||||||
userId: bigint;
|
userId: bigint;
|
||||||
accountSequence: string; // 格式: D + YYMMDD + 5位序号
|
accountSequence: string; // 格式: D + YYMMDD + 5位序号
|
||||||
phoneNumber: string | null;
|
phoneNumber: string | null;
|
||||||
|
passwordHash: string | null; // bcrypt 哈希密码
|
||||||
nickname: string;
|
nickname: string;
|
||||||
avatarUrl: string | null;
|
avatarUrl: string | null;
|
||||||
inviterSequence: string | null; // 格式: D + YYMMDD + 5位序号
|
inviterSequence: string | null; // 格式: D + YYMMDD + 5位序号
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue