refactor users module to domain/controller and release 0.253 update package
This commit is contained in:
@@ -159,3 +159,27 @@ Tlumaczenia jednostek (per jezyk).
|
|||||||
| text | Nazwa jednostki |
|
| text | Nazwa jednostki |
|
||||||
|
|
||||||
**Używane w:** `Domain\Dictionaries\DictionariesRepository`
|
**Używane w:** `Domain\Dictionaries\DictionariesRepository`
|
||||||
|
|
||||||
|
## pp_users
|
||||||
|
Uzytkownicy panelu administratora.
|
||||||
|
|
||||||
|
| Kolumna | Opis |
|
||||||
|
|---------|------|
|
||||||
|
| id | PK |
|
||||||
|
| login | Login / e-mail uzytkownika |
|
||||||
|
| password | Hash hasla (legacy: md5) |
|
||||||
|
| status | Status konta: 1 = aktywny, 0 = zablokowany |
|
||||||
|
| admin | Flaga dostepu do panelu admin |
|
||||||
|
| error_logged_count | Licznik nieudanych logowan |
|
||||||
|
| last_logged | Data ostatniego poprawnego logowania |
|
||||||
|
| last_error_logged | Data ostatniej nieudanej proby logowania |
|
||||||
|
| twofa_enabled | Czy wlaczone 2FA (0/1) |
|
||||||
|
| twofa_email | E-mail do wysylki kodu 2FA |
|
||||||
|
| twofa_code_hash | Hash aktualnego kodu 2FA |
|
||||||
|
| twofa_expires_at | Data waznosci kodu 2FA |
|
||||||
|
| twofa_sent_at | Data ostatniej wysylki kodu 2FA |
|
||||||
|
| twofa_failed_attempts | Liczba nieudanych prob 2FA |
|
||||||
|
|
||||||
|
**Uzywane w:** `Domain\User\UserRepository`, `admin\Controllers\UsersController`, `admin\factory\Users`
|
||||||
|
|
||||||
|
**Aktualizacja 2026-02-12:** uzycia `pp_users` sa prowadzone przez `Domain\\User\\UserRepository` (legacy `admin\\factory\\Users` usunieto).
|
||||||
|
|||||||
@@ -199,7 +199,8 @@ autoload/
|
|||||||
│ ├── Controllers/ # Nowe kontrolery (namespace \admin\Controllers\)
|
│ ├── Controllers/ # Nowe kontrolery (namespace \admin\Controllers\)
|
||||||
│ │ ├── BannerController.php # DI, instancyjny
|
│ │ ├── BannerController.php # DI, instancyjny
|
||||||
│ │ ├── SettingsController.php # DI, instancyjny (clearCache, save, view)
|
│ │ ├── SettingsController.php # DI, instancyjny (clearCache, save, view)
|
||||||
│ │ └── ProductArchiveController.php # DI, instancyjny (list, unarchive)
|
│ │ ├── ProductArchiveController.php # DI, instancyjny (list, unarchive)
|
||||||
|
│ │ └── UsersController.php # DI, instancyjny (view_list, user_edit, user_save, user_delete, login_form, twofa)
|
||||||
│ ├── class.Site.php # Router: nowy kontroler → fallback stary
|
│ ├── class.Site.php # Router: nowy kontroler → fallback stary
|
||||||
│ ├── controls/ # Stare kontrolery (niezależny fallback)
|
│ ├── controls/ # Stare kontrolery (niezależny fallback)
|
||||||
│ ├── factory/ # Stare helpery (niezależny fallback)
|
│ ├── factory/ # Stare helpery (niezależny fallback)
|
||||||
@@ -210,7 +211,8 @@ autoload/
|
|||||||
|
|
||||||
#### Aktualny stan migracji (uzupełnienie)
|
#### Aktualny stan migracji (uzupełnienie)
|
||||||
- Dodane repozytorium: `Domain\Dictionaries\DictionariesRepository`
|
- Dodane repozytorium: `Domain\Dictionaries\DictionariesRepository`
|
||||||
- Dodane kontrolery DI: `admin\Controllers\DictionariesController`, `admin\Controllers\FilemanagerController`
|
- Dodane kontrolery DI: `admin\Controllers\DictionariesController`, `admin\Controllers\FilemanagerController`, `admin\Controllers\UsersController`
|
||||||
|
- Dodane repozytorium: `Domain\User\UserRepository`
|
||||||
- `Domain\Settings\SettingsRepository` działa bezpośrednio na DB (bez delegacji do `admin\factory\Settings`)
|
- `Domain\Settings\SettingsRepository` działa bezpośrednio na DB (bez delegacji do `admin\factory\Settings`)
|
||||||
|
|
||||||
### Routing admin (admin\Site::route())
|
### Routing admin (admin\Site::route())
|
||||||
@@ -254,8 +256,8 @@ tests/
|
|||||||
│ └── ProductArchiveControllerTest.php # 6 testów
|
│ └── ProductArchiveControllerTest.php # 6 testów
|
||||||
└── Integration/
|
└── Integration/
|
||||||
```
|
```
|
||||||
Aktualnie w suite są też testy modułów `Dictionaries` i `Articles` (repozytoria + kontrolery DI).
|
Aktualnie w suite są też testy modułów `Dictionaries`, `Articles` i `Users` (repozytoria + kontrolery DI).
|
||||||
**Łącznie: 82 tests, 181 assertions**
|
**Łącznie: 119 tests, 256 assertions**
|
||||||
|
|
||||||
## Ostatnie modyfikacje
|
## Ostatnie modyfikacje
|
||||||
|
|
||||||
@@ -368,5 +370,24 @@ Aktualnie w suite są też testy modułów `Dictionaries` i `Articles` (repozyto
|
|||||||
- Metoda `clear_product_cache()` w klasie S
|
- Metoda `clear_product_cache()` w klasie S
|
||||||
|
|
||||||
---
|
---
|
||||||
*Dokument aktualizowany: 2026-02-10*
|
*Dokument aktualizowany: 2026-02-12*
|
||||||
|
|
||||||
|
|
||||||
|
### 2026-02-12: Migracja Users (/admin/users) (ver. 0.253)
|
||||||
|
- **NOWE:** `Domain\User\UserRepository` - repozytorium uzytkownikow (CRUD, check_login, logon, details, 2FA)
|
||||||
|
- **NOWE:** `admin\Controllers\UsersController` - kontroler DI dla akcji `view_list`, `user_edit`, `user_save`, `user_delete`, `login_form`, `twofa`
|
||||||
|
- **UPDATE:** `admin\Site` - dodany factory wpis dla modulu `Users` w mapie nowych kontrolerow
|
||||||
|
- **UPDATE:** `admin\factory\Users` - fasada deleguje logike do `Domain\User\UserRepository`
|
||||||
|
- **UPDATE:** `admin/ajax/users.php` - `check_login` korzysta bezposrednio z `UserRepository`
|
||||||
|
- **CLEANUP:** usuniety `autoload/admin/controls/class.Users.php` (brak fallback - nowy kontroler obsluguje wszystkie akcje)
|
||||||
|
- Testy: 119 tests, 256 assertions
|
||||||
|
|
||||||
|
---
|
||||||
|
*Dokument aktualizowany: 2026-02-12*
|
||||||
|
- **UPDATE:** widoki Users przeniesione z `grid/gridEdit` na `components/table-list` i `components/form-edit`
|
||||||
|
|
||||||
|
## Aktualizacja 2026-02-12 (finalizacja Users)
|
||||||
|
- Modu<64> users dzia<69>a na `Domain\\User\\UserRepository` + `admin\\Controllers\\UsersController`.
|
||||||
|
- Usuni<6E>to legacy klasy: `autoload/admin/controls/class.Users.php`, `autoload/admin/factory/class.Users.php`, `autoload/admin/view/class.Users.php`.
|
||||||
|
- Walidacja: przy w<><77>czonym 2FA pole `twofa_email` jest wymagane.
|
||||||
|
- Widoki users przeniesione na `components/table-list` i `components/form-edit`.
|
||||||
|
|||||||
@@ -250,6 +250,16 @@ grep -r "Product::getQuantity" .
|
|||||||
- Legacy cleanup: usunięto `autoload/admin/controls/class.Filemanager.php` i `autoload/admin/view/class.FileManager.php`
|
- Legacy cleanup: usunięto `autoload/admin/controls/class.Filemanager.php` i `autoload/admin/view/class.FileManager.php`
|
||||||
- Aktualizacja: ver. 0.252
|
- Aktualizacja: ver. 0.252
|
||||||
|
|
||||||
|
- **Users** (migracja kontrolera i repozytorium)
|
||||||
|
- ✅ UserRepository - **ZMIGROWANE** (2026-02-12) 🎉
|
||||||
|
- Nowa klasa: `Domain\User\UserRepository` (find, getById, save, delete, checkLogin, logon, details, updateById, sendTwofaCode, verifyTwofaCode)
|
||||||
|
- Nowy kontroler: `admin\Controllers\UsersController` (DI, instancyjny: view_list, user_edit, user_save, user_delete, login_form, twofa)
|
||||||
|
- Router: `admin\Site` - factory wpis dla modulu `Users`
|
||||||
|
- Fasada: `admin\factory\Users` deleguje do repozytorium (backward compatibility dla login/2FA flow)
|
||||||
|
- AJAX: `admin/ajax/users.php` - `check_login` oparty o `UserRepository`
|
||||||
|
- Legacy cleanup: usuniety `autoload/admin/controls/class.Users.php`
|
||||||
|
- Testy: 25 testow repozytorium (CRUD, logon, 2FA, checkLogin) + 12 testow kontrolera (kontrakty + normalizeUser)
|
||||||
|
|
||||||
### 📋 Do zrobienia
|
### 📋 Do zrobienia
|
||||||
- Order
|
- Order
|
||||||
- Category
|
- Category
|
||||||
@@ -275,16 +285,18 @@ tests/
|
|||||||
│ │ ├── Cache/CacheRepositoryTest.php
|
│ │ ├── Cache/CacheRepositoryTest.php
|
||||||
│ │ ├── Dictionaries/DictionariesRepositoryTest.php
|
│ │ ├── Dictionaries/DictionariesRepositoryTest.php
|
||||||
│ │ ├── Product/ProductRepositoryTest.php
|
│ │ ├── Product/ProductRepositoryTest.php
|
||||||
│ │ └── Settings/SettingsRepositoryTest.php
|
│ │ ├── Settings/SettingsRepositoryTest.php
|
||||||
|
│ │ └── User/UserRepositoryTest.php
|
||||||
│ └── admin/
|
│ └── admin/
|
||||||
│ └── Controllers/
|
│ └── Controllers/
|
||||||
│ ├── ArticlesControllerTest.php
|
│ ├── ArticlesControllerTest.php
|
||||||
│ ├── DictionariesControllerTest.php
|
│ ├── DictionariesControllerTest.php
|
||||||
│ ├── ProductArchiveControllerTest.php
|
│ ├── ProductArchiveControllerTest.php
|
||||||
│ └── SettingsControllerTest.php
|
│ ├── SettingsControllerTest.php
|
||||||
|
│ └── UsersControllerTest.php
|
||||||
└── Integration/
|
└── Integration/
|
||||||
```
|
```
|
||||||
**Łącznie: 82 testów, 181 asercji**
|
**Łącznie: 119 testów, 256 asercji**
|
||||||
|
|
||||||
### Przykład testu
|
### Przykład testu
|
||||||
```php
|
```php
|
||||||
@@ -368,10 +380,11 @@ vendor/bin/phpstan analyse autoload/Domain
|
|||||||
5. **Dictionaries** ✅ (repo + kontroler + form/table, ver. 0.251)
|
5. **Dictionaries** ✅ (repo + kontroler + form/table, ver. 0.251)
|
||||||
6. **ProductArchive** ✅ (migracja kontrolera + cleanup szablonów, ver. 0.252)
|
6. **ProductArchive** ✅ (migracja kontrolera + cleanup szablonów, ver. 0.252)
|
||||||
7. **Filemanager** ✅ (migracja routingu + fix `Invalid Key`, ver. 0.252)
|
7. **Filemanager** ✅ (migracja routingu + fix `Invalid Key`, ver. 0.252)
|
||||||
8. **Order**
|
8. **Users** ✅ (repo + kontroler + 2FA + legacy cleanup, ver. 0.253)
|
||||||
9. **Category**
|
9. **Order**
|
||||||
10. **ShopAttribute**
|
10. **Category**
|
||||||
11. **Pages** (`browse_list` i powiązane widoki nadal legacy)
|
11. **ShopAttribute**
|
||||||
|
12. **Pages** (`browse_list` i powiązane widoki nadal legacy)
|
||||||
|
|
||||||
- **Form Edit System** - Nowy uniwersalny system formularzy edycji
|
- **Form Edit System** - Nowy uniwersalny system formularzy edycji
|
||||||
- ✅ Klasy ViewModel: `FormFieldType`, `FormField`, `FormTab`, `FormAction`, `FormEditViewModel`
|
- ✅ Klasy ViewModel: `FormFieldType`, `FormField`, `FormTab`, `FormAction`, `FormEditViewModel`
|
||||||
@@ -386,7 +399,7 @@ vendor/bin/phpstan analyse autoload/Domain
|
|||||||
|
|
||||||
---
|
---
|
||||||
*Rozpoczęto: 2025-02-05*
|
*Rozpoczęto: 2025-02-05*
|
||||||
*Ostatnia aktualizacja: 2026-02-10*
|
*Ostatnia aktualizacja: 2026-02-12*
|
||||||
|
|
||||||
|
|
||||||
## Form Edit System - Dokumentacja użycia
|
## Form Edit System - Dokumentacja użycia
|
||||||
@@ -544,3 +557,20 @@ Gdy `persist = true`:
|
|||||||
3. **Szablon** - usuń stary szablon lub zostaw jako fallback
|
3. **Szablon** - usuń stary szablon lub zostaw jako fallback
|
||||||
4. **Testy** - zaktualizuj testy jeśli zmienił się format danych
|
4. **Testy** - zaktualizuj testy jeśli zmienił się format danych
|
||||||
|
|
||||||
|
|
||||||
|
## Aktualizacja 2026-02-12 - Users
|
||||||
|
|
||||||
|
### Users (migracja kontrolera i repozytorium)
|
||||||
|
- **NOWE:** `Domain\User\UserRepository` (delete, find, save, checkLogin, logon, details, sendTwofaCode, verifyTwofaCode)
|
||||||
|
- **NOWE:** `admin\Controllers\UsersController` (view_list, user_edit, user_save, user_delete)
|
||||||
|
- **UPDATE:** Router `admin\Site` - nowy kontroler DI dla modu<64>u `Users`
|
||||||
|
- **UPDATE:** `admin\factory\Users` jako fasada delegujaca do repozytorium
|
||||||
|
- **UPDATE:** `admin/ajax/users.php` - endpoint `check_login` oparty o `UserRepository`
|
||||||
|
- Testy po zmianie: 95 tests, 204 assertions
|
||||||
|
- **UPDATE:** UsersController: `view_list` + `user_edit` migrowane na nowy system list/form (table-list + form-edit)
|
||||||
|
|
||||||
|
## Aktualizacja 2026-02-12 (finalizacja Users)
|
||||||
|
- Users: pelna migracja na nowa architekture (Domain + DI Controller), bez fallbacku do legacy kontrolera/factory/view.
|
||||||
|
- `UsersController` obsluguje: `list/view_list`, `user_edit`, `user_save`, `user_delete`, `login_form`, `twofa`.
|
||||||
|
- Dodano walidacje warunkowa: `twofa_email` wymagany gdy `twofa_enabled = 1`.
|
||||||
|
- Widoki users migrowane z `grid/gridEdit` na `table-list` i `form-edit`.
|
||||||
|
|||||||
30
TESTING.md
30
TESTING.md
@@ -51,13 +51,15 @@ tests/
|
|||||||
| | |-- Cache/CacheRepositoryTest.php
|
| | |-- Cache/CacheRepositoryTest.php
|
||||||
| | |-- Dictionaries/DictionariesRepositoryTest.php
|
| | |-- Dictionaries/DictionariesRepositoryTest.php
|
||||||
| | |-- Product/ProductRepositoryTest.php
|
| | |-- Product/ProductRepositoryTest.php
|
||||||
| | `-- Settings/SettingsRepositoryTest.php
|
| | |-- Settings/SettingsRepositoryTest.php
|
||||||
|
| | `-- User/UserRepositoryTest.php
|
||||||
| `-- admin/
|
| `-- admin/
|
||||||
| `-- Controllers/
|
| `-- Controllers/
|
||||||
| |-- ArticlesControllerTest.php
|
| |-- ArticlesControllerTest.php
|
||||||
| |-- DictionariesControllerTest.php
|
| |-- DictionariesControllerTest.php
|
||||||
| |-- ProductArchiveControllerTest.php
|
| |-- ProductArchiveControllerTest.php
|
||||||
| `-- SettingsControllerTest.php
|
| |-- SettingsControllerTest.php
|
||||||
|
| `-- UsersControllerTest.php
|
||||||
`-- Integration/
|
`-- Integration/
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -138,3 +140,27 @@ $this->assertEquals(42, $value);
|
|||||||
- Konfiguracja PHPUnit: `phpunit.xml`
|
- Konfiguracja PHPUnit: `phpunit.xml`
|
||||||
- Bootstrap testow: `tests/bootstrap.php`
|
- Bootstrap testow: `tests/bootstrap.php`
|
||||||
- Dodatkowy opis: `tests/README.md`
|
- Dodatkowy opis: `tests/README.md`
|
||||||
|
|
||||||
|
## Aktualizacja suite
|
||||||
|
|
||||||
|
Ostatnio zweryfikowano: 2026-02-12
|
||||||
|
|
||||||
|
```text
|
||||||
|
OK (119 tests, 256 assertions)
|
||||||
|
```
|
||||||
|
|
||||||
|
Nowe testy dodane 2026-02-12:
|
||||||
|
- `tests/Unit/Domain/User/UserRepositoryTest.php` (25 testow: CRUD, logon, 2FA verify/send, checkLogin, updateById)
|
||||||
|
- `tests/Unit/admin/Controllers/UsersControllerTest.php` (12 testow: kontrakty + normalizeUser)
|
||||||
|
|
||||||
|
Aktualizacja po migracji widokow Users (2026-02-12):
|
||||||
|
```text
|
||||||
|
OK (120 tests, 262 assertions)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Aktualizacja suite (finalizacja Users)
|
||||||
|
Ostatnio zweryfikowano: 2026-02-12
|
||||||
|
|
||||||
|
```text
|
||||||
|
OK (120 tests, 262 assertions)
|
||||||
|
```
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ $a = \S::get( 'a' );
|
|||||||
|
|
||||||
if ( $a == 'check_login' )
|
if ( $a == 'check_login' )
|
||||||
{
|
{
|
||||||
$response = \admin\factory\Users::check_login( \S::get( 'login' ), \S::get( 'user_id' ) );
|
global $mdb;
|
||||||
|
$repository = new \Domain\User\UserRepository( $mdb );
|
||||||
|
$response = $repository->checkLogin( (string)\S::get( 'login' ), (int)\S::get( 'user_id' ) );
|
||||||
echo json_encode( $response );
|
echo json_encode( $response );
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -97,6 +97,7 @@ $cookie_name = 'admin_remember_' . str_replace( '.', '-', $domain );
|
|||||||
|
|
||||||
if ( isset( $_COOKIE[$cookie_name] ) && !isset( $_SESSION['user'] ) )
|
if ( isset( $_COOKIE[$cookie_name] ) && !isset( $_SESSION['user'] ) )
|
||||||
{
|
{
|
||||||
|
$users = new \Domain\User\UserRepository($mdb);
|
||||||
$payload = base64_decode($_COOKIE[$cookie_name]);
|
$payload = base64_decode($_COOKIE[$cookie_name]);
|
||||||
if ($payload !== false && strpos($payload, '.') !== false)
|
if ($payload !== false && strpos($payload, '.') !== false)
|
||||||
{
|
{
|
||||||
@@ -114,7 +115,7 @@ if ( isset( $_COOKIE[$cookie_name] ) && !isset( $_SESSION['user'] ) )
|
|||||||
$user_data = $mdb->get('pp_users', '*', ['AND' => ['login' => $data['login'], 'status' => 1]]);
|
$user_data = $mdb->get('pp_users', '*', ['AND' => ['login' => $data['login'], 'status' => 1]]);
|
||||||
if ($user_data)
|
if ($user_data)
|
||||||
{
|
{
|
||||||
\S::set_session('user', \admin\factory\Users::details($data['login']));
|
\S::set_session('user', $users->details($data['login']));
|
||||||
$redirect = $_SERVER['REQUEST_URI'] ?: '/admin/articles/view_list/';
|
$redirect = $_SERVER['REQUEST_URI'] ?: '/admin/articles/view_list/';
|
||||||
header('Location: ' . $redirect);
|
header('Location: ' . $redirect);
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
@@ -18,6 +18,21 @@ $page = max(1, (int)($list->pagination['page'] ?? 1));
|
|||||||
$totalPages = max(1, (int)($list->pagination['total_pages'] ?? 1));
|
$totalPages = max(1, (int)($list->pagination['total_pages'] ?? 1));
|
||||||
$total = (int)($list->pagination['total'] ?? 0);
|
$total = (int)($list->pagination['total'] ?? 0);
|
||||||
$perPage = (int)($list->pagination['per_page'] ?? 15);
|
$perPage = (int)($list->pagination['per_page'] ?? 15);
|
||||||
|
|
||||||
|
$isCompactColumn = function(array $column): bool {
|
||||||
|
$key = strtolower(trim((string)($column['key'] ?? '')));
|
||||||
|
$label = strtolower(trim((string)($column['label'] ?? '')));
|
||||||
|
|
||||||
|
if (in_array($key, ['status', 'active', 'enabled', 'is_active'], true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($label, ['status', 'aktywny', 'aktywnosc', 'active'], true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
@@ -42,17 +57,34 @@ $perPage = (int)($list->pagination['per_page'] ?? 15);
|
|||||||
<?php
|
<?php
|
||||||
$filterKey = (string)($filter['key'] ?? '');
|
$filterKey = (string)($filter['key'] ?? '');
|
||||||
$inputId = 'filter_' . preg_replace('/[^a-zA-Z0-9_]+/', '_', $filterKey);
|
$inputId = 'filter_' . preg_replace('/[^a-zA-Z0-9_]+/', '_', $filterKey);
|
||||||
|
$filterType = (string)($filter['type'] ?? '');
|
||||||
|
$isCompactFilter = false;
|
||||||
|
if ($filterType === 'select') {
|
||||||
|
$options = (array)($filter['options'] ?? []);
|
||||||
|
$maxOptionLen = 0;
|
||||||
|
foreach ($options as $optionLabel) {
|
||||||
|
$len = strlen(trim((string)$optionLabel));
|
||||||
|
if ($len > $maxOptionLen) {
|
||||||
|
$maxOptionLen = $len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Krotkie selekty (np. tak/nie) nie musza zajmowac szerokiej kolumny.
|
||||||
|
$isCompactFilter = count($options) <= 5 && $maxOptionLen <= 12;
|
||||||
|
}
|
||||||
|
$filterColClass = $isCompactFilter ? 'col-sm-1 col-xs-6 mb10' : 'col-sm-2 mb10';
|
||||||
?>
|
?>
|
||||||
<div class="col-sm-2 mb10">
|
<div class="<?= htmlspecialchars($filterColClass, ENT_QUOTES, 'UTF-8'); ?>">
|
||||||
<label for="<?= htmlspecialchars($inputId, ENT_QUOTES, 'UTF-8'); ?>" class="control-label">
|
<label for="<?= htmlspecialchars($inputId, ENT_QUOTES, 'UTF-8'); ?>" class="control-label">
|
||||||
<?= htmlspecialchars((string)($filter['label'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>
|
<?= htmlspecialchars((string)($filter['label'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<?php if (($filter['type'] ?? '') === 'select'): ?>
|
<?php if ($filterType === 'select'): ?>
|
||||||
<select
|
<select
|
||||||
id="<?= htmlspecialchars($inputId, ENT_QUOTES, 'UTF-8'); ?>"
|
id="<?= htmlspecialchars($inputId, ENT_QUOTES, 'UTF-8'); ?>"
|
||||||
name="<?= htmlspecialchars($filter['key'], ENT_QUOTES, 'UTF-8'); ?>"
|
name="<?= htmlspecialchars($filter['key'], ENT_QUOTES, 'UTF-8'); ?>"
|
||||||
class="form-control input-sm"
|
class="form-control input-sm<?= $isCompactFilter ? ' js-filter-compact-select' : ''; ?>"
|
||||||
|
<?= $isCompactFilter ? 'data-compact-filter="1"' : ''; ?>
|
||||||
title="<?= htmlspecialchars($filter['label'], ENT_QUOTES, 'UTF-8'); ?>"
|
title="<?= htmlspecialchars($filter['label'], ENT_QUOTES, 'UTF-8'); ?>"
|
||||||
>
|
>
|
||||||
<?php foreach (($filter['options'] ?? []) as $value => $label): ?>
|
<?php foreach (($filter['options'] ?? []) as $value => $label): ?>
|
||||||
@@ -110,8 +142,12 @@ $perPage = (int)($list->pagination['per_page'] ?? 15);
|
|||||||
'dir' => $nextDir,
|
'dir' => $nextDir,
|
||||||
'page' => 1,
|
'page' => 1,
|
||||||
]);
|
]);
|
||||||
|
$headerClass = trim((string)($column['class'] ?? ''));
|
||||||
|
if ($isCompactColumn($column)) {
|
||||||
|
$headerClass = trim($headerClass . ' table-col-compact');
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<th class="<?= htmlspecialchars((string)($column['class'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>">
|
<th class="<?= htmlspecialchars($headerClass, ENT_QUOTES, 'UTF-8'); ?>">
|
||||||
<?php if ($isSortable): ?>
|
<?php if ($isSortable): ?>
|
||||||
<a href="<?= htmlspecialchars($sortUrl, ENT_QUOTES, 'UTF-8'); ?>">
|
<a href="<?= htmlspecialchars($sortUrl, ENT_QUOTES, 'UTF-8'); ?>">
|
||||||
<?= htmlspecialchars((string)($column['label'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>
|
<?= htmlspecialchars((string)($column['label'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>
|
||||||
@@ -138,8 +174,12 @@ $perPage = (int)($list->pagination['per_page'] ?? 15);
|
|||||||
$key = $column['key'] ?? '';
|
$key = $column['key'] ?? '';
|
||||||
$raw = !empty($column['raw']);
|
$raw = !empty($column['raw']);
|
||||||
$value = $row[$key] ?? '';
|
$value = $row[$key] ?? '';
|
||||||
|
$cellClass = trim((string)($column['class'] ?? ''));
|
||||||
|
if ($isCompactColumn($column)) {
|
||||||
|
$cellClass = trim($cellClass . ' table-col-compact');
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<td class="<?= htmlspecialchars((string)($column['class'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>">
|
<td class="<?= htmlspecialchars($cellClass, ENT_QUOTES, 'UTF-8'); ?>">
|
||||||
<?php if ($raw): ?>
|
<?php if ($raw): ?>
|
||||||
<?= (string)$value; ?>
|
<?= (string)$value; ?>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
@@ -209,7 +249,7 @@ $perPage = (int)($list->pagination['per_page'] ?? 15);
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6 text-right">
|
<div class="col-sm-6 text-right">
|
||||||
<form method="get" action="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" class="form-inline">
|
<form method="get" action="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" class="form-inline table-list-per-page-form">
|
||||||
<?php foreach ($list->query as $key => $value): ?>
|
<?php foreach ($list->query as $key => $value): ?>
|
||||||
<?php if ($key !== 'per_page' && $key !== 'page'): ?>
|
<?php if ($key !== 'per_page' && $key !== 'page'): ?>
|
||||||
<input type="hidden" name="<?= htmlspecialchars((string)$key, ENT_QUOTES, 'UTF-8'); ?>" value="<?= htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); ?>" />
|
<input type="hidden" name="<?= htmlspecialchars((string)$key, ENT_QUOTES, 'UTF-8'); ?>" value="<?= htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); ?>" />
|
||||||
@@ -236,6 +276,27 @@ $perPage = (int)($list->pagination['per_page'] ?? 15);
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.js-table-filters-form .js-filter-compact-select {
|
||||||
|
width: auto;
|
||||||
|
min-width: 110px;
|
||||||
|
max-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-list-table th.table-col-compact,
|
||||||
|
.table-list-table td.table-col-compact {
|
||||||
|
width: 120px;
|
||||||
|
min-width: 120px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-list-per-page-form {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.jconfirm.table-list-confirm-dialog .jconfirm-row {
|
.jconfirm.table-list-confirm-dialog .jconfirm-row {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,90 +1,2 @@
|
|||||||
<?php
|
<?= \Tpl::view('components/form-edit', ['form' => $this->form]); ?>
|
||||||
global $gdb, $config;
|
|
||||||
|
|
||||||
$this -> user['id'] ? $password_param = 'optional' : $password_param = 'require';
|
|
||||||
|
|
||||||
$grid = new \gridEdit;
|
|
||||||
$grid -> gdb_opt = $gdb;
|
|
||||||
$grid -> include_plugins = true;
|
|
||||||
$grid -> title = 'Zapisz użytkownika';
|
|
||||||
$grid -> fields = [
|
|
||||||
[
|
|
||||||
'db' => 'id',
|
|
||||||
'type' => 'hidden',
|
|
||||||
'value' => $this -> user['id']
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'db' => 'admin',
|
|
||||||
'type' => 'hidden',
|
|
||||||
'value' => '1'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'Login',
|
|
||||||
'db' => 'login',
|
|
||||||
'type' => 'text',
|
|
||||||
'value' => $this -> user['login'],
|
|
||||||
'params' => [ 'class' => 'require', 'function' => 'check_login' ]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'Aktywny',
|
|
||||||
'db' => 'status',
|
|
||||||
'type' => 'input_switch',
|
|
||||||
'checked' => $this -> user['status'] ? true : false
|
|
||||||
], [
|
|
||||||
'db' => 'twofa_enabled',
|
|
||||||
'name' => 'Dwustopniowe uwierzytelnianie (2FA)',
|
|
||||||
'type' => 'input_switch',
|
|
||||||
'checked' => $this -> user['twofa_enabled'] ? true : false,
|
|
||||||
], [
|
|
||||||
'db' => 'twofa_email',
|
|
||||||
'name' => 'E-mail do 2FA',
|
|
||||||
'type' => 'text',
|
|
||||||
'value' => $this -> user['twofa_email'],
|
|
||||||
], [
|
|
||||||
'name' => 'Hasło',
|
|
||||||
'db' => 'password',
|
|
||||||
'type' => 'text',
|
|
||||||
'params' => [ 'class' => $password_param, 'min' => 5 ]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'Hasło - powtórz',
|
|
||||||
'db' => 'password_re',
|
|
||||||
'type' => 'text',
|
|
||||||
'params' => [ 'class' => $password_param, 'min' => 5, 'equal' => 'password', 'error_txt' => 'Podane hasła są różne' ]
|
|
||||||
]
|
|
||||||
];
|
|
||||||
$grid -> actions = [
|
|
||||||
'save' => [ 'url' => '/admin/users/user_save/', 'back_url' => '/admin/users/view_list/' ],
|
|
||||||
'cancel' => [ 'url' => '/admin/users/view_list/' ]
|
|
||||||
];
|
|
||||||
echo $grid -> draw();
|
|
||||||
?>
|
|
||||||
<script type="text/javascript">
|
|
||||||
$( function()
|
|
||||||
{
|
|
||||||
disable_menu();
|
|
||||||
});
|
|
||||||
|
|
||||||
function check_login()
|
|
||||||
{
|
|
||||||
var response = null;
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
type: 'POST',
|
|
||||||
cache: false,
|
|
||||||
async: false,
|
|
||||||
url: '/admin/ajax.php',
|
|
||||||
data:
|
|
||||||
{
|
|
||||||
a: 'check_login',
|
|
||||||
login: $.trim( $( '#login' ).val() ),
|
|
||||||
user_id: <?= (int)$this -> user['id'];?>
|
|
||||||
},
|
|
||||||
success: function( data )
|
|
||||||
{
|
|
||||||
response = $.parseJSON( data );
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,47 +1,2 @@
|
|||||||
<?php
|
<?= \Tpl::view('components/table-list', ['list' => $this->viewModel]); ?>
|
||||||
global $gdb;
|
|
||||||
|
|
||||||
$grid = new \grid( 'pp_users' );
|
|
||||||
$grid -> gdb_opt = $gdb;
|
|
||||||
$grid -> order = [ 'column' => 'login', 'type' => 'ASC' ];
|
|
||||||
$grid -> where = [ 'id[!]' => 1 ];
|
|
||||||
$grid -> search = [
|
|
||||||
[ 'name' => 'Login', 'db' => 'login', 'type' => 'text' ],
|
|
||||||
[ 'name' => 'Aktywny', 'db' => 'status', 'type' => 'select', 'replace' => [ 'array' => [ 0 => 'nie', 1 => 'tak' ] ] ]
|
|
||||||
];
|
|
||||||
$grid -> columns_view = [
|
|
||||||
[
|
|
||||||
'name' => 'Lp.',
|
|
||||||
'th' => [ 'class' => 'g-lp' ],
|
|
||||||
'td' => [ 'class' => 'g-center' ],
|
|
||||||
'autoincrement' => true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'Aktywny',
|
|
||||||
'db' => 'status',
|
|
||||||
'replace' => [ 'array' => [ 0 => '<span style="color: #FF0000;">nie</span>', 1 => 'tak' ] ],
|
|
||||||
'th' => [ 'class' => 'g-center', 'style' => 'width: 150px;' ],
|
|
||||||
'td' => [ 'class' => 'g-center' ]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'Login',
|
|
||||||
'db' => 'login',
|
|
||||||
'sort' => true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'Edytuj',
|
|
||||||
'action' => [ 'type' => 'edit', 'url' => '/admin/users/user_edit/id=[id]' ],
|
|
||||||
'th' => [ 'class' => 'g-center', 'style' => 'width: 50px;' ],
|
|
||||||
'td' => [ 'class' => 'g-center' ]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'Usuń',
|
|
||||||
'action' => [ 'type' => 'delete', 'url' => '/admin/users/user_delete/id=[id]' ],
|
|
||||||
'th' => [ 'class' => 'g-center', 'style' => 'width: 50px;' ],
|
|
||||||
'td' => [ 'class' => 'g-center' ]
|
|
||||||
]
|
|
||||||
];
|
|
||||||
$grid -> buttons = [
|
|
||||||
[ 'label' => 'Dodaj użytkownika', 'url' => '/admin/users/user_edit/', 'icon' => 'fa-plus-circle', 'class' => 'btn-success' ]
|
|
||||||
];
|
|
||||||
echo $grid -> draw();
|
|
||||||
337
autoload/Domain/User/UserRepository.php
Normal file
337
autoload/Domain/User/UserRepository.php
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
<?php
|
||||||
|
namespace Domain\User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository odpowiedzialny za dostep do danych uzytkownikow admina.
|
||||||
|
*/
|
||||||
|
class UserRepository
|
||||||
|
{
|
||||||
|
private const MAX_PER_PAGE = 100;
|
||||||
|
|
||||||
|
private $db;
|
||||||
|
|
||||||
|
public function __construct($db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getById(int $userId): ?array
|
||||||
|
{
|
||||||
|
$user = $this->db->get('pp_users', '*', ['id' => $userId]);
|
||||||
|
return $user ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateById(int $userId, array $data): bool
|
||||||
|
{
|
||||||
|
return (bool)$this->db->update('pp_users', $data, ['id' => $userId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verifyTwofaCode(int $userId, string $code): bool
|
||||||
|
{
|
||||||
|
$user = $this->getById($userId);
|
||||||
|
if (!$user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((int)($user['twofa_failed_attempts'] ?? 0) >= 5) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($user['twofa_expires_at']) || time() > strtotime((string)$user['twofa_expires_at'])) {
|
||||||
|
$this->updateById($userId, [
|
||||||
|
'twofa_code_hash' => null,
|
||||||
|
'twofa_expires_at' => null,
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ok = (!empty($user['twofa_code_hash']) && password_verify($code, (string)$user['twofa_code_hash']));
|
||||||
|
if ($ok) {
|
||||||
|
$this->updateById($userId, [
|
||||||
|
'twofa_code_hash' => null,
|
||||||
|
'twofa_expires_at' => null,
|
||||||
|
'twofa_sent_at' => null,
|
||||||
|
'twofa_failed_attempts' => 0,
|
||||||
|
'last_logged' => date('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->updateById($userId, [
|
||||||
|
'twofa_failed_attempts' => (int)($user['twofa_failed_attempts'] ?? 0) + 1,
|
||||||
|
'last_error_logged' => date('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendTwofaCode(int $userId, bool $resend = false): bool
|
||||||
|
{
|
||||||
|
$user = $this->getById($userId);
|
||||||
|
if (!$user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((int)($user['twofa_enabled'] ?? 0) !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$to = !empty($user['twofa_email']) ? (string)$user['twofa_email'] : (string)$user['login'];
|
||||||
|
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($resend && !empty($user['twofa_sent_at'])) {
|
||||||
|
$last = strtotime((string)$user['twofa_sent_at']);
|
||||||
|
if ($last && (time() - $last) < 30) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = random_int(100000, 999999);
|
||||||
|
$hash = password_hash((string)$code, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
|
$this->updateById($userId, [
|
||||||
|
'twofa_code_hash' => $hash,
|
||||||
|
'twofa_expires_at' => date('Y-m-d H:i:s', time() + 10 * 60),
|
||||||
|
'twofa_sent_at' => date('Y-m-d H:i:s'),
|
||||||
|
'twofa_failed_attempts' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$subject = 'Twoj kod logowania 2FA';
|
||||||
|
$body = 'Twoj kod logowania do panelu administratora: ' . $code . '. Kod jest wazny przez 10 minut.';
|
||||||
|
|
||||||
|
$sent = \S::send_email($to, $subject, $body);
|
||||||
|
if ($sent) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = "MIME-Version: 1.0\r\n";
|
||||||
|
$headers .= "Content-type: text/plain; charset=UTF-8\r\n";
|
||||||
|
$headers .= "From: no-reply@" . ($_SERVER['HTTP_HOST'] ?? 'localhost') . "\r\n";
|
||||||
|
$encodedSubject = mb_encode_mimeheader($subject, 'UTF-8');
|
||||||
|
|
||||||
|
return mail($to, $encodedSubject, $body, $headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(int $userId): bool
|
||||||
|
{
|
||||||
|
return (bool)$this->db->delete('pp_users', ['id' => $userId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function find(int $userId): ?array
|
||||||
|
{
|
||||||
|
$user = $this->db->get('pp_users', '*', ['id' => $userId]);
|
||||||
|
return $user ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(
|
||||||
|
int $userId,
|
||||||
|
string $login,
|
||||||
|
$status,
|
||||||
|
string $password,
|
||||||
|
string $passwordRepeat,
|
||||||
|
$admin,
|
||||||
|
$twofaEnabled = 0,
|
||||||
|
string $twofaEmail = ''
|
||||||
|
): array {
|
||||||
|
if ($userId <= 0) {
|
||||||
|
if (strlen($password) < 5) {
|
||||||
|
return ['status' => 'error', 'msg' => 'Podane haslo jest zbyt krotkie.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($password !== $passwordRepeat) {
|
||||||
|
return ['status' => 'error', 'msg' => 'Podane hasla sa rozne'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$inserted = $this->db->insert('pp_users', [
|
||||||
|
'login' => $login,
|
||||||
|
'status' => $this->toSwitchValue($status),
|
||||||
|
'admin' => (int)$admin,
|
||||||
|
'password' => md5($password),
|
||||||
|
'twofa_enabled' => $this->toSwitchValue($twofaEnabled),
|
||||||
|
'twofa_email' => $twofaEmail,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($inserted) {
|
||||||
|
return ['status' => 'ok', 'msg' => 'Uzytkownik zostal zapisany.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['status' => 'error', 'msg' => 'Podczas zapisywania uzytkownika wystapil blad.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($password !== '' && strlen($password) < 5) {
|
||||||
|
return ['status' => 'error', 'msg' => 'Podane haslo jest zbyt krotkie.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($password !== '' && $password !== $passwordRepeat) {
|
||||||
|
return ['status' => 'error', 'msg' => 'Podane hasla sa rozne'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($password !== '') {
|
||||||
|
$this->db->update('pp_users', [
|
||||||
|
'password' => md5($password),
|
||||||
|
], [
|
||||||
|
'id' => $userId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->update('pp_users', [
|
||||||
|
'login' => $login,
|
||||||
|
'admin' => (int)$admin,
|
||||||
|
'status' => $this->toSwitchValue($status),
|
||||||
|
'twofa_enabled' => $this->toSwitchValue($twofaEnabled),
|
||||||
|
'twofa_email' => $twofaEmail,
|
||||||
|
], [
|
||||||
|
'id' => $userId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ['status' => 'ok', 'msg' => 'Uzytkownik zostal zapisany.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkLogin(string $login, int $userId): array
|
||||||
|
{
|
||||||
|
$existing = $this->db->get('pp_users', 'login', [
|
||||||
|
'AND' => [
|
||||||
|
'login' => $login,
|
||||||
|
'id[!]' => $userId,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
return ['status' => 'error', 'msg' => 'Podany login jest juz zajety.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['status' => 'ok'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logon(string $login, string $password): int
|
||||||
|
{
|
||||||
|
if (!$this->db->get('pp_users', '*', ['login' => $login])) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->db->get('pp_users', '*', [
|
||||||
|
'AND' => [
|
||||||
|
'login' => $login,
|
||||||
|
'status' => 1,
|
||||||
|
'error_logged_count[<]' => 5,
|
||||||
|
],
|
||||||
|
])) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->db->get('pp_users', '*', [
|
||||||
|
'AND' => [
|
||||||
|
'login' => $login,
|
||||||
|
'status' => 1,
|
||||||
|
'password' => md5($password),
|
||||||
|
],
|
||||||
|
])) {
|
||||||
|
$this->db->update('pp_users', [
|
||||||
|
'last_logged' => date('Y-m-d H:i:s'),
|
||||||
|
'error_logged_count' => 0,
|
||||||
|
], [
|
||||||
|
'login' => $login,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->update('pp_users', [
|
||||||
|
'last_error_logged' => date('Y-m-d H:i:s'),
|
||||||
|
'error_logged_count[+]' => 1,
|
||||||
|
], [
|
||||||
|
'login' => $login,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ((int)$this->db->get('pp_users', 'error_logged_count', ['login' => $login]) >= 5) {
|
||||||
|
$this->db->update('pp_users', ['status' => 0], ['login' => $login]);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function details(string $login): ?array
|
||||||
|
{
|
||||||
|
$user = $this->db->get('pp_users', '*', ['login' => $login]);
|
||||||
|
return $user ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{items: array<int, array<string, mixed>>, total: int}
|
||||||
|
*/
|
||||||
|
public function listForAdmin(
|
||||||
|
array $filters,
|
||||||
|
string $sortColumn = 'login',
|
||||||
|
string $sortDir = 'ASC',
|
||||||
|
int $page = 1,
|
||||||
|
int $perPage = 15
|
||||||
|
): array {
|
||||||
|
$allowedSortColumns = [
|
||||||
|
'login' => 'pu.login',
|
||||||
|
'status' => 'pu.status',
|
||||||
|
];
|
||||||
|
|
||||||
|
$sortSql = $allowedSortColumns[$sortColumn] ?? 'pu.login';
|
||||||
|
$sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC';
|
||||||
|
$page = max(1, $page);
|
||||||
|
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
|
||||||
|
$offset = ($page - 1) * $perPage;
|
||||||
|
|
||||||
|
$where = ['pu.id != 1'];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
$login = trim((string)($filters['login'] ?? ''));
|
||||||
|
if ($login !== '') {
|
||||||
|
if (strlen($login) > 255) {
|
||||||
|
$login = substr($login, 0, 255);
|
||||||
|
}
|
||||||
|
$where[] = 'pu.login LIKE :login';
|
||||||
|
$params[':login'] = '%' . $login . '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = trim((string)($filters['status'] ?? ''));
|
||||||
|
if ($status === '0' || $status === '1') {
|
||||||
|
$where[] = 'pu.status = :status';
|
||||||
|
$params[':status'] = (int)$status;
|
||||||
|
}
|
||||||
|
|
||||||
|
$whereSql = implode(' AND ', $where);
|
||||||
|
|
||||||
|
$sqlCount = "
|
||||||
|
SELECT COUNT(0)
|
||||||
|
FROM pp_users AS pu
|
||||||
|
WHERE {$whereSql}
|
||||||
|
";
|
||||||
|
|
||||||
|
$stmtCount = $this->db->query($sqlCount, $params);
|
||||||
|
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
|
||||||
|
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
pu.id,
|
||||||
|
pu.login,
|
||||||
|
pu.status
|
||||||
|
FROM pp_users AS pu
|
||||||
|
WHERE {$whereSql}
|
||||||
|
ORDER BY {$sortSql} {$sortDir}, pu.id ASC
|
||||||
|
LIMIT {$perPage} OFFSET {$offset}
|
||||||
|
";
|
||||||
|
|
||||||
|
$stmt = $this->db->query($sql, $params);
|
||||||
|
$items = $stmt ? $stmt->fetchAll() : [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'items' => is_array($items) ? $items : [],
|
||||||
|
'total' => $total,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function toSwitchValue($value): int
|
||||||
|
{
|
||||||
|
return ($value === 'on' || $value === 1 || $value === '1' || $value === true) ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
346
autoload/admin/Controllers/UsersController.php
Normal file
346
autoload/admin/Controllers/UsersController.php
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
<?php
|
||||||
|
namespace admin\Controllers;
|
||||||
|
|
||||||
|
use Domain\User\UserRepository;
|
||||||
|
use admin\ViewModels\Forms\FormAction;
|
||||||
|
use admin\ViewModels\Forms\FormEditViewModel;
|
||||||
|
use admin\ViewModels\Forms\FormField;
|
||||||
|
use admin\Support\Forms\FormRequestHandler;
|
||||||
|
|
||||||
|
class UsersController
|
||||||
|
{
|
||||||
|
private UserRepository $repository;
|
||||||
|
private FormRequestHandler $formHandler;
|
||||||
|
|
||||||
|
public function __construct(UserRepository $repository)
|
||||||
|
{
|
||||||
|
$this->repository = $repository;
|
||||||
|
$this->formHandler = new FormRequestHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user_delete(): void
|
||||||
|
{
|
||||||
|
if ($this->repository->delete((int)\S::get('id'))) {
|
||||||
|
\S::alert('Uzytkownik zostal usuniety.');
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Location: /admin/users/view_list/');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user_save(): void
|
||||||
|
{
|
||||||
|
$legacyValues = \S::get('values');
|
||||||
|
if ($legacyValues) {
|
||||||
|
$values = json_decode((string)$legacyValues, true);
|
||||||
|
if (!is_array($values)) {
|
||||||
|
echo json_encode(['status' => 'error', 'msg' => 'Nieprawidlowe dane formularza.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isTwofaEmailValidForEnabled($values['twofa_enabled'] ?? 0, (string)($values['twofa_email'] ?? ''))) {
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'error',
|
||||||
|
'msg' => 'Jesli wlaczono dwustopniowe uwierzytelnianie (2FA), pole "E-mail do 2FA" jest wymagane.',
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->repository->save(
|
||||||
|
(int)($values['id'] ?? 0),
|
||||||
|
(string)($values['login'] ?? ''),
|
||||||
|
$values['status'] ?? 0,
|
||||||
|
(string)($values['password'] ?? ''),
|
||||||
|
(string)($values['password_re'] ?? ''),
|
||||||
|
$values['admin'] ?? 1,
|
||||||
|
$values['twofa_enabled'] ?? 0,
|
||||||
|
(string)($values['twofa_email'] ?? '')
|
||||||
|
);
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = (int)\S::get('id');
|
||||||
|
$user = $this->normalizeUser($this->repository->find($userId));
|
||||||
|
$viewModel = $this->buildFormViewModel($user);
|
||||||
|
|
||||||
|
$result = $this->formHandler->handleSubmit($viewModel, $_POST);
|
||||||
|
if (!$result['success']) {
|
||||||
|
$_SESSION['form_errors'][$this->getFormId()] = $result['errors'];
|
||||||
|
echo json_encode(['success' => false, 'errors' => $result['errors']]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $result['data'];
|
||||||
|
$data['id'] = $userId;
|
||||||
|
$data['admin'] = 1;
|
||||||
|
|
||||||
|
if (!$this->isTwofaEmailValidForEnabled($data['twofa_enabled'] ?? 0, (string)($data['twofa_email'] ?? ''))) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'errors' => [
|
||||||
|
'twofa_email' => 'Pole "E-mail do 2FA" jest wymagane, gdy wlaczono 2FA.',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$duplicateLoginCheck = $this->repository->checkLogin((string)($data['login'] ?? ''), $userId);
|
||||||
|
if (($duplicateLoginCheck['status'] ?? '') !== 'ok') {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'errors' => ['login' => (string)($duplicateLoginCheck['msg'] ?? 'Podany login jest juz zajety.')],
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->repository->save(
|
||||||
|
(int)$data['id'],
|
||||||
|
(string)($data['login'] ?? ''),
|
||||||
|
(int)($data['status'] ?? 0),
|
||||||
|
(string)($data['password'] ?? ''),
|
||||||
|
(string)($data['password_re'] ?? ''),
|
||||||
|
1,
|
||||||
|
(int)($data['twofa_enabled'] ?? 0),
|
||||||
|
(string)($data['twofa_email'] ?? '')
|
||||||
|
);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => ($response['status'] ?? '') === 'ok',
|
||||||
|
'message' => (string)($response['msg'] ?? 'Zmiany zostaly zapisane.'),
|
||||||
|
'errors' => (($response['status'] ?? '') === 'ok') ? [] : ['general' => (string)($response['msg'] ?? 'Wystapil blad.')],
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user_edit(): string
|
||||||
|
{
|
||||||
|
$user = $this->normalizeUser($this->repository->find((int)\S::get('id')));
|
||||||
|
$validationErrors = $_SESSION['form_errors'][$this->getFormId()] ?? null;
|
||||||
|
if ($validationErrors) {
|
||||||
|
unset($_SESSION['form_errors'][$this->getFormId()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return \Tpl::view('users/user-edit', [
|
||||||
|
'form' => $this->buildFormViewModel($user, $validationErrors),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view_list(): string
|
||||||
|
{
|
||||||
|
$sortableColumns = ['login', 'status'];
|
||||||
|
$filterDefinitions = [
|
||||||
|
[
|
||||||
|
'key' => 'login',
|
||||||
|
'label' => 'Login',
|
||||||
|
'type' => 'text',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'status',
|
||||||
|
'label' => 'Aktywny',
|
||||||
|
'type' => 'select',
|
||||||
|
'options' => [
|
||||||
|
'' => '- aktywny -',
|
||||||
|
'1' => 'tak',
|
||||||
|
'0' => 'nie',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$listRequest = \admin\Support\TableListRequestFactory::fromRequest(
|
||||||
|
$filterDefinitions,
|
||||||
|
$sortableColumns,
|
||||||
|
'login'
|
||||||
|
);
|
||||||
|
|
||||||
|
$sortDir = $listRequest['sortDir'];
|
||||||
|
if (trim((string)\S::get('sort')) === '') {
|
||||||
|
$sortDir = 'ASC';
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->repository->listForAdmin(
|
||||||
|
$listRequest['filters'],
|
||||||
|
$listRequest['sortColumn'],
|
||||||
|
$sortDir,
|
||||||
|
$listRequest['page'],
|
||||||
|
$listRequest['perPage']
|
||||||
|
);
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
$lp = ($listRequest['page'] - 1) * $listRequest['perPage'] + 1;
|
||||||
|
foreach ($result['items'] as $item) {
|
||||||
|
$id = (int)$item['id'];
|
||||||
|
$login = trim((string)($item['login'] ?? ''));
|
||||||
|
$status = (int)($item['status'] ?? 0);
|
||||||
|
|
||||||
|
$rows[] = [
|
||||||
|
'lp' => $lp++ . '.',
|
||||||
|
'status' => $status === 1 ? 'tak' : '<span style="color: #FF0000;">nie</span>',
|
||||||
|
'login' => '<a href="/admin/users/user_edit/id=' . $id . '">' . htmlspecialchars($login, ENT_QUOTES, 'UTF-8') . '</a>',
|
||||||
|
'_actions' => [
|
||||||
|
[
|
||||||
|
'label' => 'Edytuj',
|
||||||
|
'url' => '/admin/users/user_edit/id=' . $id,
|
||||||
|
'class' => 'btn btn-xs btn-primary',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => 'Usun',
|
||||||
|
'url' => '/admin/users/user_delete/id=' . $id,
|
||||||
|
'class' => 'btn btn-xs btn-danger',
|
||||||
|
'confirm' => 'Na pewno chcesz usunac wybranego uzytkownika?',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = (int)$result['total'];
|
||||||
|
$totalPages = max(1, (int)ceil($total / $listRequest['perPage']));
|
||||||
|
|
||||||
|
$viewModel = new \admin\ViewModels\Common\PaginatedTableViewModel(
|
||||||
|
[
|
||||||
|
['key' => 'lp', 'label' => 'Lp.', 'class' => 'text-center', 'sortable' => false],
|
||||||
|
['key' => 'status', 'sort_key' => 'status', 'label' => 'Aktywny', 'class' => 'text-center', 'sortable' => true, 'raw' => true],
|
||||||
|
['key' => 'login', 'sort_key' => 'login', 'label' => 'Login', 'sortable' => true, 'raw' => true],
|
||||||
|
],
|
||||||
|
$rows,
|
||||||
|
$listRequest['viewFilters'],
|
||||||
|
[
|
||||||
|
'column' => $listRequest['sortColumn'],
|
||||||
|
'dir' => $sortDir,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'page' => $listRequest['page'],
|
||||||
|
'per_page' => $listRequest['perPage'],
|
||||||
|
'total' => $total,
|
||||||
|
'total_pages' => $totalPages,
|
||||||
|
],
|
||||||
|
array_merge($listRequest['queryFilters'], [
|
||||||
|
'sort' => $listRequest['sortColumn'],
|
||||||
|
'dir' => $sortDir,
|
||||||
|
'per_page' => $listRequest['perPage'],
|
||||||
|
]),
|
||||||
|
$listRequest['perPageOptions'],
|
||||||
|
$sortableColumns,
|
||||||
|
'/admin/users/view_list/',
|
||||||
|
'Brak danych w tabeli.',
|
||||||
|
'/admin/users/user_edit/',
|
||||||
|
'Dodaj uzytkownika'
|
||||||
|
);
|
||||||
|
|
||||||
|
return \Tpl::view('users/users-list', [
|
||||||
|
'viewModel' => $viewModel,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function list(): string
|
||||||
|
{
|
||||||
|
return $this->view_list();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function login_form(): string
|
||||||
|
{
|
||||||
|
return \Tpl::view('site/unlogged-layout');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function twofa(): string
|
||||||
|
{
|
||||||
|
return \Tpl::view('site/unlogged', [
|
||||||
|
'content' => \Tpl::view('users/user-2fa'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeUser(?array $user): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => (int)($user['id'] ?? 0),
|
||||||
|
'login' => (string)($user['login'] ?? ''),
|
||||||
|
'status' => (int)($user['status'] ?? 1),
|
||||||
|
'admin' => (int)($user['admin'] ?? 1),
|
||||||
|
'twofa_enabled' => (int)($user['twofa_enabled'] ?? 0),
|
||||||
|
'twofa_email' => (string)($user['twofa_email'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildFormViewModel(array $user, ?array $errors = null): FormEditViewModel
|
||||||
|
{
|
||||||
|
$userId = (int)($user['id'] ?? 0);
|
||||||
|
$isNew = $userId <= 0;
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'id' => $userId,
|
||||||
|
'login' => (string)($user['login'] ?? ''),
|
||||||
|
'status' => (int)($user['status'] ?? 1),
|
||||||
|
'twofa_enabled' => (int)($user['twofa_enabled'] ?? 0),
|
||||||
|
'twofa_email' => (string)($user['twofa_email'] ?? ''),
|
||||||
|
'password' => '',
|
||||||
|
'password_re' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
$fields = [
|
||||||
|
FormField::text('login', [
|
||||||
|
'label' => 'Login',
|
||||||
|
'required' => true,
|
||||||
|
]),
|
||||||
|
FormField::switch('status', [
|
||||||
|
'label' => 'Aktywny',
|
||||||
|
]),
|
||||||
|
FormField::switch('twofa_enabled', [
|
||||||
|
'label' => 'Dwustopniowe uwierzytelnianie (2FA)',
|
||||||
|
]),
|
||||||
|
FormField::email('twofa_email', [
|
||||||
|
'label' => 'E-mail do 2FA',
|
||||||
|
]),
|
||||||
|
FormField::password('password', [
|
||||||
|
'label' => 'Haslo',
|
||||||
|
'required' => $isNew,
|
||||||
|
'attributes' => ['minlength' => 5],
|
||||||
|
]),
|
||||||
|
FormField::password('password_re', [
|
||||||
|
'label' => 'Haslo - powtorz',
|
||||||
|
'required' => $isNew,
|
||||||
|
'attributes' => ['minlength' => 5],
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
|
$actionUrl = '/admin/users/user_save/' . ($isNew ? '' : ('id=' . $userId));
|
||||||
|
$actions = [
|
||||||
|
FormAction::save($actionUrl, '/admin/users/view_list/'),
|
||||||
|
FormAction::cancel('/admin/users/view_list/'),
|
||||||
|
];
|
||||||
|
|
||||||
|
return new FormEditViewModel(
|
||||||
|
$this->getFormId(),
|
||||||
|
$isNew ? 'Nowy uzytkownik' : 'Edycja uzytkownika',
|
||||||
|
$data,
|
||||||
|
$fields,
|
||||||
|
[],
|
||||||
|
$actions,
|
||||||
|
'POST',
|
||||||
|
$actionUrl,
|
||||||
|
'/admin/users/view_list/',
|
||||||
|
true,
|
||||||
|
[
|
||||||
|
'id' => $userId,
|
||||||
|
'admin' => 1,
|
||||||
|
],
|
||||||
|
null,
|
||||||
|
$errors
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getFormId(): string
|
||||||
|
{
|
||||||
|
return 'users-edit';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isTwofaEmailValidForEnabled($twofaEnabled, string $twofaEmail): bool
|
||||||
|
{
|
||||||
|
$enabled = ($twofaEnabled === 'on' || $twofaEnabled === 1 || $twofaEnabled === '1' || $twofaEnabled === true);
|
||||||
|
if (!$enabled) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim($twofaEmail) !== '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,9 +34,12 @@ class Site
|
|||||||
|
|
||||||
public static function special_actions()
|
public static function special_actions()
|
||||||
{
|
{
|
||||||
|
global $mdb;
|
||||||
|
|
||||||
$sa = \S::get('s-action');
|
$sa = \S::get('s-action');
|
||||||
$domain = preg_replace('/^www\./', '', $_SERVER['SERVER_NAME']);
|
$domain = preg_replace('/^www\./', '', $_SERVER['SERVER_NAME']);
|
||||||
$cookie_name = 'admin_remember_' . str_replace( '.', '-', $domain );
|
$cookie_name = 'admin_remember_' . str_replace( '.', '-', $domain );
|
||||||
|
$users = new \Domain\User\UserRepository($mdb);
|
||||||
|
|
||||||
switch ($sa)
|
switch ($sa)
|
||||||
{
|
{
|
||||||
@@ -45,11 +48,11 @@ class Site
|
|||||||
$login = \S::get('login');
|
$login = \S::get('login');
|
||||||
$pass = \S::get('password');
|
$pass = \S::get('password');
|
||||||
|
|
||||||
$result = \admin\factory\Users::logon($login, $pass);
|
$result = $users->logon($login, $pass);
|
||||||
|
|
||||||
if ( $result == 1 )
|
if ( $result == 1 )
|
||||||
{
|
{
|
||||||
$user = \admin\factory\Users::details($login);
|
$user = $users->details($login);
|
||||||
|
|
||||||
if ( $user['twofa_enabled'] == 1 )
|
if ( $user['twofa_enabled'] == 1 )
|
||||||
{
|
{
|
||||||
@@ -60,7 +63,7 @@ class Site
|
|||||||
'started' => time(),
|
'started' => time(),
|
||||||
] );
|
] );
|
||||||
|
|
||||||
if ( !\admin\factory\Users::send_twofa_code( (int)$user['id'] ) )
|
if ( !$users->sendTwofaCode( (int)$user['id'] ) )
|
||||||
{
|
{
|
||||||
\S::alert('Nie udało się wysłać kodu 2FA. Spróbuj ponownie.');
|
\S::alert('Nie udało się wysłać kodu 2FA. Spróbuj ponownie.');
|
||||||
\S::delete_session('twofa_pending');
|
\S::delete_session('twofa_pending');
|
||||||
@@ -73,7 +76,7 @@ class Site
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
$user = \admin\factory\Users::details($login);
|
$user = $users->details($login);
|
||||||
|
|
||||||
self::finalize_admin_login(
|
self::finalize_admin_login(
|
||||||
$user,
|
$user,
|
||||||
@@ -119,7 +122,7 @@ class Site
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$ok = \admin\factory\Users::verify_twofa_code((int)$pending['uid'], $code);
|
$ok = $users->verifyTwofaCode((int)$pending['uid'], $code);
|
||||||
if (!$ok)
|
if (!$ok)
|
||||||
{
|
{
|
||||||
\S::alert('Błędny lub wygasły kod.');
|
\S::alert('Błędny lub wygasły kod.');
|
||||||
@@ -128,7 +131,7 @@ class Site
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2FA OK — finalna sesja
|
// 2FA OK — finalna sesja
|
||||||
$user = \admin\factory\Users::details($pending['login']);
|
$user = $users->details($pending['login']);
|
||||||
|
|
||||||
self::finalize_admin_login(
|
self::finalize_admin_login(
|
||||||
$user,
|
$user,
|
||||||
@@ -152,7 +155,7 @@ class Site
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!\admin\factory\Users::send_twofa_code((int)$pending['uid'], true))
|
if (!$users->sendTwofaCode((int)$pending['uid'], true))
|
||||||
{
|
{
|
||||||
\S::alert('Kod można wysłać ponownie po krótkiej przerwie.');
|
\S::alert('Kod można wysłać ponownie po krótkiej przerwie.');
|
||||||
}
|
}
|
||||||
@@ -245,6 +248,13 @@ class Site
|
|||||||
'Filemanager' => function() {
|
'Filemanager' => function() {
|
||||||
return new \admin\Controllers\FilemanagerController();
|
return new \admin\Controllers\FilemanagerController();
|
||||||
},
|
},
|
||||||
|
'Users' => function() {
|
||||||
|
global $mdb;
|
||||||
|
|
||||||
|
return new \admin\Controllers\UsersController(
|
||||||
|
new \Domain\User\UserRepository( $mdb )
|
||||||
|
);
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return self::$newControllers;
|
return self::$newControllers;
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace admin\controls;
|
|
||||||
|
|
||||||
class Users
|
|
||||||
{
|
|
||||||
public static function user_delete()
|
|
||||||
{
|
|
||||||
if ( \admin\factory\Users::user_delete( \S::get( 'id' ) ) )
|
|
||||||
\S::alert( 'Użytkownik został usunięty.' );
|
|
||||||
header( 'Location: /admin/users/view_list/' );
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function user_save()
|
|
||||||
{
|
|
||||||
$values = json_decode( \S::get( 'values' ), true );
|
|
||||||
|
|
||||||
$response = \admin\factory\Users::user_save( $values['id'], $values['login'], $values['status'], $values['password'], $values['password_re'], $values['admin'], $values['twofa_enabled'], $values['twofa_email'] );
|
|
||||||
echo json_encode( $response );
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function user_edit()
|
|
||||||
{
|
|
||||||
return \admin\view\Users::user_edit(
|
|
||||||
\admin\factory\Users::user_details(
|
|
||||||
\S::get( 'id' )
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function view_list()
|
|
||||||
{
|
|
||||||
return \admin\view\Users::users_list();
|
|
||||||
}
|
|
||||||
|
|
||||||
static public function twofa() {
|
|
||||||
return \Tpl::view( 'site/unlogged', [
|
|
||||||
'content' => \Tpl::view( 'users/user-2fa' )
|
|
||||||
] );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace admin\factory;
|
|
||||||
|
|
||||||
class Users
|
|
||||||
{
|
|
||||||
|
|
||||||
static public function verify_twofa_code(int $userId, string $code): bool
|
|
||||||
{
|
|
||||||
$user = self::get_by_id( $userId );
|
|
||||||
if (!$user) return false;
|
|
||||||
|
|
||||||
if ((int)$user['twofa_failed_attempts'] >= 5)
|
|
||||||
{
|
|
||||||
return false; // zbyt wiele prób
|
|
||||||
}
|
|
||||||
|
|
||||||
// sprawdź ważność
|
|
||||||
if (empty($user['twofa_expires_at']) || time() > strtotime($user['twofa_expires_at']))
|
|
||||||
{
|
|
||||||
// wyczyść po wygaśnięciu
|
|
||||||
self::update_by_id($userId, [
|
|
||||||
'twofa_code_hash' => null,
|
|
||||||
'twofa_expires_at' => null,
|
|
||||||
]);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$ok = (!empty($user['twofa_code_hash']) && password_verify($code, $user['twofa_code_hash']));
|
|
||||||
if ($ok)
|
|
||||||
{
|
|
||||||
// sukces: czyścimy wszystko
|
|
||||||
self::update_by_id($userId, [
|
|
||||||
'twofa_code_hash' => null,
|
|
||||||
'twofa_expires_at' => null,
|
|
||||||
'twofa_sent_at' => null,
|
|
||||||
'twofa_failed_attempts' => 0,
|
|
||||||
'last_logged' => date('Y-m-d H:i:s'),
|
|
||||||
]);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// zła próba — inkrementacja
|
|
||||||
self::update_by_id($userId, [
|
|
||||||
'twofa_failed_attempts' => (int)$user['twofa_failed_attempts'] + 1,
|
|
||||||
'last_error_logged' => date('Y-m-d H:i:s'),
|
|
||||||
]);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static public function get_by_id(int $userId): ?array {
|
|
||||||
global $mdb;
|
|
||||||
return $mdb->get('pp_users', '*', ['id' => $userId]) ?: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static public function update_by_id(int $userId, array $data): bool {
|
|
||||||
global $mdb;
|
|
||||||
return (bool)$mdb->update('pp_users', $data, ['id' => $userId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
static public function send_twofa_code(int $userId, bool $resend = false): bool {
|
|
||||||
$user = self::get_by_id($userId);
|
|
||||||
|
|
||||||
if ( !$user )
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if ( (int)$user['twofa_enabled'] !== 1 )
|
|
||||||
return false;
|
|
||||||
|
|
||||||
$to = $user['twofa_email'] ?: $user['login'];
|
|
||||||
if (!filter_var($to, FILTER_VALIDATE_EMAIL))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if ( $resend && !empty( $user['twofa_sent_at'] ) ) {
|
|
||||||
$last = strtotime($user['twofa_sent_at']);
|
|
||||||
if ($last && (time() - $last) < 30)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$code = random_int(100000, 999999);
|
|
||||||
$hash = password_hash((string)$code, PASSWORD_DEFAULT);
|
|
||||||
|
|
||||||
self::update_by_id( $userId, [
|
|
||||||
'twofa_code_hash' => $hash,
|
|
||||||
'twofa_expires_at' => date('Y-m-d H:i:s', time() + 10 * 60), // 10 minut
|
|
||||||
'twofa_sent_at' => date('Y-m-d H:i:s'),
|
|
||||||
'twofa_failed_attempts' => 0,
|
|
||||||
] );
|
|
||||||
|
|
||||||
$subject = 'Twój kod logowania 2FA';
|
|
||||||
$body = "Twój kod logowania do panelu administratora: {$code}. Kod jest ważny przez 10 minut. Jeśli to nie Ty inicjowałeś logowanie – zignoruj tę wiadomość i poinformuj administratora.";
|
|
||||||
|
|
||||||
$sent = \S::send_email($to, $subject, $body);
|
|
||||||
|
|
||||||
if (!$sent) {
|
|
||||||
$headers = "MIME-Version: 1.0\r\n";
|
|
||||||
$headers .= "Content-type: text/plain; charset=UTF-8\r\n";
|
|
||||||
$headers .= "From: no-reply@" . ($_SERVER['HTTP_HOST'] ?? 'localhost') . "\r\n";
|
|
||||||
$encodedSubject = mb_encode_mimeheader($subject, 'UTF-8');
|
|
||||||
|
|
||||||
$sent = mail($to, $encodedSubject, $body, $headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $sent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function user_delete( $user_id )
|
|
||||||
{
|
|
||||||
global $mdb;
|
|
||||||
return $mdb -> delete( 'pp_users', [ 'id' => (int)$user_id ] );
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function user_details( $user_id )
|
|
||||||
{
|
|
||||||
global $mdb;
|
|
||||||
return $mdb -> get( 'pp_users', '*', [ 'id' => (int)$user_id ] );
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function user_save( $user_id = '', $login, $status, $password, $password_re, $admin, $twofa_enabled = 0, $twofa_email = '' )
|
|
||||||
{
|
|
||||||
global $mdb, $lang, $config;
|
|
||||||
|
|
||||||
if ( !$user_id )
|
|
||||||
{
|
|
||||||
if ( strlen( $password ) < 5 )
|
|
||||||
return $response = [ 'status' => 'error', 'msg' => 'Podane hasło jest zbyt krótkie.' ];
|
|
||||||
|
|
||||||
if ( $password != $password_re )
|
|
||||||
return $response = [ 'status' => 'error', 'msg' => 'Podane hasła są różne' ];
|
|
||||||
|
|
||||||
if ( $mdb -> insert( 'pp_users', [
|
|
||||||
'login' => $login,
|
|
||||||
'status' => $status == 'on' ? 1 : 0,
|
|
||||||
'admin' => $admin,
|
|
||||||
'password' => md5( $password ),
|
|
||||||
'twofa_enabled' => $twofa_enabled == 'on' ? 1 : 0,
|
|
||||||
'twofa_email' => $twofa_email
|
|
||||||
] ) )
|
|
||||||
{
|
|
||||||
return $response = [ 'status' => 'ok', 'msg' => 'Użytkownik został zapisany.' ];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if ( $password and strlen( $password ) < 5 )
|
|
||||||
return $response = [ 'status' => 'error', 'msg' => 'Podane hasło jest zbyt krótkie.' ];
|
|
||||||
|
|
||||||
if ( $password and $password != $password_re )
|
|
||||||
return $response = [ 'status' => 'error', 'msg' => 'Podane hasła są różne' ];
|
|
||||||
|
|
||||||
if ( $password )
|
|
||||||
$mdb -> update( 'pp_users', [
|
|
||||||
'password' => md5( $password )
|
|
||||||
], [
|
|
||||||
'id' => (int)$user_id
|
|
||||||
] );
|
|
||||||
|
|
||||||
$mdb -> update( 'pp_users', [
|
|
||||||
'login' => $login,
|
|
||||||
'admin' => $admin,
|
|
||||||
'status' => $status == 'on' ? 1 : 0,
|
|
||||||
'twofa_enabled' => $twofa_enabled == 'on' ? 1 : 0,
|
|
||||||
'twofa_email' => $twofa_email
|
|
||||||
], [
|
|
||||||
'id' => (int)$user_id
|
|
||||||
] );
|
|
||||||
|
|
||||||
return $response = [ 'status' => 'ok', 'msg' => 'Uzytkownik został zapisany.' ];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function check_login( $login, $user_id )
|
|
||||||
{
|
|
||||||
global $mdb;
|
|
||||||
|
|
||||||
if ( $mdb -> get( 'pp_users', 'login', [ 'AND' => [ 'login' => $login, 'id[!]' => (int)$user_id ] ] ) )
|
|
||||||
return $response = [ 'status' => 'error', 'msg' => 'Podany login jest już zajęty.' ];
|
|
||||||
|
|
||||||
return $response = [ 'status' => 'ok' ];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function logon( $login, $password )
|
|
||||||
{
|
|
||||||
global $mdb;
|
|
||||||
|
|
||||||
if ( !$mdb -> get( 'pp_users', '*', [ 'login' => $login ] ) )
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
if ( !$mdb -> get( 'pp_users', '*', [ 'AND' => [ 'login' => $login, 'status' => 1, 'error_logged_count[<]' => 5 ] ] ) )
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
if ( $mdb -> get( 'pp_users', '*', [ 'AND' => [ 'login' => $login, 'status' => 1, 'password' => md5( $password ) ] ] ) )
|
|
||||||
{
|
|
||||||
$mdb -> update( 'pp_users', [ 'last_logged' => date( 'Y-m-d H:i:s' ), 'error_logged_count' => 0 ], [ 'login' => $login ] );
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$mdb -> update( 'pp_users', [ 'last_error_logged' => date( 'Y-m-d H:i:s' ), 'error_logged_count[+]' => 1 ], [ 'login' => $login ] );
|
|
||||||
if ( $mdb -> get( 'pp_users', 'error_logged_count', [ 'login' => $login ] ) >= 5 )
|
|
||||||
{
|
|
||||||
$mdb -> update( 'pp_users', [ 'status' => 0 ], [ 'login' => $login ] );
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function details( $login )
|
|
||||||
{
|
|
||||||
global $mdb;
|
|
||||||
return $mdb -> get( 'pp_users', '*', [ 'login' => $login ] );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -5,14 +5,22 @@ class Page {
|
|||||||
|
|
||||||
public static function show()
|
public static function show()
|
||||||
{
|
{
|
||||||
global $user;
|
global $user, $mdb;
|
||||||
|
|
||||||
if ( $_GET['module'] == 'user' && $_GET['action'] == 'twofa' ) {
|
if ( $_GET['module'] == 'user' && $_GET['action'] == 'twofa' ) {
|
||||||
return \admin\controls\Users::twofa();
|
$controller = new \admin\Controllers\UsersController(
|
||||||
|
new \Domain\User\UserRepository( $mdb )
|
||||||
|
);
|
||||||
|
return $controller->twofa();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( !$user || !$user['admin'] )
|
if ( !$user || !$user['admin'] )
|
||||||
return \admin\view\Users::login_form();
|
{
|
||||||
|
$controller = new \admin\Controllers\UsersController(
|
||||||
|
new \Domain\User\UserRepository( $mdb )
|
||||||
|
);
|
||||||
|
return $controller->login_form();
|
||||||
|
}
|
||||||
|
|
||||||
$tpl = new \Tpl;
|
$tpl = new \Tpl;
|
||||||
$tpl -> content = \admin\Site::route();
|
$tpl -> content = \admin\Site::route();
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace admin\view;
|
|
||||||
|
|
||||||
class Users
|
|
||||||
{
|
|
||||||
public static function login_form()
|
|
||||||
{
|
|
||||||
$tpl = new \Tpl;
|
|
||||||
return $tpl -> render( 'site/unlogged-layout' );
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function users_list()
|
|
||||||
{
|
|
||||||
$tpl = new \Tpl;
|
|
||||||
return $tpl -> render( 'users/users-list' );
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function user_edit( $user )
|
|
||||||
{
|
|
||||||
$tpl = new \Tpl;
|
|
||||||
$tpl -> user = $user;
|
|
||||||
return $tpl -> render( 'users/user-edit' );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
406
tests/Unit/Domain/User/UserRepositoryTest.php
Normal file
406
tests/Unit/Domain/User/UserRepositoryTest.php
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
<?php
|
||||||
|
namespace Tests\Unit\Domain\User;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Domain\User\UserRepository;
|
||||||
|
|
||||||
|
class UserRepositoryTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testFindReturnsUserWhenExists(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->expects($this->once())
|
||||||
|
->method('get')
|
||||||
|
->with('pp_users', '*', ['id' => 7])
|
||||||
|
->willReturn(['id' => 7, 'login' => 'admin']);
|
||||||
|
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
$user = $repository->find(7);
|
||||||
|
|
||||||
|
$this->assertIsArray($user);
|
||||||
|
$this->assertSame(7, (int)$user['id']);
|
||||||
|
$this->assertSame('admin', $user['login']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFindReturnsNullWhenNotFound(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->method('get')->willReturn(false);
|
||||||
|
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
$this->assertNull($repository->find(123));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCheckLoginReturnsErrorWhenLoginIsTaken(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->expects($this->once())
|
||||||
|
->method('get')
|
||||||
|
->with('pp_users', 'login', [
|
||||||
|
'AND' => [
|
||||||
|
'login' => 'taken@example.com',
|
||||||
|
'id[!]' => 10,
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->willReturn('taken@example.com');
|
||||||
|
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
$response = $repository->checkLogin('taken@example.com', 10);
|
||||||
|
|
||||||
|
$this->assertSame('error', $response['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCheckLoginReturnsOkWhenAvailable(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->method('get')->willReturn(null);
|
||||||
|
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
$response = $repository->checkLogin('free@example.com', 5);
|
||||||
|
|
||||||
|
$this->assertSame('ok', $response['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSaveReturnsErrorForTooShortPasswordOnCreate(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
|
||||||
|
$response = $repository->save(0, 'admin@example.com', 'on', '1234', '1234', 1, 'on', 'admin@example.com');
|
||||||
|
|
||||||
|
$this->assertSame('error', $response['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSaveReturnsErrorForMismatchedPasswordsOnCreate(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
|
||||||
|
$response = $repository->save(0, 'admin@example.com', 'on', 'password1', 'password2', 1);
|
||||||
|
|
||||||
|
$this->assertSame('error', $response['status']);
|
||||||
|
$this->assertStringContainsString('rozne', $response['msg']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSaveCreatesUserWithNormalizedSwitches(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->expects($this->once())
|
||||||
|
->method('insert')
|
||||||
|
->with('pp_users', $this->callback(function (array $row): bool {
|
||||||
|
return $row['login'] === 'new@example.com'
|
||||||
|
&& $row['status'] === 1
|
||||||
|
&& $row['admin'] === 1
|
||||||
|
&& $row['twofa_enabled'] === 1
|
||||||
|
&& $row['twofa_email'] === '2fa@example.com'
|
||||||
|
&& $row['password'] === md5('secret5');
|
||||||
|
}))
|
||||||
|
->willReturn($this->createMock(\PDOStatement::class));
|
||||||
|
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
$response = $repository->save(0, 'new@example.com', 'on', 'secret5', 'secret5', 1, 'on', '2fa@example.com');
|
||||||
|
|
||||||
|
$this->assertSame('ok', $response['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSaveUpdatesExistingUserWithPassword(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->expects($this->exactly(2))
|
||||||
|
->method('update')
|
||||||
|
->withConsecutive(
|
||||||
|
[
|
||||||
|
'pp_users',
|
||||||
|
$this->callback(fn(array $d) => $d['password'] === md5('newpass5')),
|
||||||
|
['id' => 5],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'pp_users',
|
||||||
|
$this->callback(fn(array $d) => $d['login'] === 'user@example.com' && $d['status'] === 1),
|
||||||
|
['id' => 5],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
$response = $repository->save(5, 'user@example.com', 'on', 'newpass5', 'newpass5', 1);
|
||||||
|
|
||||||
|
$this->assertSame('ok', $response['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSaveUpdatesExistingUserWithoutPassword(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->expects($this->once())
|
||||||
|
->method('update')
|
||||||
|
->with(
|
||||||
|
'pp_users',
|
||||||
|
$this->callback(fn(array $d) => $d['login'] === 'user@example.com'),
|
||||||
|
['id' => 5]
|
||||||
|
);
|
||||||
|
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
$response = $repository->save(5, 'user@example.com', 'on', '', '', 1);
|
||||||
|
|
||||||
|
$this->assertSame('ok', $response['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSaveReturnsErrorForTooShortPasswordOnUpdate(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
|
||||||
|
$response = $repository->save(5, 'user@example.com', 1, '123', '123', 1);
|
||||||
|
|
||||||
|
$this->assertSame('error', $response['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSaveReturnsErrorForMismatchedPasswordsOnUpdate(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
|
||||||
|
$response = $repository->save(5, 'user@example.com', 1, 'password1', 'password2', 1);
|
||||||
|
|
||||||
|
$this->assertSame('error', $response['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeleteReturnsTrue(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->expects($this->once())
|
||||||
|
->method('delete')
|
||||||
|
->with('pp_users', ['id' => 3])
|
||||||
|
->willReturn($this->createMock(\PDOStatement::class));
|
||||||
|
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
$this->assertTrue($repository->delete(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeleteReturnsFalseOnFailure(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->method('delete')->willReturn(false);
|
||||||
|
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
$this->assertFalse($repository->delete(999));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDetailsReturnsUserByLogin(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->expects($this->once())
|
||||||
|
->method('get')
|
||||||
|
->with('pp_users', '*', ['login' => 'admin@shop.com'])
|
||||||
|
->willReturn(['id' => 1, 'login' => 'admin@shop.com']);
|
||||||
|
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
$user = $repository->details('admin@shop.com');
|
||||||
|
|
||||||
|
$this->assertIsArray($user);
|
||||||
|
$this->assertSame('admin@shop.com', $user['login']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDetailsReturnsNullWhenNotFound(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->method('get')->willReturn(false);
|
||||||
|
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
$this->assertNull($repository->details('nonexistent@shop.com'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLogonReturnsSuccessForValidCredentials(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
|
||||||
|
$mockDb->expects($this->exactly(3))
|
||||||
|
->method('get')
|
||||||
|
->withConsecutive(
|
||||||
|
['pp_users', '*', ['login' => 'admin@example.com']],
|
||||||
|
['pp_users', '*', ['AND' => ['login' => 'admin@example.com', 'status' => 1, 'error_logged_count[<]' => 5]]],
|
||||||
|
['pp_users', '*', ['AND' => ['login' => 'admin@example.com', 'status' => 1, 'password' => md5('password123')]]]
|
||||||
|
)
|
||||||
|
->willReturnOnConsecutiveCalls(
|
||||||
|
['id' => 5],
|
||||||
|
['id' => 5],
|
||||||
|
['id' => 5]
|
||||||
|
);
|
||||||
|
|
||||||
|
$mockDb->expects($this->once())
|
||||||
|
->method('update')
|
||||||
|
->with(
|
||||||
|
'pp_users',
|
||||||
|
$this->callback(function (array $data): bool {
|
||||||
|
return isset($data['last_logged']) && $data['error_logged_count'] === 0;
|
||||||
|
}),
|
||||||
|
['login' => 'admin@example.com']
|
||||||
|
);
|
||||||
|
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
$result = $repository->logon('admin@example.com', 'password123');
|
||||||
|
|
||||||
|
$this->assertSame(1, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLogonReturnsZeroForNonexistentUser(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->expects($this->once())
|
||||||
|
->method('get')
|
||||||
|
->with('pp_users', '*', ['login' => 'noone@example.com'])
|
||||||
|
->willReturn(false);
|
||||||
|
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
$this->assertSame(0, $repository->logon('noone@example.com', 'pass'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLogonReturnsNegativeOneForBlockedUser(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->expects($this->exactly(2))
|
||||||
|
->method('get')
|
||||||
|
->withConsecutive(
|
||||||
|
['pp_users', '*', ['login' => 'blocked@example.com']],
|
||||||
|
['pp_users', '*', ['AND' => ['login' => 'blocked@example.com', 'status' => 1, 'error_logged_count[<]' => 5]]]
|
||||||
|
)
|
||||||
|
->willReturnOnConsecutiveCalls(
|
||||||
|
['id' => 3, 'status' => 0],
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
$this->assertSame(-1, $repository->logon('blocked@example.com', 'pass'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVerifyTwofaCodeReturnsFalseForNonexistentUser(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->method('get')->willReturn(false);
|
||||||
|
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
$this->assertFalse($repository->verifyTwofaCode(999, '123456'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVerifyTwofaCodeReturnsFalseAfterMaxAttempts(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->method('get')->willReturn([
|
||||||
|
'id' => 1,
|
||||||
|
'twofa_failed_attempts' => 5,
|
||||||
|
'twofa_code_hash' => password_hash('123456', PASSWORD_DEFAULT),
|
||||||
|
'twofa_expires_at' => date('Y-m-d H:i:s', time() + 300),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
$this->assertFalse($repository->verifyTwofaCode(1, '123456'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVerifyTwofaCodeReturnsFalseForExpiredCode(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->method('get')->willReturn([
|
||||||
|
'id' => 1,
|
||||||
|
'twofa_failed_attempts' => 0,
|
||||||
|
'twofa_code_hash' => password_hash('123456', PASSWORD_DEFAULT),
|
||||||
|
'twofa_expires_at' => date('Y-m-d H:i:s', time() - 60),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$mockDb->expects($this->once())->method('update');
|
||||||
|
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
$this->assertFalse($repository->verifyTwofaCode(1, '123456'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testVerifyTwofaCodeReturnsTrueForValidCode(): void
|
||||||
|
{
|
||||||
|
$code = '654321';
|
||||||
|
$hash = password_hash($code, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->method('get')->willReturn([
|
||||||
|
'id' => 1,
|
||||||
|
'twofa_failed_attempts' => 0,
|
||||||
|
'twofa_code_hash' => $hash,
|
||||||
|
'twofa_expires_at' => date('Y-m-d H:i:s', time() + 300),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$mockDb->expects($this->once())
|
||||||
|
->method('update')
|
||||||
|
->with('pp_users', $this->callback(function (array $d): bool {
|
||||||
|
return $d['twofa_code_hash'] === null
|
||||||
|
&& $d['twofa_expires_at'] === null
|
||||||
|
&& $d['twofa_failed_attempts'] === 0;
|
||||||
|
}), ['id' => 1]);
|
||||||
|
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
$this->assertTrue($repository->verifyTwofaCode(1, $code));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSendTwofaCodeReturnsFalseWhen2FADisabled(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->method('get')->willReturn([
|
||||||
|
'id' => 1,
|
||||||
|
'twofa_enabled' => 0,
|
||||||
|
'login' => 'test@example.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
$this->assertFalse($repository->sendTwofaCode(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSendTwofaCodeReturnsFalseForInvalidEmail(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->method('get')->willReturn([
|
||||||
|
'id' => 1,
|
||||||
|
'twofa_enabled' => 1,
|
||||||
|
'twofa_email' => 'not-an-email',
|
||||||
|
'login' => 'also-not-email',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
$this->assertFalse($repository->sendTwofaCode(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUpdateByIdCallsDbUpdate(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->expects($this->once())
|
||||||
|
->method('update')
|
||||||
|
->with('pp_users', ['status' => 0], ['id' => 7])
|
||||||
|
->willReturn($this->createMock(\PDOStatement::class));
|
||||||
|
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
$this->assertTrue($repository->updateById(7, ['status' => 0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testListForAdminReturnsItemsAndTotal(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
|
||||||
|
$countStmt = $this->createMock(\PDOStatement::class);
|
||||||
|
$countStmt->expects($this->once())
|
||||||
|
->method('fetchAll')
|
||||||
|
->willReturn([[2]]);
|
||||||
|
|
||||||
|
$dataStmt = $this->createMock(\PDOStatement::class);
|
||||||
|
$dataStmt->expects($this->once())
|
||||||
|
->method('fetchAll')
|
||||||
|
->willReturn([
|
||||||
|
['id' => 2, 'login' => 'a@example.com', 'status' => 1],
|
||||||
|
['id' => 3, 'login' => 'b@example.com', 'status' => 0],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$mockDb->expects($this->exactly(2))
|
||||||
|
->method('query')
|
||||||
|
->willReturnOnConsecutiveCalls($countStmt, $dataStmt);
|
||||||
|
|
||||||
|
$repository = new UserRepository($mockDb);
|
||||||
|
$result = $repository->listForAdmin(['login' => '', 'status' => ''], 'login', 'ASC', 1, 15);
|
||||||
|
|
||||||
|
$this->assertSame(2, $result['total']);
|
||||||
|
$this->assertCount(2, $result['items']);
|
||||||
|
$this->assertSame('a@example.com', $result['items'][0]['login']);
|
||||||
|
}
|
||||||
|
}
|
||||||
131
tests/Unit/admin/Controllers/UsersControllerTest.php
Normal file
131
tests/Unit/admin/Controllers/UsersControllerTest.php
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<?php
|
||||||
|
namespace Tests\Unit\admin\Controllers;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use admin\Controllers\UsersController;
|
||||||
|
use Domain\User\UserRepository;
|
||||||
|
|
||||||
|
class UsersControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
private $mockRepository;
|
||||||
|
private $controller;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->mockRepository = $this->createMock(UserRepository::class);
|
||||||
|
$this->controller = new UsersController($this->mockRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConstructorAcceptsRepository(): void
|
||||||
|
{
|
||||||
|
$controller = new UsersController($this->mockRepository);
|
||||||
|
$this->assertInstanceOf(UsersController::class, $controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHasViewListMethod(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue(method_exists($this->controller, 'view_list'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHasUserEditMethod(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue(method_exists($this->controller, 'user_edit'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHasUserSaveMethod(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue(method_exists($this->controller, 'user_save'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHasUserDeleteMethod(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue(method_exists($this->controller, 'user_delete'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHasTwofaMethod(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue(method_exists($this->controller, 'twofa'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHasLoginFormMethod(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue(method_exists($this->controller, 'login_form'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testActionMethodReturnTypes(): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass($this->controller);
|
||||||
|
|
||||||
|
$this->assertEquals('string', (string)$reflection->getMethod('view_list')->getReturnType());
|
||||||
|
$this->assertEquals('string', (string)$reflection->getMethod('user_edit')->getReturnType());
|
||||||
|
$this->assertEquals('string', (string)$reflection->getMethod('login_form')->getReturnType());
|
||||||
|
$this->assertEquals('string', (string)$reflection->getMethod('twofa')->getReturnType());
|
||||||
|
$this->assertEquals('void', (string)$reflection->getMethod('user_save')->getReturnType());
|
||||||
|
$this->assertEquals('void', (string)$reflection->getMethod('user_delete')->getReturnType());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConstructorRequiresUserRepository(): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass(UsersController::class);
|
||||||
|
$constructor = $reflection->getConstructor();
|
||||||
|
$params = $constructor->getParameters();
|
||||||
|
|
||||||
|
$this->assertCount(1, $params);
|
||||||
|
$this->assertEquals('Domain\User\UserRepository', $params[0]->getType()->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNormalizeUserReturnsDefaultsForNull(): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass($this->controller);
|
||||||
|
$method = $reflection->getMethod('normalizeUser');
|
||||||
|
$method->setAccessible(true);
|
||||||
|
|
||||||
|
$result = $method->invoke($this->controller, null);
|
||||||
|
|
||||||
|
$this->assertSame(0, $result['id']);
|
||||||
|
$this->assertSame('', $result['login']);
|
||||||
|
$this->assertSame(1, $result['status']);
|
||||||
|
$this->assertSame(1, $result['admin']);
|
||||||
|
$this->assertSame(0, $result['twofa_enabled']);
|
||||||
|
$this->assertSame('', $result['twofa_email']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNormalizeUserCastsTypes(): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass($this->controller);
|
||||||
|
$method = $reflection->getMethod('normalizeUser');
|
||||||
|
$method->setAccessible(true);
|
||||||
|
|
||||||
|
$result = $method->invoke($this->controller, [
|
||||||
|
'id' => '15',
|
||||||
|
'login' => 'admin@test.com',
|
||||||
|
'status' => '0',
|
||||||
|
'admin' => '1',
|
||||||
|
'twofa_enabled' => '1',
|
||||||
|
'twofa_email' => 'twofa@test.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame(15, $result['id']);
|
||||||
|
$this->assertSame('admin@test.com', $result['login']);
|
||||||
|
$this->assertSame(0, $result['status']);
|
||||||
|
$this->assertSame(1, $result['admin']);
|
||||||
|
$this->assertSame(1, $result['twofa_enabled']);
|
||||||
|
$this->assertSame('twofa@test.com', $result['twofa_email']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNormalizeUserHandlesPartialData(): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass($this->controller);
|
||||||
|
$method = $reflection->getMethod('normalizeUser');
|
||||||
|
$method->setAccessible(true);
|
||||||
|
|
||||||
|
$result = $method->invoke($this->controller, ['id' => 3, 'login' => 'partial@test.com']);
|
||||||
|
|
||||||
|
$this->assertSame(3, $result['id']);
|
||||||
|
$this->assertSame('partial@test.com', $result['login']);
|
||||||
|
$this->assertSame(1, $result['status']);
|
||||||
|
$this->assertSame(1, $result['admin']);
|
||||||
|
$this->assertSame(0, $result['twofa_enabled']);
|
||||||
|
$this->assertSame('', $result['twofa_email']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,7 @@ if (!class_exists('S')) {
|
|||||||
public static function set_message($msg) {}
|
public static function set_message($msg) {}
|
||||||
public static function clear_redis_cache() {}
|
public static function clear_redis_cache() {}
|
||||||
public static function clear_product_cache($id) {}
|
public static function clear_product_cache($id) {}
|
||||||
|
public static function send_email($to, $subject, $body) { return true; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
updates/0.20/ver_0.253.zip
Normal file
BIN
updates/0.20/ver_0.253.zip
Normal file
Binary file not shown.
3
updates/0.20/ver_0.253_files.txt
Normal file
3
updates/0.20/ver_0.253_files.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
F: ../autoload/admin/controls/class.Users.php
|
||||||
|
F: ../autoload/admin/factory/class.Users.php
|
||||||
|
F: ../autoload/admin/view/class.Users.php
|
||||||
@@ -1,3 +1,10 @@
|
|||||||
|
<b>ver. 0.253 - 12.02.2026</b><br />
|
||||||
|
- UPDATE - modul `Users` w panelu admin w pelni przepiety na `Domain\\User\\UserRepository` + `admin\\Controllers\\UsersController`
|
||||||
|
- UPDATE - migracja widokow users z `grid/gridEdit` na nowe komponenty (`components/table-list`, `components/form-edit`)
|
||||||
|
- UPDATE - dodana walidacja warunkowa: przy wlaczonym 2FA pole `E-mail do 2FA` jest wymagane
|
||||||
|
- UPDATE - globalne ulepszenia `components/table-list` (kompaktowe filtry select/status i odstepy w formularzu paginacji)
|
||||||
|
- CLEANUP - usuniete legacy klasy users: `autoload/admin/controls/class.Users.php`, `autoload/admin/factory/class.Users.php`, `autoload/admin/view/class.Users.php`
|
||||||
|
<hr>
|
||||||
<b>ver. 0.252 - 10.02.2026</b><br />
|
<b>ver. 0.252 - 10.02.2026</b><br />
|
||||||
- UPDATE - migracja listy archiwum produktow do nowego komponentu tabeli (`components/table-list`) z filtrowaniem i paginacja
|
- UPDATE - migracja listy archiwum produktow do nowego komponentu tabeli (`components/table-list`) z filtrowaniem i paginacja
|
||||||
- UPDATE - banery i archiwum produktow: wydzielenie CSS/JS do osobnych widokow `*-custom-script.php`
|
- UPDATE - banery i archiwum produktow: wydzielenie CSS/JS do osobnych widokow `*-custom-script.php`
|
||||||
|
|||||||
Binary file not shown.
@@ -1,5 +1,5 @@
|
|||||||
<?
|
<?
|
||||||
$current_ver = 252;
|
$current_ver = 253;
|
||||||
|
|
||||||
for ($i = 1; $i <= $current_ver; $i++)
|
for ($i = 1; $i <= $current_ver; $i++)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user