global update and global fix
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user