From d3ee321d21e9b7eeef2ef3af04258ce04aac19a6 Mon Sep 17 00:00:00 2001 From: ZHAO HONG Date: Mon, 13 Apr 2026 16:34:23 +0800 Subject: [PATCH] feat(user): track password change time --- openapi.json | 17 ++++++++++++++++- src/user/entities/user.entity.ts | 10 ++++++++++ src/user/user.service.spec.ts | 17 +++++++++++++++++ src/user/user.service.ts | 7 ++++++- 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/openapi.json b/openapi.json index ea9a308..430c3ee 100644 --- a/openapi.json +++ b/openapi.json @@ -1,5 +1,5 @@ { - "hash": "32fdca154a0dc1d7a0200aa8059b4927d0a7cf297acc843beb792e348b72a6da", + "hash": "beb77cb0b37924ed4121b87dbd01aea157eb2cde455025934bcea22b64dba1a6", "openapi": "3.0.0", "paths": { "/hello": { @@ -6010,6 +6010,11 @@ "description": "密码", "writeOnly": true }, + "passwordChangedAt": { + "type": "string", + "description": "上次修改密码时间(与密码哈希一并维护,用于口令轮换等策略)", + "format": "date-time" + }, "hasPassword": { "type": "boolean", "description": "是否有密码", @@ -7556,6 +7561,11 @@ "description": "密码", "writeOnly": true }, + "passwordChangedAt": { + "type": "string", + "description": "上次修改密码时间(与密码哈希一并维护,用于口令轮换等策略)", + "format": "date-time" + }, "hasPassword": { "type": "boolean", "description": "是否有密码", @@ -7695,6 +7705,11 @@ "UpdateUserDto": { "type": "object", "properties": { + "passwordChangedAt": { + "type": "string", + "description": "上次修改密码时间(与密码哈希一并维护,用于口令轮换等策略)", + "format": "date-time" + }, "hasPassword": { "type": "boolean", "description": "是否有密码", diff --git a/src/user/entities/user.entity.ts b/src/user/entities/user.entity.ts index 0533534..00449ac 100644 --- a/src/user/entities/user.entity.ts +++ b/src/user/entities/user.entity.ts @@ -158,6 +158,16 @@ export class UserDoc { @Prop({ hideJSON: true }) password?: string; + /** + * 上次修改密码时间(与密码哈希一并维护,用于口令轮换等策略) + */ + @IsOptional() + @IsDate() + @Type(() => Date) + @ApiProperty({ type: String, format: 'date-time', required: false }) + @Prop() + passwordChangedAt?: Date; + /** * 手机号 */ diff --git a/src/user/user.service.spec.ts b/src/user/user.service.spec.ts index ef0523d..20efdbe 100644 --- a/src/user/user.service.spec.ts +++ b/src/user/user.service.spec.ts @@ -59,10 +59,13 @@ describe('UserService', () => { describe('createUser', () => { it('should create a user', async () => { const userDoc = mockUser(); + const before = Date.now(); const user = await userService.create(userDoc); expect(user).toBeDefined(); expect(typeof user.id).toBe('string'); expect(userService.checkPassword(user.password, userDoc.password)).toBeTruthy(); + expect(user.passwordChangedAt).toBeInstanceOf(Date); + expect(user.passwordChangedAt.getTime()).toBeGreaterThanOrEqual(before); }); it('should keep an explicit id when provided', async () => { @@ -153,11 +156,25 @@ describe('UserService', () => { }); }); + describe('updatePassword', () => { + it('should set passwordChangedAt when password is updated', async () => { + const user = await userService.create(mockUser()); + const before = Date.now(); + const updated = await userService.updatePassword(user.id, 'new-secret-1'); + expect(userService.checkPassword(updated.password, 'new-secret-1')).toBe(true); + expect(updated.passwordChangedAt).toBeInstanceOf(Date); + expect(updated.passwordChangedAt.getTime()).toBeGreaterThanOrEqual(before); + }); + }); + describe('upsertUser', () => { it('should upsert a user', async () => { const userDoc = mockUser(); + const before = Date.now(); const user = await userService.upsertByPhone('18888888888', userDoc); expect(user.email).toBe(userDoc.email); + expect(user.passwordChangedAt).toBeInstanceOf(Date); + expect(user.passwordChangedAt.getTime()).toBeGreaterThanOrEqual(before); }); it('should upsert a user by id', async () => { diff --git a/src/user/user.service.ts b/src/user/user.service.ts index 3801432..c14b96c 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -36,6 +36,7 @@ function hashPwd(dto: CreateUserDto) { const res = { ...dto }; if (dto.password) { res.password = createHash(dto.password); + res.passwordChangedAt = new Date(); } return res; } @@ -193,7 +194,11 @@ export class UserService { updatePassword(id: string, password: string): Promise { return this.userModel - .findByIdAndUpdate(id, { password: createHash(password) }, { new: true }) + .findByIdAndUpdate( + id, + { password: createHash(password), passwordChangedAt: new Date() }, + { new: true } + ) .exec(); }