implement account lockout after 3 failed login attempts with 5-minute cooldown period
Some checks failed
Build and Push Docker Images / build-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (push) Has been cancelled

This commit is contained in:
Egor Pozharov
2026-04-29 17:00:37 +06:00
parent 459b60c9aa
commit a3929bec8d
11 changed files with 150 additions and 12 deletions

View File

@@ -76,16 +76,16 @@ async function fetchApi<T>(
});
if (!response.ok) {
const errorData = await response.json().catch(() => null);
// Handle 401 unauthorized
if (response.status === 401) {
if (response.status === 401 && endpoint !== '/api/auth/login') {
handleUnauthorized();
throw new ApiError('Unauthorized', 401);
}
const errorData = await response.json().catch(() => null);
throw new ApiError(
errorData?.error || `HTTP ${response.status}`,
response.status,
errorData?.details
errorData
);
}

View File

@@ -1,5 +1,6 @@
import { create } from 'zustand';
import { authApi } from '../api/auth';
import { ApiError } from '../api/client';
import { useToastStore } from './toastStore';
import type { User, LoginRequest } from '../types';
@@ -16,6 +17,28 @@ interface AuthState {
const TOKEN_KEY = 'auth_token';
const USER_KEY = 'auth_user';
const LOCKED_ACCOUNT_CODE = 'account_temporarily_locked';
function getLoginErrorMessage(error: unknown): string {
if (error instanceof ApiError) {
const details = error.details as { code?: string; locked_until?: string } | null;
if (error.status === 429 && details?.code === LOCKED_ACCOUNT_CODE) {
if (details.locked_until) {
const lockedUntil = new Date(details.locked_until);
const secondsLeft = Math.ceil((lockedUntil.getTime() - Date.now()) / 1000);
if (Number.isFinite(secondsLeft) && secondsLeft > 0) {
const minutesLeft = Math.ceil(secondsLeft / 60);
return `Слишком много неверных попыток. Попробуйте через ${minutesLeft} мин.`;
}
}
return 'Слишком много неверных попыток. Попробуйте через 5 минут';
}
}
return error instanceof Error ? error.message : 'Ошибка входа';
}
export const useAuthStore = create<AuthState>((set) => ({
token: null,
@@ -45,8 +68,7 @@ export const useAuthStore = create<AuthState>((set) => ({
useToastStore.getState().addToast('Вход выполнен успешно', 'success');
} catch (error) {
set({ isLoading: false });
const message = error instanceof Error ? error.message : 'Ошибка входа';
useToastStore.getState().addToast(message, 'error');
useToastStore.getState().addToast(getLoginErrorMessage(error), 'error');
throw error;
}
},