feat: add admin js
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user