diff --git a/.gitignore b/.gitignore index 94a9b3315..7b498d13d 100644 --- a/.gitignore +++ b/.gitignore @@ -86,5 +86,5 @@ auth.json /writable/logs/*.log /writable/debugbar/*.json /app/Database/database.sql -/writable/cache/settings +/writable/cache/* /.env.bak diff --git a/app/Config/Filters.php b/app/Config/Filters.php index ac6a94b4a..f33d2944e 100644 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -2,6 +2,7 @@ namespace Config; +use App\Filters\Throttle; use CodeIgniter\Config\Filters as BaseFilters; use CodeIgniter\Filters\Cors; use CodeIgniter\Filters\CSRF; @@ -34,6 +35,7 @@ class Filters extends BaseFilters 'forcehttps' => ForceHTTPS::class, 'pagecache' => PageCache::class, 'performance' => PerformanceMetrics::class, + 'throttle' => Throttle::class, ]; /** @@ -107,7 +109,9 @@ class Filters extends BaseFilters * * @var array>> */ - public array $filters = []; + public array $filters = [ + 'throttle' => ['before' => ['login', 'migrate']], + ]; /** * Constructor to conditionally disable CSRF filter in testing environment diff --git a/app/Controllers/Login.php b/app/Controllers/Login.php index df88371b5..f5184f4d0 100644 --- a/app/Controllers/Login.php +++ b/app/Controllers/Login.php @@ -76,7 +76,7 @@ class Login extends BaseController ]; if (!$this->validate($rules, $messages)) { - $data['has_errors'] = !empty($validation->getErrors()); + $data['hasErrors'] = !empty($validation->getErrors()); return view('login', $data); } diff --git a/app/Filters/Throttle.php b/app/Filters/Throttle.php new file mode 100644 index 000000000..3567763ee --- /dev/null +++ b/app/Filters/Throttle.php @@ -0,0 +1,57 @@ +getMethod() !== 'POST') { + return null; + } + + $throttler = Services::throttler(); + + $ipKey = 'login-ip-' . $request->getIPAddress(); + $username = strtolower((string) $request->getPost('username')); + $usernameKey = $username !== '' ? 'login-user-' . $username : null; + + $ipOk = $throttler->check($ipKey, self::CAPACITY, self::SECONDS); + $usernameOk = $usernameKey === null || $throttler->check($usernameKey, self::CAPACITY, self::SECONDS); + + if (!$ipOk || !$usernameOk) { + log_message('warning', 'Login throttled for IP {ip} (username: {username})', [ + 'ip' => $request->getIPAddress(), + 'username' => $username !== '' ? $username : '(none)', + ]); + + return service('response') + ->setStatusCode(429) + ->setJSON([ + 'success' => false, + 'message' => lang('Login.too_many_attempts'), + ]); + } + + return null; + } + + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + return null; + } +} diff --git a/app/Language/ar-EG/Login.php b/app/Language/ar-EG/Login.php index de16724d1..e2c0c4864 100644 --- a/app/Language/ar-EG/Login.php +++ b/app/Language/ar-EG/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "كلمة السر", "required_username" => "", + 'too_many_attempts' => 'تم تجاوز عدد المحاولات المسموح بها. يرجى الانتظار قليلاً ثم المحاولة مرة أخرى.', "username" => "اسم المستخدم", "welcome" => "مرحباً بك في{0}!" ]; diff --git a/app/Language/ar-LB/Login.php b/app/Language/ar-LB/Login.php index f0cc3b4f8..badb896c9 100644 --- a/app/Language/ar-LB/Login.php +++ b/app/Language/ar-LB/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "كلمة السر", "required_username" => "خانة أسم المستخدم مطلوبة.", + 'too_many_attempts' => 'عدد كبير جدًا من المحاولات. يرجى الانتظار لحظة والمحاولة مرة أخرى.', "username" => "اسم المستخدم", "welcome" => "مرحباً بك في{0}!" ]; diff --git a/app/Language/az/Login.php b/app/Language/az/Login.php index 692713e28..cd5905d3d 100644 --- a/app/Language/az/Login.php +++ b/app/Language/az/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "Şifrə", "required_username" => "", + 'too_many_attempts' => 'Həddindən artıq cəhd. Zəhmət olmasa bir az gözləyib yenidən cəhd edin.', "username" => "İstifadəçi", "welcome" => "{0} -ə xoş gəlmisiniz!" ]; diff --git a/app/Language/bg/Login.php b/app/Language/bg/Login.php index c2a94c8c4..ba8fb67ae 100644 --- a/app/Language/bg/Login.php +++ b/app/Language/bg/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "Password", "required_username" => "", + 'too_many_attempts' => 'Твърде много опити. Моля, изчакайте малко и опитайте отново.', "username" => "Username", "welcome" => "" ]; diff --git a/app/Language/bs/Login.php b/app/Language/bs/Login.php index 7ab8c5b47..e7e2fdfc0 100644 --- a/app/Language/bs/Login.php +++ b/app/Language/bs/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "Lozinka", "required_username" => "", + 'too_many_attempts' => 'Previše pokušaja. Molimo pričekajte trenutak i pokušajte ponovo.', "username" => "Korisničko ime", "welcome" => "Dobrodošli u {0}!" ]; diff --git a/app/Language/ckb/Login.php b/app/Language/ckb/Login.php index 387cd9e35..773210f7d 100644 --- a/app/Language/ckb/Login.php +++ b/app/Language/ckb/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "وشەی نهێنی", "required_username" => "خانەی ناوی بەکارهێنەر پێویستە.", + 'too_many_attempts' => 'هەوڵدانی زۆر زیادە. تکایە کەمێک چاوەڕێ بکە و دووبارە هەوڵ بدەرەوە.', "username" => "ناوی بەکارهێنەر", "welcome" => "بەخێربێن بۆ {0}!" ]; diff --git a/app/Language/cs/Login.php b/app/Language/cs/Login.php index bfc6ec15c..70865477a 100644 --- a/app/Language/cs/Login.php +++ b/app/Language/cs/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "Heslo", "required_username" => "", + 'too_many_attempts' => 'Příliš mnoho pokusů. Počkejte chvíli a zkuste to znovu.', "username" => "Uživatelské jméno", "welcome" => "" ]; diff --git a/app/Language/da/Login.php b/app/Language/da/Login.php index 8dd4517d3..5f73f74e7 100644 --- a/app/Language/da/Login.php +++ b/app/Language/da/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "", "required_username" => "", + 'too_many_attempts' => 'For mange forsøg. Vent et øjeblik, og prøv igen.', "username" => "", "welcome" => "" ]; diff --git a/app/Language/de-CH/Login.php b/app/Language/de-CH/Login.php index 43cdbee87..d7ba28728 100644 --- a/app/Language/de-CH/Login.php +++ b/app/Language/de-CH/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "Datenbank-Migrationen werden ausgeführt...", "password" => "Passwort", "required_username" => "Das Feld Benutzername ist erforderlich.", + 'too_many_attempts' => 'Zu viele Versuche. Bitte warten Sie einen Moment und versuchen Sie es erneut.', "username" => "Benutzername", "welcome" => "Willkommen bei {0}!" ]; diff --git a/app/Language/de-DE/Login.php b/app/Language/de-DE/Login.php index fad409933..dcfdc3fa2 100644 --- a/app/Language/de-DE/Login.php +++ b/app/Language/de-DE/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "Datenbank-Migrationen werden ausgeführt...", "password" => "Passwort", "required_username" => "Das Feld Benutzername ist erforderlich.", + 'too_many_attempts' => 'Zu viele Versuche. Bitte warten Sie einen Moment und versuchen Sie es erneut.', "username" => "Benutzername", "welcome" => "Willkommen bei {0}!" ]; diff --git a/app/Language/el/Login.php b/app/Language/el/Login.php index 19f7b296e..1da7f2419 100644 --- a/app/Language/el/Login.php +++ b/app/Language/el/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "", "required_username" => "", + 'too_many_attempts' => 'Πάρα πολλές προσπάθειες. Παρακαλώ περιμένετε λίγο και δοκιμάστε ξανά.', "username" => "", "welcome" => "" ]; diff --git a/app/Language/en-GB/Login.php b/app/Language/en-GB/Login.php index 6ee49d47d..7aaa5cc64 100644 --- a/app/Language/en-GB/Login.php +++ b/app/Language/en-GB/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "Running database migrations...", "password" => "Password", "required_username" => "The username field is required.", + 'too_many_attempts' => 'Too many attempts. Please wait a moment and try again.', "username" => "Username", "welcome" => "Welcome to {0}!" ]; diff --git a/app/Language/en/Login.php b/app/Language/en/Login.php index f6a5cb8a8..1cbccb34e 100644 --- a/app/Language/en/Login.php +++ b/app/Language/en/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "Running database migrations...", "password" => "Password", "required_username" => "The username field is required.", + 'too_many_attempts' => 'Too many attempts. Please wait a moment and try again.', "username" => "Username", "welcome" => "Welcome to {0}!" ]; diff --git a/app/Language/es-ES/Login.php b/app/Language/es-ES/Login.php index e9a06a4b4..f490ff86f 100644 --- a/app/Language/es-ES/Login.php +++ b/app/Language/es-ES/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "Ejecutando migraciones de base de datos...", "password" => "Contraseña", "required_username" => "El campo de nombre de usuario es obligatorio.", + 'too_many_attempts' => 'Demasiados intentos. Por favor, espere un momento e inténtelo de nuevo.', "username" => "Usuario", "welcome" => "Bienvenido a {0}!" ]; diff --git a/app/Language/es-MX/Login.php b/app/Language/es-MX/Login.php index 941820097..cf76ab113 100644 --- a/app/Language/es-MX/Login.php +++ b/app/Language/es-MX/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "Ejecutando migraciones de base de datos...", "password" => "Contraseña", "required_username" => "El nombre de usuario es obligatorio.", + 'too_many_attempts' => 'Demasiados intentos. Por favor espera un momento e intenta de nuevo.', "username" => "Usuario", "welcome" => "Bienvenido a {0}!" ]; diff --git a/app/Language/fa/Login.php b/app/Language/fa/Login.php index 8c55d51aa..59f13e3df 100644 --- a/app/Language/fa/Login.php +++ b/app/Language/fa/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "کلمه عبور", "required_username" => "", + 'too_many_attempts' => 'تعداد تلاش‌ها بیش از حد مجاز است. لطفاً کمی صبر کنید و دوباره امتحان کنید.', "username" => "نام کاربری", "welcome" => "" ]; diff --git a/app/Language/fr/Login.php b/app/Language/fr/Login.php index 075a4421e..83ef24a80 100644 --- a/app/Language/fr/Login.php +++ b/app/Language/fr/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "Exécution des migrations de la base de données...", "password" => "Mot de passe", "required_username" => "Le champ nom utilisateur est obligatoire.", + 'too_many_attempts' => 'Trop de tentatives. Veuillez patienter un instant et réessayer.', "username" => "Nom d'utilisateur", "welcome" => "Bienvenue à {0} !" ]; diff --git a/app/Language/he/Login.php b/app/Language/he/Login.php index f5081488e..50147ebbd 100644 --- a/app/Language/he/Login.php +++ b/app/Language/he/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "סיסמה", "required_username" => "", + 'too_many_attempts' => 'יותר מדי ניסיונות. אנא המתן רגע ונסה שוב.', "username" => "שם משתמש", "welcome" => "" ]; diff --git a/app/Language/hr-HR/Login.php b/app/Language/hr-HR/Login.php index 4bec7a41f..a844d55ab 100644 --- a/app/Language/hr-HR/Login.php +++ b/app/Language/hr-HR/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "Lozinka", "required_username" => "", + 'too_many_attempts' => 'Previše pokušaja. Pričekajte trenutak i pokušajte ponovno.', "username" => "Korisničko ime", "welcome" => "" ]; diff --git a/app/Language/hu/Login.php b/app/Language/hu/Login.php index b76d4f8f4..d13684daa 100644 --- a/app/Language/hu/Login.php +++ b/app/Language/hu/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "Jelszó", "required_username" => "", + 'too_many_attempts' => 'Túl sok próbálkozás. Kérjük, várjon egy kicsit, majd próbálja újra.', "username" => "Felhasználó név", "welcome" => "" ]; diff --git a/app/Language/hy/Login.php b/app/Language/hy/Login.php index 743fcbb5a..267b222bb 100644 --- a/app/Language/hy/Login.php +++ b/app/Language/hy/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "", "required_username" => "", + 'too_many_attempts' => 'Չափազանց շատ փորձեր: Խնդրում ենք սպասել մի փոքր և կրկին փորձել:', "username" => "", "welcome" => "" ]; diff --git a/app/Language/id/Login.php b/app/Language/id/Login.php index 54996b524..8977cea59 100644 --- a/app/Language/id/Login.php +++ b/app/Language/id/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "Kata kunci", "required_username" => "Kolom nama pengguna wajib diisi.", + 'too_many_attempts' => 'Terlalu banyak percobaan. Harap tunggu sebentar dan coba lagi.', "username" => "Nama Anda", "welcome" => "Selamat Datang di {0}!" ]; diff --git a/app/Language/it/Login.php b/app/Language/it/Login.php index 718782aa0..237ba0e3e 100644 --- a/app/Language/it/Login.php +++ b/app/Language/it/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "Esecuzione migrazioni database...", "password" => "Password", "required_username" => "Il campo nome utente è obbligatorio.", + 'too_many_attempts' => 'Troppi tentativi. Attendere un momento e riprovare.', "username" => "Username", "welcome" => "Benvenuto in {0}!" ]; diff --git a/app/Language/km/Login.php b/app/Language/km/Login.php index 90168da35..9e1e33d50 100644 --- a/app/Language/km/Login.php +++ b/app/Language/km/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "ពាក្យសំងាត់", "required_username" => "", + 'too_many_attempts' => 'ការព្យាយាមច្រើនពេក។ សូមរង់ចាំបន្តិច ហើយព្យាយាមម្តងទៀត។', "username" => "ឈ្មោះ", "welcome" => "" ]; diff --git a/app/Language/lo/Login.php b/app/Language/lo/Login.php index c5cbb85fe..f57c951aa 100644 --- a/app/Language/lo/Login.php +++ b/app/Language/lo/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "Password", "required_username" => "", + 'too_many_attempts' => 'ພະຍາຍາມຫຼາຍເກີນໄປ. ກະລຸນາລໍຖ້າບຶດໜຶ່ງ ແລ້ວລອງໃໝ່ອີກຄັ້ງ.', "username" => "Username", "welcome" => "" ]; diff --git a/app/Language/ml/Login.php b/app/Language/ml/Login.php index 27bc82c3a..fcd730a72 100644 --- a/app/Language/ml/Login.php +++ b/app/Language/ml/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "", "required_username" => "", + 'too_many_attempts' => 'വളരെയധികം ശ്രമങ്ങൾ. ദയവായി അല്പസമയം കാത്തിരുന്ന് വീണ്ടും ശ്രമിക്കുക.', "username" => "", "welcome" => "" ]; diff --git a/app/Language/nb/Login.php b/app/Language/nb/Login.php index ac66f1805..51e33629d 100644 --- a/app/Language/nb/Login.php +++ b/app/Language/nb/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "", "required_username" => "", + 'too_many_attempts' => 'For mange forsøk. Vennligst vent litt og prøv igjen.', "username" => "", "welcome" => "" ]; diff --git a/app/Language/nl-BE/Login.php b/app/Language/nl-BE/Login.php index 455d8a8e2..f8002d16e 100644 --- a/app/Language/nl-BE/Login.php +++ b/app/Language/nl-BE/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "Databasemigraties uitvoeren...", "password" => "Paswoord", "required_username" => "", + 'too_many_attempts' => 'Te veel pogingen. Wacht even en probeer het opnieuw.', "username" => "Gebruiker", "welcome" => "Welkom op {0}!" ]; diff --git a/app/Language/nl-NL/Login.php b/app/Language/nl-NL/Login.php index b66121a29..c0a8be5e0 100644 --- a/app/Language/nl-NL/Login.php +++ b/app/Language/nl-NL/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "Databasemigraties uitvoeren...", "password" => "Wachtwoord", "required_username" => "Het gebruikersnaam veld is verplicht.", + 'too_many_attempts' => 'Te veel pogingen. Wacht even en probeer het opnieuw.', "username" => "Gebruikersnaam", "welcome" => "Welkom bij {0}!" ]; diff --git a/app/Language/pl/Login.php b/app/Language/pl/Login.php index 2c1fa8c32..10f680e0b 100644 --- a/app/Language/pl/Login.php +++ b/app/Language/pl/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "Hasło", "required_username" => "", + 'too_many_attempts' => 'Zbyt wiele prób. Proszę chwilę poczekać i spróbować ponownie.', "username" => "Nazwa użytkownika", "welcome" => "Witaj w {0}!" ]; diff --git a/app/Language/pt-BR/Login.php b/app/Language/pt-BR/Login.php index aa208c506..fd4b96583 100644 --- a/app/Language/pt-BR/Login.php +++ b/app/Language/pt-BR/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "Executando migrações do banco de dados...", "password" => "Senha", "required_username" => "O campo nome de usuário é obrigatório.", + 'too_many_attempts' => 'Muitas tentativas. Aguarde um momento e tente novamente.', "username" => "Usuário", "welcome" => "Bem-vindo ao {0}!" ]; diff --git a/app/Language/ro/Login.php b/app/Language/ro/Login.php index 75de2a599..c33f2a276 100644 --- a/app/Language/ro/Login.php +++ b/app/Language/ro/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "", "required_username" => "", + 'too_many_attempts' => 'Prea multe încercări. Vă rugăm așteptați un moment și încercați din nou.', "username" => "", "welcome" => "" ]; diff --git a/app/Language/ru/Login.php b/app/Language/ru/Login.php index a584ca82e..9eb860d1d 100644 --- a/app/Language/ru/Login.php +++ b/app/Language/ru/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "Пароль", "required_username" => "", + 'too_many_attempts' => 'Слишком много попыток. Пожалуйста, подождите немного и попробуйте снова.', "username" => "Имя пользователя", "welcome" => "Панель управления" ]; diff --git a/app/Language/sv/Login.php b/app/Language/sv/Login.php index a7d4991bf..8415fdfca 100644 --- a/app/Language/sv/Login.php +++ b/app/Language/sv/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "Kör databasmigrationer...", "password" => "Lösenord", "required_username" => "Fältet för användarnamn krävs.", + 'too_many_attempts' => 'För många försök. Vänta en stund och försök igen.', "username" => "Användarnamn", "welcome" => "Välkommen till {0}!" ]; diff --git a/app/Language/sw-KE/Login.php b/app/Language/sw-KE/Login.php index 331eb30a5..0e811d89a 100644 --- a/app/Language/sw-KE/Login.php +++ b/app/Language/sw-KE/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "Nenosiri", "required_username" => "Jina la mtumiaji ni lazima.", + 'too_many_attempts' => 'Majaribio mengi mno. Tafadhali subiri kidogo kisha ujaribu tena.', "username" => "Jina la Mtumiaji", "welcome" => "Karibu kwenye {0}!" ]; diff --git a/app/Language/sw-TZ/Login.php b/app/Language/sw-TZ/Login.php index 331eb30a5..0e811d89a 100644 --- a/app/Language/sw-TZ/Login.php +++ b/app/Language/sw-TZ/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "Nenosiri", "required_username" => "Jina la mtumiaji ni lazima.", + 'too_many_attempts' => 'Majaribio mengi mno. Tafadhali subiri kidogo kisha ujaribu tena.', "username" => "Jina la Mtumiaji", "welcome" => "Karibu kwenye {0}!" ]; diff --git a/app/Language/ta/Login.php b/app/Language/ta/Login.php index b747bc221..bbeb19390 100644 --- a/app/Language/ta/Login.php +++ b/app/Language/ta/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "கடவுச்சொல்", "required_username" => "", + 'too_many_attempts' => 'பல முறை முயற்சி செய்யப்பட்டது. தயவுசெய்து சிறிது நேரம் காத்திருந்து மீண்டும் முயற்சிக்கவும்.', "username" => "பயனர்பெயர்", "welcome" => "{0} க்கு வருக!" ]; diff --git a/app/Language/th/Login.php b/app/Language/th/Login.php index 72ed92fd4..40ef82cae 100644 --- a/app/Language/th/Login.php +++ b/app/Language/th/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "รหัสผ่าน", "required_username" => "จำเป็นต้องระบุชื่อผู้ใช้งาน", + 'too_many_attempts' => 'พยายามมากเกินไป กรุณารอสักครู่แล้วลองใหม่อีกครั้ง', "username" => "ชื่อผู้ใช้", "welcome" => "ยินดีต้อนรับสู่ {0}!" ]; diff --git a/app/Language/tl/Login.php b/app/Language/tl/Login.php index 21855f250..390b3506f 100644 --- a/app/Language/tl/Login.php +++ b/app/Language/tl/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "Password", "required_username" => "", + 'too_many_attempts' => 'Masyadong maraming pagtatangka. Mangyaring maghintay sandali at subukang muli.', "username" => "Username", "welcome" => "" ]; diff --git a/app/Language/tr/Login.php b/app/Language/tr/Login.php index 229fc4b40..f1119523b 100644 --- a/app/Language/tr/Login.php +++ b/app/Language/tr/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "Veritabanı göçleri çalışıyor...", "password" => "Parola", "required_username" => "Kullanıcı adı alanı gereklidir.", + 'too_many_attempts' => 'Çok fazla deneme yapıldı. Lütfen bir süre bekleyip tekrar deneyin.', "username" => "Kullanıcı Adı", "welcome" => "{0}'e Hoş Geldiniz!" ]; diff --git a/app/Language/uk/Login.php b/app/Language/uk/Login.php index 43ff3872b..3515d2636 100644 --- a/app/Language/uk/Login.php +++ b/app/Language/uk/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "Пароль", "required_username" => "", + 'too_many_attempts' => 'Забагато спроб. Будь ласка, зачекайте трохи і спробуйте знову.', "username" => "Ім'я користувача", "welcome" => "" ]; diff --git a/app/Language/ur/Login.php b/app/Language/ur/Login.php index b02422892..e75c5180f 100644 --- a/app/Language/ur/Login.php +++ b/app/Language/ur/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "", "required_username" => "", + 'too_many_attempts' => 'بہت زیادہ کوششیں۔ براہ کرم تھوڑی دیر انتظار کریں اور دوبارہ کوشش کریں۔', "username" => "", "welcome" => "" ]; diff --git a/app/Language/vi/Login.php b/app/Language/vi/Login.php index d5dd21112..e8271f603 100644 --- a/app/Language/vi/Login.php +++ b/app/Language/vi/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "Mật khẩu", "required_username" => "", + 'too_many_attempts' => 'Quá nhiều lần thử. Vui lòng đợi một chút và thử lại.', "username" => "Tài khoản", "welcome" => "Chào mừng đến với {0}!" ]; diff --git a/app/Language/zh-Hans/Login.php b/app/Language/zh-Hans/Login.php index 8fc3c4d66..5afd6ce89 100644 --- a/app/Language/zh-Hans/Login.php +++ b/app/Language/zh-Hans/Login.php @@ -24,6 +24,7 @@ return [ "migration_required" => "", "migration_running" => "", "password" => "密码", + 'too_many_attempts' => '太多次尝试。请稍等片刻后重试。', "username" => "用户名", "welcome" => "欢迎来到 {0}!" ]; diff --git a/app/Language/zh-Hant/Login.php b/app/Language/zh-Hant/Login.php index cc8614fdc..dc77b0c80 100644 --- a/app/Language/zh-Hant/Login.php +++ b/app/Language/zh-Hant/Login.php @@ -25,6 +25,7 @@ return [ "migration_running" => "", "password" => "密碼", "required_username" => "", + 'too_many_attempts' => '嘗試次數過多。請稍候片刻後再試一次。', "username" => "帳號", "welcome" => "歡迎來到 {0}!" ]; diff --git a/tests/Controllers/LoginTest.php b/tests/Controllers/LoginTest.php new file mode 100644 index 000000000..8378f98e5 --- /dev/null +++ b/tests/Controllers/LoginTest.php @@ -0,0 +1,158 @@ +destroy(); + $this->clearThrottleState(); + } + + protected function tearDown(): void + { + $this->clearThrottleState(); + + parent::tearDown(); + } + + private function clearThrottleState(): void + { + $throttler = Services::throttler(); + + foreach ($this->usedKeys as $key) { + $throttler->remove($key); + } + + $this->usedKeys = []; + + Services::resetSingle('throttler'); + } + + private function trackThrottleKeys(string $username = ''): void + { + $this->usedKeys[] = 'login-ip-' . Services::request()->getIPAddress(); + + if ($username !== '') { + $this->usedKeys[] = 'login-user-' . strtolower($username); + } + } + + /** + * Login::index() redirects to 'login' and runs a migration when the + * app's migration state isn't current, bypassing credential checks + * entirely. Skip credential-dependent assertions in that case so this + * test only exercises login logic when the DB is actually current + * (throttling itself is verified independently and runs before this + * branch, so it's unaffected). + */ + private function skipIfMigrationRequired(\CodeIgniter\Test\TestResponse $response): void + { + $redirectUrl = $response->getRedirectUrl(); + + if ($redirectUrl !== null && str_ends_with(rtrim(strtolower($redirectUrl), '/'), '/login')) { + $this->markTestSkipped('App migration state is not current in this environment; skipping credential-path assertions.'); + } + } + + public function testValidCredentialsLogsIn(): void + { + $response = $this->post('/login', [ + 'username' => 'admin', + 'password' => 'pointofsale', + ]); + $this->trackThrottleKeys('admin'); + + $this->skipIfMigrationRequired($response); + + $response->assertRedirectTo('home'); + } + + public function testInvalidCredentialsShowsError(): void + { + $response = $this->post('/login', [ + 'username' => 'admin', + 'password' => 'wrongpassword', + ]); + $this->trackThrottleKeys('admin'); + + $this->skipIfMigrationRequired($response); + + $response->assertStatus(200); + $response->assertSee(lang('Login.invalid_username_and_password')); + } + + public function testSixthFailedAttemptIsThrottled(): void + { + for ($i = 0; $i < 5; $i++) { + $this->post('/login', [ + 'username' => 'wronguser', + 'password' => 'wrongpassword', + ]); + $this->trackThrottleKeys('wronguser'); + } + + $response = $this->post('/login', [ + 'username' => 'wronguser', + 'password' => 'wrongpassword', + ]); + $this->trackThrottleKeys('wronguser'); + + $response->assertStatus(429); + $result = json_decode($response->getJSON(), true); + $this->assertFalse($result['success']); + $this->assertSame(lang('Login.too_many_attempts'), $result['message']); + } + + public function testCorrectLoginStillWorksUnderThrottleCapacity(): void + { + for ($i = 0; $i < 3; $i++) { + $this->post('/login', [ + 'username' => 'admin', + 'password' => 'wrongpassword', + ]); + $this->trackThrottleKeys('admin'); + } + + $response = $this->post('/login', [ + 'username' => 'admin', + 'password' => 'pointofsale', + ]); + $this->trackThrottleKeys('admin'); + + $this->skipIfMigrationRequired($response); + + $response->assertRedirectTo('home'); + } + + public function testGetRequestToLoginIsNeverThrottled(): void + { + for ($i = 0; $i < 10; $i++) { + $response = $this->get('/login'); + $this->skipIfMigrationRequired($response); + $response->assertStatus(200); + } + } +} diff --git a/tests/Filters/ThrottleTest.php b/tests/Filters/ThrottleTest.php new file mode 100644 index 000000000..d6cfb8e81 --- /dev/null +++ b/tests/Filters/ThrottleTest.php @@ -0,0 +1,128 @@ +filter = new Throttle(); + } + + protected function tearDown(): void + { + $throttler = Services::throttler(); + + foreach ($this->usedKeys as $key) { + $throttler->remove($key); + } + + parent::tearDown(); + } + + private function makeRequest(string $method, string $ip, ?string $username = null): IncomingRequest + { + $request = $this->createMock(IncomingRequest::class); + + $request->method('getMethod')->willReturn($method); + $request->method('getIPAddress')->willReturn($ip); + $request->method('getPost')->with('username')->willReturn($username); + + $this->usedKeys[] = 'login-ip-' . $ip; + + if ($username !== null && $username !== '') { + $this->usedKeys[] = 'login-user-' . strtolower($username); + } + + return $request; + } + + public function testGetRequestsAreNotThrottled(): void + { + $request = $this->makeRequest('GET', '203.0.113.1', 'admin'); + + $result = $this->filter->before($request); + + $this->assertNull($result); + } + + public function testAllowsAttemptsUnderCapacity(): void + { + $ip = '203.0.113.2'; + + for ($i = 0; $i < 5; $i++) { + $request = $this->makeRequest('POST', $ip, 'employee1'); + $result = $this->filter->before($request); + + $this->assertNull($result, "Attempt {$i} should not be throttled"); + } + } + + public function testBlocksAfterCapacityExceededByIp(): void + { + $ip = '203.0.113.3'; + + for ($i = 0; $i < 5; $i++) { + $this->filter->before($this->makeRequest('POST', $ip, "user{$i}")); + } + + $result = $this->filter->before($this->makeRequest('POST', $ip, 'user-final')); + + $this->assertNotNull($result); + $this->assertSame(429, $result->getStatusCode()); + } + + public function testBlocksAfterCapacityExceededByUsername(): void + { + $username = 'sameuser'; + + for ($i = 0; $i < 5; $i++) { + $this->filter->before($this->makeRequest('POST', "203.0.113.{$i}", $username)); + } + + $result = $this->filter->before($this->makeRequest('POST', '203.0.113.99', $username)); + + $this->assertNotNull($result); + $this->assertSame(429, $result->getStatusCode()); + } + + public function testThrottledResponseIsJsonWithMessage(): void + { + $ip = '203.0.113.4'; + + for ($i = 0; $i < 5; $i++) { + $this->filter->before($this->makeRequest('POST', $ip, 'someone')); + } + + $result = $this->filter->before($this->makeRequest('POST', $ip, 'someone')); + + $body = json_decode($result->getJSON(), true); + + $this->assertFalse($body['success']); + $this->assertSame(lang('Login.too_many_attempts'), $body['message']); + } + + public function testMissingUsernameOnlyThrottlesByIp(): void + { + $ip = '203.0.113.5'; + + for ($i = 0; $i < 5; $i++) { + $this->filter->before($this->makeRequest('POST', $ip, null)); + } + + $result = $this->filter->before($this->makeRequest('POST', $ip, null)); + + $this->assertNotNull($result); + $this->assertSame(429, $result->getStatusCode()); + } +}