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()