add cooldown notification repeat
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { BadRequestException, HttpException, HttpStatus, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
@@ -214,6 +214,30 @@ export class AuthService {
|
||||
async sendOtp(recipient: string, channel?: string, ipAddress?: string) {
|
||||
const existingUser = await this.findUserByRecipient(recipient);
|
||||
const target = this.normalizeOtpTarget(this.resolveOtpTarget(recipient, channel, existingUser));
|
||||
const cooldownSeconds = await this.settings.getNumber('OTP_RESEND_COOLDOWN_SECONDS', 60);
|
||||
const resendKey = this.otpResendKey(target);
|
||||
|
||||
const ttl = await this.redis.client.ttl(resendKey);
|
||||
if (ttl > 0) {
|
||||
throw new HttpException(
|
||||
{
|
||||
message: `Запросить новый код можно через ${this.formatOtpCooldown(ttl)}`,
|
||||
code: 'OTP_RESEND_COOLDOWN',
|
||||
resendAvailableAt: new Date(Date.now() + ttl * 1000).toISOString()
|
||||
},
|
||||
HttpStatus.TOO_MANY_REQUESTS
|
||||
);
|
||||
}
|
||||
|
||||
await this.prisma.authCode.updateMany({
|
||||
where: {
|
||||
target,
|
||||
purpose: 'passwordless-login',
|
||||
usedAt: null
|
||||
},
|
||||
data: { usedAt: new Date() }
|
||||
});
|
||||
|
||||
const deliveryChannel = target.includes('@') ? 'email' : 'sms';
|
||||
const expiryMinutes = await this.settings.getNumber('OTP_EXPIRY_MINUTES', 10);
|
||||
const code = String(randomInt(100000, 999999));
|
||||
@@ -238,7 +262,9 @@ export class AuthService {
|
||||
await this.publishOtpPushNotification(existingUser.id, code, ipAddress, 'Вход в аккаунт');
|
||||
}
|
||||
await this.redis.client.del(this.otpAttemptKey(target));
|
||||
return { sent: true, expiresAt: expiresAt.toISOString(), maskedTarget: this.maskTarget(target) };
|
||||
await this.redis.client.set(resendKey, '1', 'EX', cooldownSeconds);
|
||||
const resendAvailableAt = new Date(Date.now() + cooldownSeconds * 1000).toISOString();
|
||||
return { sent: true, expiresAt: expiresAt.toISOString(), maskedTarget: this.maskTarget(target), resendAvailableAt };
|
||||
}
|
||||
|
||||
private async publishOtpPushNotification(userId: string, code: string, ipAddress: string | undefined, action: string) {
|
||||
@@ -329,6 +355,22 @@ export class AuthService {
|
||||
return `otp:attempts:passwordless-login:${target.toLowerCase()}`;
|
||||
}
|
||||
|
||||
private otpResendKey(target: string) {
|
||||
return `otp:resend:passwordless-login:${target.toLowerCase()}`;
|
||||
}
|
||||
|
||||
private formatOtpCooldown(seconds: number) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const rest = seconds % 60;
|
||||
if (minutes > 0 && rest > 0) {
|
||||
return `${minutes}:${String(rest).padStart(2, '0')}`;
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return `${minutes} мин`;
|
||||
}
|
||||
return `${rest} сек`;
|
||||
}
|
||||
|
||||
private maskTarget(value: string) {
|
||||
if (value.includes('@')) {
|
||||
const [name, domain] = value.split('@');
|
||||
|
||||
@@ -17,6 +17,7 @@ export const DEFAULT_SYSTEM_SETTINGS = [
|
||||
{ key: 'PIN_MAX_LENGTH', value: '6', description: 'Максимальная длина PIN-кода' },
|
||||
{ key: 'MAX_FAMILY_MEMBERS', value: '6', description: 'Максимальное количество участников в семейной группе' },
|
||||
{ key: 'OTP_EXPIRY_MINUTES', value: '10', description: 'Время жизни OTP-кода для входа (минуты)' },
|
||||
{ key: 'OTP_RESEND_COOLDOWN_SECONDS', value: '60', description: 'Минимальный интервал между повторными запросами OTP-кода (секунды)' },
|
||||
{ key: 'OTP_MAX_ATTEMPTS', value: '5', description: 'Максимальное количество попыток ввода OTP' },
|
||||
{
|
||||
key: 'ACCOUNT_DELETE_GRACE_DAYS',
|
||||
|
||||
@@ -15,6 +15,8 @@ function httpToGrpc(code: number): number {
|
||||
return GrpcStatus.NOT_FOUND;
|
||||
case 409:
|
||||
return GrpcStatus.ALREADY_EXISTS;
|
||||
case 429:
|
||||
return GrpcStatus.RESOURCE_EXHAUSTED;
|
||||
default:
|
||||
return GrpcStatus.UNKNOWN;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user