diff --git a/openapi.json b/openapi.json index 430c3ee..02e9ff1 100644 --- a/openapi.json +++ b/openapi.json @@ -1,5 +1,5 @@ { - "hash": "beb77cb0b37924ed4121b87dbd01aea157eb2cde455025934bcea22b64dba1a6", + "hash": "8428bef64b9fc5a5c46282383ecf8d80999b834f948d8a5b3565c9306068f66b", "openapi": "3.0.0", "paths": { "/hello": { @@ -7705,16 +7705,6 @@ "UpdateUserDto": { "type": "object", "properties": { - "passwordChangedAt": { - "type": "string", - "description": "上次修改密码时间(与密码哈希一并维护,用于口令轮换等策略)", - "format": "date-time" - }, - "hasPassword": { - "type": "boolean", - "description": "是否有密码", - "readOnly": true - }, "avatar": { "type": "string", "description": "头像" diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 0c0b374..ed1ee01 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -1,3 +1,4 @@ +import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { BadRequestException, Body, @@ -7,6 +8,7 @@ import { Get, HttpCode, HttpStatus, + Inject, NotFoundException, Post, Query, @@ -14,6 +16,7 @@ import { } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { Cache } from 'cache-manager'; import { get, isEqual } from 'lodash'; import { JwtPayload } from 'src/auth'; @@ -59,6 +62,7 @@ function checkUserActive(user: UserDocument) { @Controller('auth') export class AuthController { constructor( + @Inject(CACHE_MANAGER) private readonly cacheManager: Cache, private readonly sessionService: SessionService, private readonly userService: UserService, private readonly jwtService: JwtService, @@ -69,6 +73,10 @@ export class AuthController { private readonly thirdPartyService: ThirdPartyService ) {} + private invalidateUserCache(userId: string) { + return this.cacheManager.del(`/users/${userId}`); + } + /** * login with username/phone/email and password */ @@ -615,6 +623,7 @@ export class AuthController { } await this.userService.updatePassword(user.id, dto.password); + await this.invalidateUserCache(user.id); } /** @@ -640,6 +649,7 @@ export class AuthController { } await this.userService.updatePassword(user.id, dto.password); + await this.invalidateUserCache(user.id); } } diff --git a/src/user/dto/update-user.dto.ts b/src/user/dto/update-user.dto.ts index eb41c0b..482f93d 100644 --- a/src/user/dto/update-user.dto.ts +++ b/src/user/dto/update-user.dto.ts @@ -2,4 +2,8 @@ import { OmitType, PartialType } from '@nestjs/swagger'; import { UserDoc } from '../entities/user.entity'; -export class UpdateUserDto extends OmitType(PartialType(UserDoc), ['password'] as const) {} +export class UpdateUserDto extends OmitType(PartialType(UserDoc), [ + 'password', + 'passwordChangedAt', + 'hasPassword', +] as const) {} diff --git a/test/auth-login-logout.e2e-spec.ts b/test/auth-login-logout.e2e-spec.ts index 547713b..913f452 100644 --- a/test/auth-login-logout.e2e-spec.ts +++ b/test/auth-login-logout.e2e-spec.ts @@ -6,6 +6,7 @@ import { Connection } from 'mongoose'; import request from 'supertest'; import { SessionWithToken } from 'src/auth'; +import { CaptchaService } from 'src/captcha'; import { auth } from 'src/config'; import { NamespaceService } from 'src/namespace'; import { UserService } from 'src/user'; @@ -30,6 +31,7 @@ describe('Web auth (e2e)', () => { let app: INestApplication; let userService: UserService; let namespaceService: NamespaceService; + let captchaService: CaptchaService; const dbName = 'auth-login-logout-e2e'; const mongoUrl = `${mongoTestBaseUrl}/${dbName}`; @@ -48,6 +50,7 @@ describe('Web auth (e2e)', () => { userService = moduleFixture.get(UserService); namespaceService = moduleFixture.get(NamespaceService); + captchaService = moduleFixture.get(CaptchaService); // 准备一个初始化 namespace await namespaceService.create({ @@ -162,4 +165,57 @@ describe('Web auth (e2e)', () => { .set('Accept', 'application/json') .expect(401); }); + + it('reset password by email should invalidate cached user details', async () => { + const userDoc = mockUser(); + const user = await userService.create(userDoc); + const originalPasswordChangedAt = user.passwordChangedAt?.toISOString(); + const captchaKey = `reset-email-${user.id}`; + const captchaCode = '123456'; + + await request(app.getHttpServer()) + .get(`/users/${user.id}`) + .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) + .set('Accept', 'application/json') + .expect(200); + + await captchaService.create({ + key: captchaKey, + code: captchaCode, + }); + + await request(app.getHttpServer()) + .post('/auth/@resetPasswordByEmail') + .send({ + email: userDoc.email, + key: captchaKey, + code: captchaCode, + password: 'Abc12345@', + }) + .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) + .set('Accept', 'application/json') + .expect(204); + + const refreshedUserResp = await request(app.getHttpServer()) + .get(`/users/${user.id}`) + .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) + .set('Accept', 'application/json') + .expect(200); + + expect(refreshedUserResp.body.passwordChangedAt).not.toBe(originalPasswordChangedAt); + + await request(app.getHttpServer()) + .post('/auth/@login') + .send({ + login: userDoc.username, + password: 'Abc12345@', + }) + .set('Content-Type', 'application/json') + .set('x-api-key', auth.apiKey) + .set('Accept', 'application/json') + .expect(200); + }); });