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,
|
dto: LoginDto,
|
||||||
ip?: string,
|
ip?: string,
|
||||||
userAgent?: 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 }> {
|
): 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') {
|
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('租户不存在或已停用');
|
throw new UnauthorizedException('租户不存在或已停用');
|
||||||
}
|
}
|
||||||
const user = await this.prisma.client.user.findFirst({
|
const user = await this.prisma.client.user.findFirst({
|
||||||
where: { tenantCode: dto.tenantCode, username: dto.username, deletedAt: null },
|
where: { tenantCode, username, deletedAt: null },
|
||||||
});
|
});
|
||||||
if (!user) {
|
if (!user) {
|
||||||
await this.recordLoginLog(dto.tenantCode, dto.username, 'fail', '账号或密码错误', ip, userAgent);
|
await this.recordLoginLog(tenantCode, username, 'fail', '账号或密码错误', ip, userAgent);
|
||||||
throw new UnauthorizedException('账号或密码错误');
|
throw new UnauthorizedException('账号或密码错误');
|
||||||
}
|
}
|
||||||
if (user.status !== 'enabled') {
|
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('账号已停用或锁定');
|
throw new UnauthorizedException('账号已停用或锁定');
|
||||||
}
|
}
|
||||||
const ok = await bcrypt.compare(dto.password, user.passwordHash);
|
const ok = await bcrypt.compare(password, user.passwordHash);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
await this.recordLoginLog(dto.tenantCode, dto.username, 'fail', '账号或密码错误', ip, userAgent);
|
await this.recordLoginLog(tenantCode, username, 'fail', '账号或密码错误', ip, userAgent);
|
||||||
throw new UnauthorizedException('账号或密码错误');
|
throw new UnauthorizedException('账号或密码错误');
|
||||||
}
|
}
|
||||||
|
await this.writePermissionCache(tenantCode, user.id);
|
||||||
await this.writePermissionCache(dto.tenantCode, user.id);
|
await this.recordLoginLog(tenantCode, username, 'success', null, ip, userAgent);
|
||||||
await this.recordLoginLog(dto.tenantCode, dto.username, 'success', null, ip, userAgent);
|
|
||||||
await this.recordAudit({
|
await this.recordAudit({
|
||||||
tenantCode: dto.tenantCode,
|
tenantCode,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
module: 'auth',
|
module: 'auth',
|
||||||
@ -115,7 +152,7 @@ export class AuthService {
|
|||||||
ip: ip ?? null,
|
ip: ip ?? null,
|
||||||
userAgent: userAgent ?? 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';
|
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class LoginDto {
|
export class LoginDto {
|
||||||
@ApiProperty({ description: '租户编码' })
|
@ApiPropertyOptional({ description: '租户编码(多租户二选一时传入;缺省按用户名自动探测)' })
|
||||||
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
tenantCode?: string;
|
||||||
tenantCode!: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '用户名' })
|
@ApiProperty({ description: '用户名' })
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user