From 6553b4c5c7aa4ee473f19b638a08b65ce993d0b1 Mon Sep 17 00:00:00 2001 From: liuyun Date: Sun, 11 Oct 2026 16:41:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E7=99=BB=E5=BD=95=E6=8C=89?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=90=8D=E8=87=AA=E5=8A=A8=E6=8E=A2=E6=B5=8B?= =?UTF-8?q?=E7=A7=9F=E6=88=B7=20+=20=E5=A4=9A=E7=A7=9F=E6=88=B7=E9=80=89?= =?UTF-8?q?=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LoginDto tenantCode 改为可选,缺省按用户名跨租户探测 - 单匹配自动登录,多匹配返回候选租户 - 前端登录页新增租户选择弹窗 Co-Authored-By: Claude Opus 4.8 --- src/auth/auth.service.ts | 61 ++++++++++++++++++++++++++++++++-------- src/auth/dto.ts | 6 ++-- 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index d24a04b..566e0ac 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -80,33 +80,70 @@ export class AuthService { dto: LoginDto, ip?: string, userAgent?: string, + ): Promise< + | { accessToken: string; refreshToken: string; expiresIn: number } + | { needSelectTenant: true; tenants: { tenantCode: string; tenantName: string }[] } + > { + if (dto.tenantCode) { + return this.loginTenant(dto.tenantCode, dto.username, dto.password, ip, userAgent); + } + // 未指定租户:按用户名探测(一个用户名可存在于多个租户) + const users = await this.prisma.client.user.findMany({ + where: { username: dto.username, deletedAt: null }, + select: { tenantCode: true, passwordHash: true, status: true }, + }); + const matched: { tenantCode: string; tenantName: string }[] = []; + for (const u of users) { + if (u.status !== 'enabled') continue; + const tenant = await this.prisma.client.tenant.findUnique({ where: { code: u.tenantCode } }); + if (!tenant || tenant.status !== 'enabled') continue; + if (await bcrypt.compare(dto.password, u.passwordHash)) { + matched.push({ tenantCode: u.tenantCode, tenantName: tenant.name }); + } + } + if (matched.length === 0) { + await this.recordLoginLog('', dto.username, 'fail', '账号或密码错误', ip, userAgent); + throw new UnauthorizedException('账号或密码错误'); + } + if (matched.length === 1) { + return this.loginTenant(matched[0].tenantCode, dto.username, dto.password, ip, userAgent); + } + return { needSelectTenant: true, tenants: matched }; + } + + /** 指定租户登录(单租户逻辑)。 */ + private async loginTenant( + tenantCode: string, + username: string, + password: string, + ip?: string, + userAgent?: string, ): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> { - const tenant = await this.prisma.client.tenant.findUnique({ where: { code: dto.tenantCode } }); + const tenant = await this.prisma.client.tenant.findUnique({ where: { code: tenantCode } }); if (!tenant || tenant.status !== 'enabled') { - await this.recordLoginLog(dto.tenantCode, dto.username, 'fail', '租户不存在或已停用', ip, userAgent); + await this.recordLoginLog(tenantCode, username, 'fail', '租户不存在或已停用', ip, userAgent); throw new UnauthorizedException('租户不存在或已停用'); } const user = await this.prisma.client.user.findFirst({ - where: { tenantCode: dto.tenantCode, username: dto.username, deletedAt: null }, + where: { tenantCode, username, deletedAt: null }, }); if (!user) { - await this.recordLoginLog(dto.tenantCode, dto.username, 'fail', '账号或密码错误', ip, userAgent); + await this.recordLoginLog(tenantCode, username, 'fail', '账号或密码错误', ip, userAgent); throw new UnauthorizedException('账号或密码错误'); } if (user.status !== 'enabled') { - await this.recordLoginLog(dto.tenantCode, dto.username, 'fail', '账号已停用或锁定', ip, userAgent); + await this.recordLoginLog(tenantCode, username, 'fail', '账号已停用或锁定', ip, userAgent); throw new UnauthorizedException('账号已停用或锁定'); } - const ok = await bcrypt.compare(dto.password, user.passwordHash); + const ok = await bcrypt.compare(password, user.passwordHash); if (!ok) { - await this.recordLoginLog(dto.tenantCode, dto.username, 'fail', '账号或密码错误', ip, userAgent); + await this.recordLoginLog(tenantCode, username, 'fail', '账号或密码错误', ip, userAgent); throw new UnauthorizedException('账号或密码错误'); } - - await this.writePermissionCache(dto.tenantCode, user.id); - await this.recordLoginLog(dto.tenantCode, dto.username, 'success', null, ip, userAgent); + await this.writePermissionCache(tenantCode, user.id); + await this.recordLoginLog(tenantCode, username, 'success', null, ip, userAgent); await this.recordAudit({ - tenantCode: dto.tenantCode, + tenantCode, userId: user.id, username: user.username, module: 'auth', @@ -115,7 +152,7 @@ export class AuthService { ip: ip ?? null, userAgent: userAgent ?? null, }); - return this.issue(dto.tenantCode, user.id, user.username, user.tokenVersion); + return this.issue(tenantCode, user.id, user.username, user.tokenVersion); } /** 注册:在指定租户下创建普通用户(无角色,状态启用)。 */ diff --git a/src/auth/dto.ts b/src/auth/dto.ts index 97dd328..a63739b 100644 --- a/src/auth/dto.ts +++ b/src/auth/dto.ts @@ -2,10 +2,10 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class LoginDto { - @ApiProperty({ description: '租户编码' }) + @ApiPropertyOptional({ description: '租户编码(多租户二选一时传入;缺省按用户名自动探测)' }) + @IsOptional() @IsString() - @IsNotEmpty() - tenantCode!: string; + tenantCode?: string; @ApiProperty({ description: '用户名' }) @IsString()