fix idp on started

This commit is contained in:
lendry
2026-06-24 16:53:06 +03:00
parent d16eccb4c2
commit e60d55f6bd
15 changed files with 709 additions and 52 deletions

View File

@@ -11,6 +11,7 @@ import { normalizeLdapUserFilter, resolveLdapBindIdentity, resolveLdapPort, vali
import { AuthTokens, LoginCommand, PublicUser, RegisterCommand } from './dto';
import { AccessService } from './access.service';
import { SettingsService } from './settings.service';
import { MessagingService } from '../infra/messaging.service';
const FIRST_ADMIN_LOCK = 'locks:first-super-admin';
@@ -23,7 +24,8 @@ export class AuthService {
private readonly config: ConfigService,
private readonly access: AccessService,
private readonly settings: SettingsService,
private readonly ldapClient: LdapClientService
private readonly ldapClient: LdapClientService,
private readonly messaging: MessagingService
) {}
async register(command: RegisterCommand): Promise<PublicUser> {
@@ -131,19 +133,27 @@ export class AuthService {
async sendOtp(recipient: string, channel?: string) {
const existingUser = await this.findUserByRecipient(recipient);
const target = this.resolveOtpTarget(recipient, channel, existingUser);
const deliveryChannel = target.includes('@') ? 'email' : 'sms';
const expiryMinutes = await this.settings.getNumber('OTP_EXPIRY_MINUTES', 10);
const code = String(randomInt(100000, 999999));
const expiresAt = new Date(Date.now() + 5 * 60_000);
const expiresAt = new Date(Date.now() + expiryMinutes * 60_000);
await this.prisma.authCode.create({
data: {
userId: existingUser?.id,
target,
channel: target.includes('@') ? 'email' : 'sms',
channel: deliveryChannel,
purpose: 'passwordless-login',
codeHash: await bcrypt.hash(code, 10),
expiresAt
}
});
console.log('[OTP MOCK] Code for', target, 'is:', code);
await this.messaging.sendVerificationCode({
channel: deliveryChannel as 'email' | 'sms',
target,
code,
expiryMinutes
});
await this.redis.client.del(this.otpAttemptKey(target));
return { sent: true, expiresAt: expiresAt.toISOString(), maskedTarget: this.maskTarget(target) };
}
@@ -160,8 +170,20 @@ export class AuthService {
});
if (!authCode) throw new UnauthorizedException('Код входа не найден или истек');
const attemptKey = this.otpAttemptKey(authCode.target);
const maxAttempts = await this.settings.getNumber('OTP_MAX_ATTEMPTS', 5);
const attempts = Number((await this.redis.client.get(attemptKey)) ?? 0);
if (attempts >= maxAttempts) {
throw new UnauthorizedException('Превышено число попыток ввода кода');
}
const matches = await bcrypt.compare(command.code, authCode.codeHash);
if (!matches) throw new UnauthorizedException('Неверный код входа');
if (!matches) {
await this.redis.client.set(attemptKey, String(attempts + 1), 'EX', 15 * 60);
throw new UnauthorizedException('Неверный код входа');
}
await this.redis.client.del(attemptKey);
await this.prisma.authCode.update({ where: { id: authCode.id }, data: { usedAt: new Date() } });
const user = await this.findOrCreatePasswordlessUser(command.recipient);
@@ -183,6 +205,10 @@ export class AuthService {
return map[channel] ?? recipient;
}
private otpAttemptKey(target: string) {
return `otp:attempts:passwordless-login:${target.toLowerCase()}`;
}
private maskTarget(value: string) {
if (value.includes('@')) {
const [name, domain] = value.split('@');