From 6578db51c70e1924053adc019edcb3177c326d25 Mon Sep 17 00:00:00 2001 From: liuyun Date: Sun, 11 Oct 2026 16:34:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20PATCH=20/auth/me/password=20?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=9C=AC=E4=BA=BA=E5=AF=86=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 校验原密码 + 更新后 tokenVersion 吊销其它会话 Co-Authored-By: Claude Opus 4.8 --- src/auth/auth.controller.ts | 12 ++++++++++-- src/auth/auth.service.ts | 21 ++++++++++++++++++++- src/auth/dto.ts | 12 ++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index a25de64..dfdc2e4 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -1,7 +1,7 @@ -import { Body, Controller, Get, Post, Req, UnauthorizedException } from '@nestjs/common'; +import { Body, Controller, Get, Patch, Post, Req, UnauthorizedException } from '@nestjs/common'; import type { Request } from 'express'; import { AuthService } from './auth.service.js'; -import { LoginDto, RefreshDto, RegisterDto } from './dto.js'; +import { ChangePasswordDto, LoginDto, RefreshDto, RegisterDto } from './dto.js'; import { Public } from './decorators.js'; import { getRequestContext } from '../common/context/request-context.js'; @@ -48,4 +48,12 @@ export class AuthController { if (!ctx) throw new UnauthorizedException('未登录'); return this.auth.getPermissionSet(ctx.tenantCode, ctx.userId); } + + /** 修改本人密码(仅需登录)。 */ + @Patch('me/password') + changePassword(@Body() dto: ChangePasswordDto) { + const ctx = getRequestContext(); + if (!ctx) throw new UnauthorizedException('未登录'); + return this.auth.changePassword(ctx.tenantCode, ctx.userId, dto); + } } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 1037adc..d24a04b 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -4,7 +4,7 @@ import bcrypt from 'bcryptjs'; import { createHash, randomUUID } from 'node:crypto'; import { PrismaService } from '../prisma/prisma.service.js'; import { RedisService } from '../common/redis/redis.service.js'; -import { LoginDto, RefreshDto, RegisterDto } from './dto.js'; +import { LoginDto, RefreshDto, RegisterDto, ChangePasswordDto } from './dto.js'; import { resolveDataScope, resolveVisibleOrgs } from '../common/data-scope.js'; import type { DataScope } from '../common/enums.js'; import type { RequestContext } from '../common/context/request-context.js'; @@ -140,6 +140,25 @@ export class AuthService { return { id: user.id, username: user.username }; } + /** 修改本人密码:校验原密码,更新后吊销其它会话。 */ + async changePassword(tenantCode: string, userId: string, dto: ChangePasswordDto): Promise { + const user = await this.prisma.client.user.findFirst({ + where: { tenantCode, id: userId, deletedAt: null }, + }); + if (!user) throw new UnauthorizedException('账号不存在'); + const ok = await bcrypt.compare(dto.oldPassword, user.passwordHash); + if (!ok) throw new BadRequestException('原密码错误'); + const passwordHash = await bcrypt.hash(dto.newPassword, 10); + await this.prisma.client.user.updateMany({ + where: { tenantCode, id: userId }, + data: { passwordHash, tokenVersion: { increment: 1 } }, + }); + await this.prisma.client.refreshToken.deleteMany({ where: { tenantCode, userId } }); + await this.redis.del(permCacheKey(tenantCode, userId)); + await this.redis.del(userStateCacheKey(tenantCode, userId)); + return null; + } + async refresh(dto: RefreshDto): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> { const record = await this.prisma.client.refreshToken.findUnique({ where: { jti: dto.refreshToken } }); if (!record || record.revokedAt || record.expireAt.getTime() < Date.now()) { diff --git a/src/auth/dto.ts b/src/auth/dto.ts index ebc251e..97dd328 100644 --- a/src/auth/dto.ts +++ b/src/auth/dto.ts @@ -45,6 +45,18 @@ export class RegisterDto { phone?: string; } +export class ChangePasswordDto { + @ApiProperty({ description: '原密码' }) + @IsString() + @IsNotEmpty() + oldPassword!: string; + + @ApiProperty({ description: '新密码' }) + @IsString() + @IsNotEmpty() + newPassword!: string; +} + export class RefreshDto { @ApiProperty({ description: '刷新令牌' }) @IsString()