From 9452d14962aea99b33b4e26d577e0edc1baadfb7 Mon Sep 17 00:00:00 2001 From: hailin Date: Fri, 19 Dec 2025 03:23:36 -0800 Subject: [PATCH] =?UTF-8?q?feat(identity-service):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=AF=86=E7=A0=81=E8=AE=BE=E7=BD=AE=E5=92=8C=E7=9F=AD=E4=BF=A1?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 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 --- .../identity-service/package-lock.json | 46 +++++++++++++++++++ .../services/identity-service/package.json | 2 + .../migration.sql | 2 + .../identity-service/prisma/schema.prisma | 1 + .../src/api/dto/request/index.ts | 2 + .../src/api/dto/request/set-password.dto.ts | 16 +++++++ .../api/dto/request/verify-sms-code.dto.ts | 23 ++++++++++ .../src/application/commands/index.ts | 15 ++++++ .../entities/user-account.entity.ts | 1 + 9 files changed, 108 insertions(+) create mode 100644 backend/services/identity-service/prisma/migrations/20251218000000_add_password_hash/migration.sql create mode 100644 backend/services/identity-service/src/api/dto/request/set-password.dto.ts create mode 100644 backend/services/identity-service/src/api/dto/request/verify-sms-code.dto.ts diff --git a/backend/services/identity-service/package-lock.json b/backend/services/identity-service/package-lock.json index b476c54d..a1e0de12 100644 --- a/backend/services/identity-service/package-lock.json +++ b/backend/services/identity-service/package-lock.json @@ -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", diff --git a/backend/services/identity-service/package.json b/backend/services/identity-service/package.json index 6ce09b9e..1d819ed3 100644 --- a/backend/services/identity-service/package.json +++ b/backend/services/identity-service/package.json @@ -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", diff --git a/backend/services/identity-service/prisma/migrations/20251218000000_add_password_hash/migration.sql b/backend/services/identity-service/prisma/migrations/20251218000000_add_password_hash/migration.sql new file mode 100644 index 00000000..b26e204a --- /dev/null +++ b/backend/services/identity-service/prisma/migrations/20251218000000_add_password_hash/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable: Add password_hash column to user_accounts +ALTER TABLE "user_accounts" ADD COLUMN "password_hash" VARCHAR(100); diff --git a/backend/services/identity-service/prisma/schema.prisma b/backend/services/identity-service/prisma/schema.prisma index 43fcc3cc..bcdea888 100644 --- a/backend/services/identity-service/prisma/schema.prisma +++ b/backend/services/identity-service/prisma/schema.prisma @@ -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 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 9f0c7b36..7b1d1b2a 100644 --- a/backend/services/identity-service/src/api/dto/request/index.ts +++ b/backend/services/identity-service/src/api/dto/request/index.ts @@ -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'; diff --git a/backend/services/identity-service/src/api/dto/request/set-password.dto.ts b/backend/services/identity-service/src/api/dto/request/set-password.dto.ts new file mode 100644 index 00000000..159d8ba8 --- /dev/null +++ b/backend/services/identity-service/src/api/dto/request/set-password.dto.ts @@ -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; +} diff --git a/backend/services/identity-service/src/api/dto/request/verify-sms-code.dto.ts b/backend/services/identity-service/src/api/dto/request/verify-sms-code.dto.ts new file mode 100644 index 00000000..1194f4f3 --- /dev/null +++ b/backend/services/identity-service/src/api/dto/request/verify-sms-code.dto.ts @@ -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; +} diff --git a/backend/services/identity-service/src/application/commands/index.ts b/backend/services/identity-service/src/application/commands/index.ts index 4cfb9cde..a75a251a 100644 --- a/backend/services/identity-service/src/application/commands/index.ts +++ b/backend/services/identity-service/src/application/commands/index.ts @@ -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 ============ // 钱包状态 diff --git a/backend/services/identity-service/src/infrastructure/persistence/entities/user-account.entity.ts b/backend/services/identity-service/src/infrastructure/persistence/entities/user-account.entity.ts index bdcd1c32..4f8a7f90 100644 --- a/backend/services/identity-service/src/infrastructure/persistence/entities/user-account.entity.ts +++ b/backend/services/identity-service/src/infrastructure/persistence/entities/user-account.entity.ts @@ -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位序号