global update and global fix

This commit is contained in:
lendry
2026-06-25 23:48:57 +03:00
parent 3880c68d59
commit b0ea87e898
121 changed files with 13759 additions and 663 deletions

View File

@@ -17,6 +17,11 @@ import { canonicalPhoneE164, phoneLookupVariants } from '../infra/phone.util';
import { TotpService } from './totp.service';
import { RbacService } from './rbac.service';
import { DEFAULT_VERIFICATION_ICON, resolveVerificationIcon } from './verification.constants';
import {
assertHumanAccount,
assertNotReservedUsername,
HUMAN_USER_WHERE
} from './system-account.util';
const FIRST_ADMIN_LOCK = 'locks:first-super-admin';
const TOTP_LOGIN_CHALLENGE_TTL_SECONDS = 5 * 60;
@@ -44,6 +49,8 @@ export class AuthService {
throw new BadRequestException('Укажите почту или телефон');
}
assertNotReservedUsername(command.username);
const passwordHash = command.password ? await bcrypt.hash(command.password, 12) : null;
const lockAcquired = await this.redis.acquireLock(FIRST_ADMIN_LOCK, 10_000);
@@ -86,7 +93,8 @@ export class AuthService {
where: {
OR: [{ email: command.login }, { phone: command.login }, { username: command.login }],
deletedAt: null,
status: UserStatus.ACTIVE
status: UserStatus.ACTIVE,
...HUMAN_USER_WHERE
},
include: { pinCode: true }
});
@@ -95,6 +103,8 @@ export class AuthService {
throw new UnauthorizedException('Неверный логин или пароль');
}
assertHumanAccount(user, { asAuth: true });
if (!user.passwordHash) {
throw new UnauthorizedException('Для этого аккаунта используйте вход по коду');
}
@@ -115,6 +125,7 @@ export class AuthService {
if (!user || user.status !== UserStatus.ACTIVE) {
throw new UnauthorizedException('Пользователь не найден');
}
assertHumanAccount(user, { asAuth: true });
if (!(await this.totp.isEnabledForUser(user.id))) {
throw new BadRequestException('Для этого аккаунта не настроен аутентификатор');
}
@@ -172,13 +183,15 @@ export class AuthService {
await this.redis.client.del(attemptKey);
const user = await this.prisma.user.findFirst({
where: { id: payload.sub, deletedAt: null, status: UserStatus.ACTIVE },
where: { id: payload.sub, deletedAt: null, status: UserStatus.ACTIVE, ...HUMAN_USER_WHERE },
include: { pinCode: true }
});
if (!user) {
throw new UnauthorizedException('Пользователь не найден или заблокирован');
}
assertHumanAccount(user, { asAuth: true });
const auth = await this.createAuthTokens(user, challenge.deviceId, challenge);
await this.writeSignInEvent(user.id, true, challenge.signIn, challenge.deviceId);
return auth;
@@ -391,6 +404,7 @@ export class AuthService {
? await this.findUserByTempPasswordToken(command.tempAuthToken)
: await this.findUserByRecipient(command.login);
if (!user?.passwordHash || user.status !== UserStatus.ACTIVE) throw new UnauthorizedException('Пользователь не найден или заблокирован');
assertHumanAccount(user, { asAuth: true });
const matches = await bcrypt.compare(command.password, user.passwordHash);
if (!matches) {
await this.writeSignInEvent(user.id, false, command, undefined, 'Неверный пароль');
@@ -402,12 +416,13 @@ export class AuthService {
async createQrApprovedLogin(userId: string, command: Pick<LoginCommand, 'fingerprint' | 'deviceName' | 'deviceType' | 'ipAddress' | 'userAgent'>) {
const user = await this.prisma.user.findFirst({
where: { id: userId, deletedAt: null, status: UserStatus.ACTIVE },
where: { id: userId, deletedAt: null, status: UserStatus.ACTIVE, ...HUMAN_USER_WHERE },
include: { pinCode: true }
});
if (!user) {
throw new UnauthorizedException('Пользователь не найден или заблокирован');
}
assertHumanAccount(user, { asAuth: true });
const device = await this.upsertDevice(user.id, {
fingerprint: command.fingerprint,
deviceName: command.deviceName,
@@ -575,6 +590,8 @@ export class AuthService {
deviceId: string,
options?: { skipTotp?: boolean }
): Promise<AuthTokens> {
assertHumanAccount(user, { asAuth: true });
if (!options?.skipTotp && (await this.totp.isEnabledForUser(user.id))) {
return this.issueTotpChallenge(user, deviceCommand, signInCommand, deviceId);
}
@@ -648,6 +665,8 @@ export class AuthService {
}
private async createAuthTokens(user: User & { pinCode?: { isEnabled: boolean } | null }, deviceId: string, command: Pick<LoginCommand, 'ipAddress' | 'userAgent'>): Promise<AuthTokens> {
assertHumanAccount(user, { asAuth: true });
const pinVerified = !user.pinCode?.isEnabled;
const refreshToken = randomBytes(48).toString('base64url');
const expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30);
@@ -700,14 +719,14 @@ export class AuthService {
private async findUserByRecipient(recipient: string) {
if (recipient.includes('@')) {
return this.prisma.user.findFirst({
where: { email: recipient.trim().toLowerCase(), deletedAt: null, status: UserStatus.ACTIVE },
where: { email: recipient.trim().toLowerCase(), deletedAt: null, status: UserStatus.ACTIVE, ...HUMAN_USER_WHERE },
include: { pinCode: true }
});
}
const variants = phoneLookupVariants(recipient);
return this.prisma.user.findFirst({
where: { phone: { in: variants }, deletedAt: null, status: UserStatus.ACTIVE },
where: { phone: { in: variants }, deletedAt: null, status: UserStatus.ACTIVE, ...HUMAN_USER_WHERE },
include: { pinCode: true }
});
}
@@ -722,6 +741,7 @@ export class AuthService {
if (!user || user.deletedAt || user.status !== UserStatus.ACTIVE) {
throw new UnauthorizedException('Пользователь не найден или заблокирован');
}
assertHumanAccount(user, { asAuth: true });
return user;
}
@@ -763,10 +783,12 @@ export class AuthService {
throw new UnauthorizedException('Пользователь не найден или заблокирован');
}
assertHumanAccount(user, { asAuth: true });
return this.toPublicUser(user);
}
async toPublicUser(user: Pick<User, 'id' | 'email' | 'phone' | 'backupEmail' | 'backupPhone' | 'displayName' | 'username' | 'avatarUrl' | 'avatarStorageKey' | 'isSuperAdmin' | 'isVerified' | 'verificationIcon' | 'status' | 'passwordHash'>): Promise<PublicUser> {
async toPublicUser(user: Pick<User, 'id' | 'email' | 'phone' | 'backupEmail' | 'backupPhone' | 'displayName' | 'username' | 'avatarUrl' | 'avatarStorageKey' | 'isSuperAdmin' | 'isVerified' | 'verificationIcon' | 'status' | 'passwordHash' | 'isSystemAccount' | 'linkedBotId'>): Promise<PublicUser> {
const access = await this.access.getUserAccess(user.id, user.isSuperAdmin);
return {
id: user.id,
@@ -795,11 +817,14 @@ export class AuthService {
canManageSettings: access.canManageSettings,
canViewUsers: access.canViewUsers,
canViewUserDocuments: access.canViewUserDocuments,
canVerifyUsers: access.canVerifyUsers
canVerifyUsers: access.canVerifyUsers,
canManageBots: access.canManageBots
};
}
async issueAccessToken(user: Pick<User, 'id' | 'isSuperAdmin'>, sessionId: string, pinVerified: boolean) {
async issueAccessToken(user: Pick<User, 'id' | 'isSuperAdmin' | 'isSystemAccount' | 'linkedBotId'>, sessionId: string, pinVerified: boolean) {
assertHumanAccount(user, { asAuth: true });
return this.jwt.signAsync(
{
sub: user.id,