Files
heala-backend/src/modules/admin-panel/providers/admin-panel-auth.provider.ts
T
2026-10-07 16:34:11 +03:30

87 lines
2.3 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import type { CurrentAdmin } from 'adminjs';
import { Role } from '@/common/enums/roles.enum';
import { UsersService } from '@/modules/users/providers/users.service';
import { HashingProvider } from '@/common/modules/hashing/providers/hashing.provider';
import { RedisService } from '@/common/modules/redis/providers/redis.service';
const MAX_FAILED_ATTEMPTS = 5;
const ATTEMPT_WINDOW_SECONDS = 15 * 60;
/**
* Hash of a random throwaway string. Comparing against it when the phone
* number is unknown keeps the response time identical to the success path,
* so attackers cannot enumerate superuser phone numbers via timing.
*/
const DUMMY_BCRYPT_HASH =
'$2b$10$ZFUmcSvMhrye2p.Dt1J7rOcfEdi.NcDLk3Bf55taT3ed57cAcpiNS';
@Injectable()
export class AdminPanelAuthenticator {
constructor(
/**
* Inject Users Service
*/
private readonly usersService: UsersService,
/**
* Inject Hashing Provider
*/
private readonly hashingProvider: HashingProvider,
/**
* Inject Redis Service
*/
private readonly redisService: RedisService,
) {}
public async authenticate(
phone: string,
password: string,
): Promise<CurrentAdmin | null> {
const normalizedPhone = phone?.trim();
if (!normalizedPhone || !password) {
return null;
}
const attemptsKey = `admin:panel:login:${normalizedPhone}`;
const failedAttempts =
(await this.redisService.get<number>(attemptsKey)) ?? 0;
if (failedAttempts >= MAX_FAILED_ATTEMPTS) {
return null;
}
const user = await this.usersService.findOneByPhone(normalizedPhone);
const isSuperuser = !!user && user.role === Role.SUPERUSER;
const passwordMatches = await this.hashingProvider.compare(
password,
isSuperuser && user.password ? user.password : DUMMY_BCRYPT_HASH,
);
if (!isSuperuser || !user.password || !passwordMatches) {
await this.redisService.set(
attemptsKey,
failedAttempts + 1,
ATTEMPT_WINDOW_SECONDS,
);
return null;
}
await this.redisService.del(attemptsKey);
return {
id: user.id.toString(),
firstName: user.firstName,
lastName: user.lastName,
phone: user.phone,
role: user.role,
// AdminJS requires an email on CurrentAdmin for the UI
email: user.phone,
};
}
}