mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-09-21 02:07:23 -04:00
Merge remote-tracking branch 'OpensourcePOS/master' into feature-stock-location-dropdown-order
This commit is contained in:
commit
a8219942e2
51 files changed
+395
-3
No files matched your search
+1
-1
@@ -86,5 +86,5 @@ auth.json
|
||||
/writable/logs/*.log
|
||||
/writable/debugbar/*.json
|
||||
/app/Database/database.sql
|
||||
/writable/cache/settings
|
||||
/writable/cache/*
|
||||
/.env.bak
|
||||
@@ -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<string, array<string, list<string>>>
|
||||
*/
|
||||
public array $filters = [];
|
||||
public array $filters = [
|
||||
'throttle' => ['before' => ['login', 'migrate']],
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor to conditionally disable CSRF filter in testing environment
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filters;
|
||||
|
||||
use CodeIgniter\Filters\FilterInterface;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Config\Services;
|
||||
|
||||
/**
|
||||
* Rate limits login/migrate POST attempts, keyed by IP and by submitted
|
||||
* username, to mitigate brute-force and credential-stuffing attacks
|
||||
* (GHSA-hm9c-xchj-xgcp). Backed by CodeIgniter's cache-based Throttler,
|
||||
* so limits are per-server (not shared across nodes on file cache).
|
||||
*/
|
||||
class Throttle implements FilterInterface
|
||||
{
|
||||
private const CAPACITY = 5;
|
||||
private const SECONDS = 60;
|
||||
|
||||
public function before(RequestInterface $request, $arguments = null)
|
||||
{
|
||||
if ($request->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;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ return [
|
||||
"migration_running" => "",
|
||||
"password" => "كلمة السر",
|
||||
"required_username" => "",
|
||||
'too_many_attempts' => 'تم تجاوز عدد المحاولات المسموح بها. يرجى الانتظار قليلاً ثم المحاولة مرة أخرى.',
|
||||
"username" => "اسم المستخدم",
|
||||
"welcome" => "مرحباً بك في{0}!"
|
||||
];
|
||||
@@ -25,6 +25,7 @@ return [
|
||||
"migration_running" => "",
|
||||
"password" => "كلمة السر",
|
||||
"required_username" => "خانة أسم المستخدم مطلوبة.",
|
||||
'too_many_attempts' => 'عدد كبير جدًا من المحاولات. يرجى الانتظار لحظة والمحاولة مرة أخرى.',
|
||||
"username" => "اسم المستخدم",
|
||||
"welcome" => "مرحباً بك في{0}!"
|
||||
];
|
||||
@@ -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!"
|
||||
];
|
||||
@@ -25,6 +25,7 @@ return [
|
||||
"migration_running" => "",
|
||||
"password" => "Password",
|
||||
"required_username" => "",
|
||||
'too_many_attempts' => 'Твърде много опити. Моля, изчакайте малко и опитайте отново.',
|
||||
"username" => "Username",
|
||||
"welcome" => ""
|
||||
];
|
||||
@@ -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}!"
|
||||
];
|
||||
@@ -25,6 +25,7 @@ return [
|
||||
"migration_running" => "",
|
||||
"password" => "وشەی نهێنی",
|
||||
"required_username" => "خانەی ناوی بەکارهێنەر پێویستە.",
|
||||
'too_many_attempts' => 'هەوڵدانی زۆر زیادە. تکایە کەمێک چاوەڕێ بکە و دووبارە هەوڵ بدەرەوە.',
|
||||
"username" => "ناوی بەکارهێنەر",
|
||||
"welcome" => "بەخێربێن بۆ {0}!"
|
||||
];
|
||||
@@ -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" => ""
|
||||
];
|
||||
@@ -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" => ""
|
||||
];
|
||||
@@ -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}!"
|
||||
];
|
||||
@@ -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}!"
|
||||
];
|
||||
@@ -25,6 +25,7 @@ return [
|
||||
"migration_running" => "",
|
||||
"password" => "",
|
||||
"required_username" => "",
|
||||
'too_many_attempts' => 'Πάρα πολλές προσπάθειες. Παρακαλώ περιμένετε λίγο και δοκιμάστε ξανά.',
|
||||
"username" => "",
|
||||
"welcome" => ""
|
||||
];
|
||||
@@ -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}!"
|
||||
];
|
||||
@@ -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}!"
|
||||
];
|
||||
@@ -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}!"
|
||||
];
|
||||
@@ -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}!"
|
||||
];
|
||||
@@ -25,6 +25,7 @@ return [
|
||||
"migration_running" => "",
|
||||
"password" => "کلمه عبور",
|
||||
"required_username" => "",
|
||||
'too_many_attempts' => 'تعداد تلاشها بیش از حد مجاز است. لطفاً کمی صبر کنید و دوباره امتحان کنید.',
|
||||
"username" => "نام کاربری",
|
||||
"welcome" => ""
|
||||
];
|
||||
@@ -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} !"
|
||||
];
|
||||
@@ -25,6 +25,7 @@ return [
|
||||
"migration_running" => "",
|
||||
"password" => "סיסמה",
|
||||
"required_username" => "",
|
||||
'too_many_attempts' => 'יותר מדי ניסיונות. אנא המתן רגע ונסה שוב.',
|
||||
"username" => "שם משתמש",
|
||||
"welcome" => ""
|
||||
];
|
||||
@@ -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" => ""
|
||||
];
|
||||
@@ -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" => ""
|
||||
];
|
||||
@@ -25,6 +25,7 @@ return [
|
||||
"migration_running" => "",
|
||||
"password" => "",
|
||||
"required_username" => "",
|
||||
'too_many_attempts' => 'Չափազանց շատ փորձեր: Խնդրում ենք սպասել մի փոքր և կրկին փորձել:',
|
||||
"username" => "",
|
||||
"welcome" => ""
|
||||
];
|
||||
@@ -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}!"
|
||||
];
|
||||
@@ -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}!"
|
||||
];
|
||||
@@ -25,6 +25,7 @@ return [
|
||||
"migration_running" => "",
|
||||
"password" => "ពាក្យសំងាត់",
|
||||
"required_username" => "",
|
||||
'too_many_attempts' => 'ការព្យាយាមច្រើនពេក។ សូមរង់ចាំបន្តិច ហើយព្យាយាមម្តងទៀត។',
|
||||
"username" => "ឈ្មោះ",
|
||||
"welcome" => ""
|
||||
];
|
||||
@@ -25,6 +25,7 @@ return [
|
||||
"migration_running" => "",
|
||||
"password" => "Password",
|
||||
"required_username" => "",
|
||||
'too_many_attempts' => 'ພະຍາຍາມຫຼາຍເກີນໄປ. ກະລຸນາລໍຖ້າບຶດໜຶ່ງ ແລ້ວລອງໃໝ່ອີກຄັ້ງ.',
|
||||
"username" => "Username",
|
||||
"welcome" => ""
|
||||
];
|
||||
@@ -25,6 +25,7 @@ return [
|
||||
"migration_running" => "",
|
||||
"password" => "",
|
||||
"required_username" => "",
|
||||
'too_many_attempts' => 'വളരെയധികം ശ്രമങ്ങൾ. ദയവായി അല്പസമയം കാത്തിരുന്ന് വീണ്ടും ശ്രമിക്കുക.',
|
||||
"username" => "",
|
||||
"welcome" => ""
|
||||
];
|
||||
@@ -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" => ""
|
||||
];
|
||||
@@ -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}!"
|
||||
];
|
||||
@@ -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}!"
|
||||
];
|
||||
@@ -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}!"
|
||||
];
|
||||
@@ -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}!"
|
||||
];
|
||||
@@ -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" => ""
|
||||
];
|
||||
@@ -25,6 +25,7 @@ return [
|
||||
"migration_running" => "",
|
||||
"password" => "Пароль",
|
||||
"required_username" => "",
|
||||
'too_many_attempts' => 'Слишком много попыток. Пожалуйста, подождите немного и попробуйте снова.',
|
||||
"username" => "Имя пользователя",
|
||||
"welcome" => "Панель управления"
|
||||
];
|
||||
@@ -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}!"
|
||||
];
|
||||
@@ -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}!"
|
||||
];
|
||||
@@ -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}!"
|
||||
];
|
||||
@@ -25,6 +25,7 @@ return [
|
||||
"migration_running" => "",
|
||||
"password" => "கடவுச்சொல்",
|
||||
"required_username" => "",
|
||||
'too_many_attempts' => 'பல முறை முயற்சி செய்யப்பட்டது. தயவுசெய்து சிறிது நேரம் காத்திருந்து மீண்டும் முயற்சிக்கவும்.',
|
||||
"username" => "பயனர்பெயர்",
|
||||
"welcome" => "{0} க்கு வருக!"
|
||||
];
|
||||
@@ -25,6 +25,7 @@ return [
|
||||
"migration_running" => "",
|
||||
"password" => "รหัสผ่าน",
|
||||
"required_username" => "จำเป็นต้องระบุชื่อผู้ใช้งาน",
|
||||
'too_many_attempts' => 'พยายามมากเกินไป กรุณารอสักครู่แล้วลองใหม่อีกครั้ง',
|
||||
"username" => "ชื่อผู้ใช้",
|
||||
"welcome" => "ยินดีต้อนรับสู่ {0}!"
|
||||
];
|
||||
@@ -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" => ""
|
||||
];
|
||||
@@ -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!"
|
||||
];
|
||||
@@ -25,6 +25,7 @@ return [
|
||||
"migration_running" => "",
|
||||
"password" => "Пароль",
|
||||
"required_username" => "",
|
||||
'too_many_attempts' => 'Забагато спроб. Будь ласка, зачекайте трохи і спробуйте знову.',
|
||||
"username" => "Ім'я користувача",
|
||||
"welcome" => ""
|
||||
];
|
||||
@@ -25,6 +25,7 @@ return [
|
||||
"migration_running" => "",
|
||||
"password" => "",
|
||||
"required_username" => "",
|
||||
'too_many_attempts' => 'بہت زیادہ کوششیں۔ براہ کرم تھوڑی دیر انتظار کریں اور دوبارہ کوشش کریں۔',
|
||||
"username" => "",
|
||||
"welcome" => ""
|
||||
];
|
||||
@@ -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}!"
|
||||
];
|
||||
@@ -24,6 +24,7 @@ return [
|
||||
"migration_required" => "",
|
||||
"migration_running" => "",
|
||||
"password" => "密码",
|
||||
'too_many_attempts' => '太多次尝试。请稍等片刻后重试。',
|
||||
"username" => "用户名",
|
||||
"welcome" => "欢迎来到 {0}!"
|
||||
];
|
||||
@@ -25,6 +25,7 @@ return [
|
||||
"migration_running" => "",
|
||||
"password" => "密碼",
|
||||
"required_username" => "",
|
||||
'too_many_attempts' => '嘗試次數過多。請稍候片刻後再試一次。',
|
||||
"username" => "帳號",
|
||||
"welcome" => "歡迎來到 {0}!"
|
||||
];
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Controllers;
|
||||
|
||||
use CodeIgniter\Config\Services;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use CodeIgniter\Test\DatabaseTestTrait;
|
||||
use CodeIgniter\Test\FeatureTestTrait;
|
||||
|
||||
/**
|
||||
* Test suite for the Login controller, including the CI Throttler
|
||||
* mitigation for brute-force/credential-stuffing (GHSA-hm9c-xchj-xgcp).
|
||||
*/
|
||||
class LoginTest extends CIUnitTestCase
|
||||
{
|
||||
use DatabaseTestTrait;
|
||||
use FeatureTestTrait;
|
||||
|
||||
protected $migrate = true;
|
||||
protected $migrateOnce = true;
|
||||
protected $refresh = false;
|
||||
protected $namespace = null;
|
||||
|
||||
private array $usedKeys = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Services::session()->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Filters;
|
||||
|
||||
use App\Filters\Throttle;
|
||||
use CodeIgniter\HTTP\IncomingRequest;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use Config\Services;
|
||||
|
||||
class ThrottleTest extends CIUnitTestCase
|
||||
{
|
||||
private Throttle $filter;
|
||||
private array $usedKeys = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->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());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user