fix and update
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { BadRequestException, HttpException, HttpStatus, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { BadRequestException, HttpException, HttpStatus, Injectable, Logger, OnModuleInit, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
@@ -30,7 +30,9 @@ const TOTP_LOGIN_CHALLENGE_TTL_SECONDS = 5 * 60;
|
||||
type DeviceCommand = Pick<LoginCommand, 'fingerprint' | 'deviceName' | 'deviceType' | 'ipAddress' | 'userAgent'>;
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
export class AuthService implements OnModuleInit {
|
||||
private readonly logger = new Logger(AuthService.name);
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly redis: RedisService,
|
||||
@@ -45,6 +47,10 @@ export class AuthService {
|
||||
private readonly rbac: RbacService
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.repairMissingFirstHumanSuperAdmin();
|
||||
}
|
||||
|
||||
async register(command: RegisterCommand): Promise<PublicUser> {
|
||||
if (!command.email && !command.phone) {
|
||||
throw new BadRequestException('Укажите почту или телефон');
|
||||
@@ -62,7 +68,7 @@ export class AuthService {
|
||||
try {
|
||||
const user = await this.prisma.$transaction(
|
||||
async (tx) => {
|
||||
const usersCount = await tx.user.count({ where: { deletedAt: null } });
|
||||
const grantSuperAdmin = await this.shouldGrantFirstHumanSuperAdmin(tx);
|
||||
return tx.user.create({
|
||||
data: {
|
||||
email: command.email,
|
||||
@@ -70,7 +76,7 @@ export class AuthService {
|
||||
passwordHash,
|
||||
displayName: command.displayName,
|
||||
username: command.username,
|
||||
isSuperAdmin: usersCount === 0
|
||||
isSuperAdmin: grantSuperAdmin
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -89,6 +95,79 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
private async shouldGrantFirstHumanSuperAdmin(tx: Prisma.TransactionClient): Promise<boolean> {
|
||||
const [humanUsersCount, superAdminsCount] = await Promise.all([
|
||||
tx.user.count({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
...HUMAN_USER_WHERE
|
||||
}
|
||||
}),
|
||||
tx.user.count({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
isSuperAdmin: true,
|
||||
...HUMAN_USER_WHERE
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
return humanUsersCount === 0 && superAdminsCount === 0;
|
||||
}
|
||||
|
||||
private async repairMissingFirstHumanSuperAdmin(): Promise<void> {
|
||||
let lockAcquired = false;
|
||||
|
||||
try {
|
||||
lockAcquired = await this.redis.acquireLock(FIRST_ADMIN_LOCK, 10_000);
|
||||
if (!lockAcquired) return;
|
||||
|
||||
const promoted = await this.prisma.$transaction(
|
||||
async (tx) => {
|
||||
const superAdminsCount = await tx.user.count({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
isSuperAdmin: true,
|
||||
...HUMAN_USER_WHERE
|
||||
}
|
||||
});
|
||||
|
||||
if (superAdminsCount > 0) return null;
|
||||
|
||||
const firstHumanUser = await tx.user.findFirst({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
...HUMAN_USER_WHERE
|
||||
},
|
||||
orderBy: [{ createdAt: 'asc' }, { id: 'asc' }],
|
||||
select: { id: true }
|
||||
});
|
||||
|
||||
if (!firstHumanUser) return null;
|
||||
|
||||
return tx.user.update({
|
||||
where: { id: firstHumanUser.id },
|
||||
data: { isSuperAdmin: true },
|
||||
select: { id: true }
|
||||
});
|
||||
},
|
||||
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable }
|
||||
);
|
||||
|
||||
if (promoted) {
|
||||
this.logger.warn(`Восстановлены права супер-администратора для первого пользователя: ${promoted.id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Не удалось проверить первого супер-администратора: ${error instanceof Error ? error.message : 'неизвестная ошибка'}`
|
||||
);
|
||||
} finally {
|
||||
if (lockAcquired) {
|
||||
await this.redis.releaseLock(FIRST_ADMIN_LOCK);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async login(command: LoginCommand): Promise<AuthTokens> {
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
@@ -587,14 +666,14 @@ export class AuthService {
|
||||
try {
|
||||
const user = await this.prisma.$transaction(
|
||||
async (tx) => {
|
||||
const usersCount = await tx.user.count({ where: { deletedAt: null } });
|
||||
const grantSuperAdmin = await this.shouldGrantFirstHumanSuperAdmin(tx);
|
||||
return tx.user.create({
|
||||
data: {
|
||||
email: ldapResult.email ?? undefined,
|
||||
username: ldapResult.username,
|
||||
displayName: ldapResult.displayName,
|
||||
passwordHash: null,
|
||||
isSuperAdmin: usersCount === 0,
|
||||
isSuperAdmin: grantSuperAdmin,
|
||||
linkedAccounts: {
|
||||
create: {
|
||||
providerName: 'ldap',
|
||||
@@ -789,14 +868,14 @@ export class AuthService {
|
||||
try {
|
||||
const user = await this.prisma.$transaction(
|
||||
async (tx) => {
|
||||
const usersCount = await tx.user.count({ where: { deletedAt: null } });
|
||||
const grantSuperAdmin = await this.shouldGrantFirstHumanSuperAdmin(tx);
|
||||
return tx.user.create({
|
||||
data: {
|
||||
email: recipient.includes('@') ? recipient : undefined,
|
||||
phone: recipient.includes('@') ? undefined : recipient,
|
||||
passwordHash: null,
|
||||
displayName: recipient,
|
||||
isSuperAdmin: usersCount === 0
|
||||
isSuperAdmin: grantSuperAdmin
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user