feat(auth): 登录按用户名自动探测租户 + 多租户选择
- LoginDto tenantCode 改为可选,缺省按用户名跨租户探测 - 单匹配自动登录,多匹配返回候选租户 - 前端登录页新增租户选择弹窗 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
6578db51c7
commit
6553b4c5c7
@ -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);
|
||||
}
|
||||
|
||||
/** 注册:在指定租户下创建普通用户(无角色,状态启用)。 */
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user