Użytkownicy (User)
Ostatnia aktualizacja dokumentacji: 2026-02-26 Stan synchronizacji z kodem: Zsynchronizowany
1. Opis ogólny
Domena User zarządza kontami pracowników systemu zajmij.to — administratorów, supervisorów i operatorów (employees) działających w imieniu jednostek samorządu terytorialnego (JST). Odpowiada za pełny cykl życia konta: tworzenie użytkownika z przypisaniem roli, uwierzytelnianie JWT (z opcjonalnym jednorazowym kodem OTP), zarządzanie sesjami na wielu urządzeniach poprzez mechanizm refresh tokenów oraz bezpieczeństwo konta (wykrywanie podejrzanych logowań, alerty e-mail, rate limiting). Domena nie obsługuje klientów-mieszkańców — ci należą do domeny Client.
2. Architektura domeny
2.1 Modele danych
User (Domain\User\Model\User)
Główny model reprezentujący konto pracownika systemu. Implementuje JWTSubject (uwierzytelnianie JWT), używa HasUuids (UUID jako klucz główny), HasRoles (Spatie Permission), LogsActivity (Spatie ActivityLog) oraz ReceivesWelcomeNotification (Spatie Welcome Notification).
| Pole | Typ (PHP) | Cast | Opis |
id | string | string | UUID, klucz główny |
name | string | string | Imię i nazwisko, min. 5 znaków |
email | string | string | Adres e-mail, unikalny, służy do logowania |
email_verified_at | Carbon\|null | datetime:Y-m-d H:i:s | Data weryfikacji e-mail |
welcome_valid_until | Carbon\|null | datetime:Y-m-d H:i:s | Data ważności linku powitalnego |
password | string | string | Hasło hashowane bcrypt (ukryte w serializacji) |
remember_token | string\|null | string | Token "zapamiętaj mnie" (ukryty w serializacji) |
is_active | bool | boolean | Czy konto jest aktywne |
gender | string\|null | string | Płeć: male, female, none |
phone | string\|null | string | Numer telefonu, tylko cyfry, 7–15 znaków |
rodo_agreement | bool | boolean | Zgoda RODO |
statute_agreement | bool | boolean | Zgoda na regulamin systemu |
login_with_one_time_password | bool | boolean | Czy wymagane logowanie przez OTP |
created_at | Carbon | datetime:Y-m-d H:i:s | Data utworzenia |
updated_at | Carbon | datetime:Y-m-d H:i:s | Data ostatniej modyfikacji |
Relacje:
| Metoda | Typ | Opis |
role() | HasOneThrough | Jedna rola przez ModelHasRoles (Spatie) |
userOneTimePassword() | HasOne | Aktywny jednorazowy kod OTP |
organizationEmployees() | HasMany | Przypisania do organizacji |
complaintMessages() | HasMany | Wiadomości w reklamacjach (jako autor) |
Metody cache:
organizationIds(): array — zwraca ID organizacji użytkownika (cache array-store, rememberForever) managedItemIds(): array — zwraca ID obiektów zarządzanych przez użytkownika (cache array-store, rememberForever)
Observer: UserObserver::class (via atrybut #[ObservedBy])
UserOneTimePassword (Domain\User\Model\UserOneTimePassword)
Przechowuje aktywny jednorazowy kod logowania (OTP) dla użytkownika. Każdy użytkownik może mieć co najwyżej jeden aktywny OTP (poprzedni jest usuwany przed wygenerowaniem nowego).
| Pole | Typ (PHP) | Opis |
id | string | UUID, klucz główny |
user_id | string | FK → users.id (cascade delete) |
password | string | Kod OTP hashowany bcrypt (ukryty w serializacji) |
expired_at | Carbon\|null | Data wygaśnięcia kodu (TTL = 5 minut) |
created_at | Carbon | Data utworzenia |
updated_at | Carbon | Data modyfikacji |
Relacje: user(): BelongsTo → User
Observer: UserOneTimePasswordObserver::class
UserRefreshToken (Domain\User\Model\UserRefreshToken)
Przechowuje refresh tokeny (JWT) powiązane z konkretnymi urządzeniami/sesjami użytkownika. Token nie jest przechowywany wprost — zapisywany jest jedynie jego hash SHA-256.
| Pole | Typ (PHP) | Opis |
id | string | UUID, klucz główny |
user_id | string | FK → users.id (cascade delete) |
token_hash | string | SHA-256 hash refresh tokenu (unikalna, max 64 znaki) |
device | string\|null | User-Agent przeglądarki/aplikacji (max 255 znaków) |
ip_address | string\|null | Adres IP podczas tworzenia tokenu |
fingerprint | string\|null | SHA-256 hash User-Agent (powiązanie z urządzeniem) |
expires_at | Carbon\|null | Data wygaśnięcia tokenu |
revoked_at | Carbon\|null | Data unieważnienia tokenu (NULL = aktywny) |
created_at | Carbon | Data utworzenia |
updated_at | Carbon | Data modyfikacji |
Relacje: user(): BelongsTo → User
Indeksy bazy danych: (user_id, revoked_at), expires_at, token_hash
2.2 Diagram relacji
erDiagram
users {
uuid id PK
string name
string email
boolean is_active
enum gender
string phone
boolean rodo_agreement
boolean statute_agreement
boolean login_with_one_time_password
timestamp welcome_valid_until
timestamp email_verified_at
}
user_one_time_passwords {
uuid id PK
uuid user_id FK
string password
datetime expired_at
}
user_refresh_tokens {
uuid id PK
uuid user_id FK
string token_hash
string device
string ip_address
string fingerprint
datetime expires_at
datetime revoked_at
}
password_reset_tokens {
string email PK
string token
timestamp created_at
}
model_has_roles {
uuid role_id FK
string model_type
uuid model_id FK
}
roles {
uuid id PK
string name
}
organization_employees {
uuid user_id FK
uuid organization_id FK
}
users ||--o| user_one_time_passwords : "hasOne"
users ||--o{ user_refresh_tokens : "hasMany"
users ||--o{ model_has_roles : "hasMany"
model_has_roles }o--|| roles : "belongsTo"
users ||--o{ organization_employees : "hasMany"
2.3 Struktura tabel w bazie danych
Tabela users
| Nazwa kolumny | Typ | Nullable | Default | Opis |
id | uuid | Nie | — | Klucz główny (UUID) |
name | varchar | Nie | — | Imię i nazwisko |
email | varchar (unique) | Nie | — | Adres e-mail (login) |
email_verified_at | timestamp | Tak | NULL | Weryfikacja e-mail |
welcome_valid_until | timestamp | Tak | NULL | Ważność linku powitalnego |
password | varchar | Nie | — | Hasło (bcrypt) |
remember_token | varchar(100) | Tak | NULL | Token sesji webowej |
is_active | boolean | Nie | true | Aktywność konta |
gender | enum(male,female,none) | Nie | — | Płeć |
phone | varchar | Nie | — | Numer telefonu |
rodo_agreement | boolean | Nie | false | Zgoda RODO |
statute_agreement | boolean | Nie | false | Zgoda na regulamin |
login_with_one_time_password | boolean | Nie | false | Wymaganie OTP przy logowaniu |
created_at | timestamp | Tak | NULL | Data utworzenia |
updated_at | timestamp | Tak | NULL | Data modyfikacji |
Tabela user_one_time_passwords
| Nazwa kolumny | Typ | Nullable | Default | Opis |
id | uuid | Nie | — | Klucz główny (UUID) |
user_id | uuid (FK) | Nie | — | Powiązanie z users.id (cascade delete) |
password | varchar | Nie | — | Kod OTP (bcrypt hash) |
expired_at | datetime | Nie | — | Data wygaśnięcia (TTL 5 minut) |
created_at | timestamp | Tak | NULL | Data utworzenia |
updated_at | timestamp | Tak | NULL | Data modyfikacji |
Tabela user_refresh_tokens
| Nazwa kolumny | Typ | Nullable | Default | Opis |
id | uuid | Nie | — | Klucz główny (UUID) |
user_id | uuid (FK) | Nie | — | Powiązanie z users.id (cascade delete) |
token_hash | varchar(64) (unique) | Nie | — | SHA-256 hash tokenu |
device | varchar | Tak | NULL | User-Agent (max 255 znaków) |
ip_address | varchar | Tak | NULL | Adres IP |
fingerprint | varchar(64) | Tak | NULL | SHA-256 hash User-Agent (dodany migracją 2026-02-13) |
expires_at | datetime | Nie | — | Data wygaśnięcia |
revoked_at | datetime | Tak | NULL | Data unieważnienia (NULL = aktywny) |
created_at | timestamp | Tak | NULL | Data utworzenia |
updated_at | timestamp | Tak | NULL | Data modyfikacji |
Tabela password_reset_tokens
| Nazwa kolumny | Typ | Nullable | Opis |
email | varchar (PK) | Nie | E-mail użytkownika |
token | varchar | Nie | Token resetu hasła |
created_at | timestamp | Tak | Data wygenerowania |
Tabela sessions
| Nazwa kolumny | Typ | Nullable | Opis |
id | varchar (PK) | Nie | ID sesji |
user_id | uuid (FK) | Tak | Powiązanie z users.id (cascade delete) |
ip_address | varchar(45) | Tak | Adres IP |
user_agent | text | Tak | User-Agent |
payload | longtext | Nie | Payload sesji |
last_activity | integer (indexed) | Nie | Ostatnia aktywność (timestamp Unix) |
3. Endpointy API
Wszystkie endpointy przechodzą przez globalny middleware (require_app_client, global_database_transaction, app_mode, check_site_enable, set_locale, security_headers).
3.1 Endpointy publiczne (no-auth, prefix: /api/no-auth)
POST /api/no-auth/login
- Opis: Logowanie użytkownika. Zwraca JWT access token i ustawia HttpOnly cookie z refresh tokenem.
- Autoryzacja: Publiczny (throttle:
login) - Body (Request):
{
"email": "jan.kowalski@example.com",
"password": "TajneHaslo123!",
"one_time_password": "482391"
}
Walidacja (LoginUserAuthControllerRequest):
| Pole | Reguły | Opis |
email | required, string, email | Adres e-mail |
password | required, string | Hasło |
one_time_password | sometimes, nullable, string | Kod OTP (wymagany jeśli konto ma OTP włączone) |
- Response (sukces, HTTP 200):
{
"is_error": false,
"status_code": 200,
"data": {
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"user_name": "Jan Kowalski",
"user_role": "admin",
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"access_token_expires_at": "2026-02-26 15:30:00",
"message": "You have been logged in successfully",
"needed_one_time_password": false
}
}
Cookie: refresh_token (HttpOnly, Secure, SameSite=Strict, path=/api, TTL wg jwt.refresh_cookie_minutes — domyślnie 10080 minut = 7 dni)
| Kod HTTP | Sytuacja |
| 422 | Nieprawidłowe dane (email/hasło, konto nieaktywne, błędny OTP) |
| 429 | Zbyt wiele prób logowania (max 5 prób, rate limited) |
- Reguły biznesowe:
- Użytkownik musi istnieć w bazie (
users.email) - Konto musi być aktywne (
is_active = true) - Hasło musi być prawidłowe (bcrypt
Hash::check) - Jeśli
login_with_one_time_password = true: wymagane podanie kodu OTP - Jeśli OTP wygasł lub nie istnieje — nowy OTP jest automatycznie wysyłany na e-mail
- Logowanie z nowego urządzenia lub IP (sprawdzanie ostatnich 30 dni) wyzwala powiadomienie e-mail
UserNewLoginNotification - Rate limiting: 5 prób na klucz
{email}|{ip}, blokada z informacją o czasie oczekiwania
POST /api/no-auth/refresh-token
- Opis: Odświeżenie access tokenu przy użyciu refresh tokenu z cookie. Implementuje rotację tokenów — stary token jest unieważniany, nowy jest wystawiany.
- Autoryzacja: Publiczny (throttle:
refresh-token). Refresh token pobierany z cookie refresh_token. - Body (Request): Brak (token pobierany z HttpOnly cookie)
- Response (sukces, HTTP 200):
{
"is_error": false,
"status_code": 200,
"data": {
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"access_token_expires_at": "2026-02-26 15:45:00",
"message": "Token refreshed successfully"
}
}
Cookie: Nowy refresh_token (HttpOnly, Secure, SameSite=Strict)
| Kod HTTP | Sytuacja |
| 401 | Brak cookie, token nieważny lub wygasły |
| 500 | Błąd rotacji tokenu (RefreshTokenRotationException) |
- Reguły biznesowe:
- Token musi istnieć w tabeli
user_refresh_tokens (wyszukiwanie po token_hash = SHA256(token)) - Token musi być aktywny (
revoked_at IS NULL) i nie wygasły (expires_at > now()) - Jeśli token ma
fingerprint: weryfikowany SHA-256 hash User-Agent; niezgodność = unieważnienie wszystkich tokenów + alert e-mail UserSecurityAlertNotification z typem fingerprint_mismatch - Próba użycia już unieważnionego tokenu = unieważnienie WSZYSTKICH tokenów użytkownika + alert e-mail z typem
token_reuse - Po walidacji: stary token otrzymuje
revoked_at = now(), nowy token jest tworzony
POST /api/no-auth/reset-password
- Opis: Ustawia nowe hasło przy użyciu tokenu z linku resetującego.
- Autoryzacja: Publiczny (throttle:
public-store) - Body (Request):
{
"token": "abc123def456...",
"email": "jan.kowalski@example.com",
"password": "NoweHaslo123!",
"password_confirmation": "NoweHaslo123!"
}
Walidacja (NewPasswordUserAuthControllerRequest):
| Pole | Reguły |
token | required |
email | required, email |
password | required, confirmed, Password::defaults() |
- Response (sukces, HTTP 200):
{
"is_error": false,
"status_code": 200,
"data": "Password has been successfully changed"
}
| Kod HTTP | Sytuacja |
| 422 | Nieprawidłowy token, e-mail lub niezgodne hasła |
- Reguły biznesowe:
- Używa Laravel Password Broker (
Password::reset) - Po zresetowaniu hasła: ustawia nowy
password (bcrypt) i generuje nowy remember_token - Emituje event
PasswordReset
POST /api/no-auth/store-user-one-time-password
- Opis: Wysyła jednorazowy kod logowania (OTP) na e-mail użytkownika. Weryfikuje tożsamość przez parę e-mail + telefon.
- Autoryzacja: Publiczny (throttle:
public-store) - Body (Request):
{
"email": "jan.kowalski@example.com",
"phone": "512345678"
}
Walidacja (StoreUserOneTimePasswordControllerRequest):
| Pole | Reguły |
email | required, email, exists:users,email |
phone | required, exists:users,phone |
- Response (sukces, HTTP 200):
{
"is_error": false,
"status_code": 200,
"data": "A one-time login code has been sent to you"
}
- Response (konflikt, HTTP 409):
{
"is_error": false,
"status_code": 409,
"data": "You must wait 243 seconds before you can resend the one-time login code."
}
| Kod HTTP | Sytuacja |
| 404 | Nie znaleziono użytkownika z podaną parą email+phone |
| 409 | Aktualny OTP jeszcze nie wygasł — zwraca czas oczekiwania w sekundach |
| 422 | Błąd walidacji |
- Reguły biznesowe:
- Użytkownik musi istnieć i mieć
login_with_one_time_password = true (sprawdzane w SendUserOneTimePassword) - Jeśli aktywny OTP jeszcze nie wygasł — ponowne wysłanie jest zablokowane; zwracana jest liczba sekund do wygaśnięcia
- Kod OTP: losowa liczba całkowita z zakresu 111111–999999 (6 cyfr)
- TTL kodu OTP: 5 minut (wartość
UserEnumService::USER_OTP_TTL_MINUTES = '5') - Kod jest hashowany bcrypt przed zapisem w bazie
- Kanał dostarczenia: wyłącznie e-mail (
UserEnumService::ONE_TIME_PASSWORD_CHANNEL_MAIL = 'mail')
3.2 Endpointy uwierzytelnione (auth, middleware: auth:api, user_is_active, to_user_access, user_accepted_agreements)
GET /api/me
- Opis: Zwraca dane zalogowanego użytkownika.
- Autoryzacja: Role:
admin, supervisor, employee - Body (Request): Brak
- Response (sukces, HTTP 200):
{
"is_error": false,
"status_code": 200,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Jan Kowalski",
"email": "jan.kowalski@example.com",
"gender": "male",
"gender_lang": "Man",
"phone": "512345678",
"is_active": true,
"rodo_agreement": true,
"statute_agreement": true,
"login_with_one_time_password": false,
"created_at": "2026-01-15 10:00:00",
"updated_at": "2026-02-20 14:30:00",
"role": {
"id": "role-uuid",
"name": "admin"
}
}
}
POST /api/logout
- Opis: Wylogowuje użytkownika z bieżącej sesji — unieważnia JWT access token i refresh token z cookie.
- Autoryzacja: Role:
admin, supervisor, employee - Body (Request): Brak (refresh token pobierany z cookie)
- Response (sukces, HTTP 200):
{
"is_error": false,
"status_code": 200,
"data": "You have been logged out successfully"
}
Cookie: refresh_token usuwany (forget)
POST /api/logout-everywhere
- Opis: Wylogowuje użytkownika ze wszystkich urządzeń — unieważnia JWT access token i WSZYSTKIE aktywne refresh tokeny.
- Autoryzacja: Role:
admin, supervisor, employee - Body (Request): Brak
- Response (sukces, HTTP 200):
{
"is_error": false,
"status_code": 200,
"data": "You have been logged out from all devices"
}
Cookie: refresh_token usuwany (forget)
POST /api/forgot-password
- Opis: Wysyła link do resetowania hasła na podany adres e-mail.
- Autoryzacja: Role:
admin, supervisor - Body (Request):
{
"email": "jan.kowalski@example.com"
}
Walidacja (inline w kontrolerze): email — required, email
- Response (sukces, HTTP 200):
{
"is_error": false,
"status_code": 200,
"data": "Password reset link has been sent successfully"
}
| Kod HTTP | Sytuacja |
| 422 | Nie można wysłać linku (np. e-mail nie istnieje w systemie) |
GET /api/users
- Opis: Lista użytkowników z filtrowaniem i paginacją.
- Autoryzacja: Role:
admin, supervisor, employee - Query Parameters:
| Parametr | Opis |
filter[id] | Filtrowanie po ID (exact) |
filter[role.name] | Filtrowanie po nazwie roli (exact): admin, supervisor, employee |
filter[organizationEmployees.organization_id] | Filtrowanie po ID organizacji |
page | Numer strony (paginacja) |
per_page | Liczba wyników na stronę |
sort | Sortowanie (Spatie QueryBuilder) |
- Response (sukces, HTTP 200):
{
"is_error": false,
"status_code": 200,
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Jan Kowalski",
"email": "jan.kowalski@example.com",
"gender": "male",
"gender_lang": "Man",
"phone": "512345678",
"is_active": true,
"rodo_agreement": true,
"statute_agreement": true,
"login_with_one_time_password": false,
"created_at": "2026-01-15 10:00:00",
"updated_at": "2026-02-20 14:30:00",
"role": { "id": "role-uuid", "name": "admin" }
}
],
"meta": {
"current_page": 1,
"per_page": 15,
"total": 42
}
}
GET /api/users/{userId}
- Opis: Szczegóły konkretnego użytkownika.
- Autoryzacja: Role:
admin, supervisor, employee - Parametry URL:
userId — UUID użytkownika - Response (sukces, HTTP 200): Jak wyżej (pojedynczy obiekt
data) - Response (błędy):
| Kod HTTP | Sytuacja |
| 404 | Użytkownik nie istnieje |
POST /api/users
- Opis: Tworzy nowe konto użytkownika. Administrator musi podać rolę. Jeśli hasło nie jest podane, generowane jest losowe. Po utworzeniu wysyłana jest wiadomość powitalna oraz link do ustawienia hasła.
- Autoryzacja: Tylko rola:
admin - Body (Request):
{
"name": "Anna Nowak",
"email": "anna.nowak@example.com",
"password": "HasloOpcjonalne1!",
"password_confirmation": "HasloOpcjonalne1!",
"gender": "female",
"phone": "600123456",
"rodo_agreement": true,
"statute_agreement": true,
"login_with_one_time_password": false,
"role_id": "role-uuid-supervisor"
}
Walidacja (StoreUserControllerRequest):
| Pole | Reguły |
name | required, string, min:5 |
email | required, email, unique:users,email |
password | nullable, sometimes, confirmed, Password::min(8) |
gender | required, in:male,female,none |
phone | required, string, min:7, max:15, regex:/^[0-9]+$/ |
rodo_agreement | required, boolean |
statute_agreement | required, boolean |
login_with_one_time_password | required, boolean |
role_id | required, uuid, exists:roles,id |
- Response (sukces, HTTP 200):
{
"is_error": false,
"status_code": 200,
"data": "User has been successfully added"
}
- Reguły biznesowe:
- Jeśli
password nie podano — generowane jest losowe 8-znakowe hasło (bcrypt) - Rola jest przypisywana przez Spatie
assignRole - Link powitalny ważny 3 dni (
now()->addDays(3)) — wysyłany przez sendWelcomeNotification - Observer
UserObserver::created wyzwala Password::sendResetLink — wysyła link do ustawienia hasła - Notification
UserWelcomeNotification informuje o utworzeniu konta
PATCH /api/users/{userId}
- Opis: Aktualizuje dane użytkownika.
- Autoryzacja: Role:
admin, supervisor, employee - Parametry URL:
userId — UUID użytkownika - Body (Request):
{
"name": "Anna Nowak-Kowalska",
"email": "anna.nowak@example.com",
"password": "NoweHaslo123!",
"password_confirmation": "NoweHaslo123!",
"gender": "female",
"phone": "600123456",
"rodo_agreement": true,
"statute_agreement": true,
"login_with_one_time_password": true
}
Walidacja (UpdateUserControllerRequest):
| Pole | Reguły |
name | required, string, min:5 |
email | sometimes, nullable, email, unique:users,email,{userId} |
password | sometimes, confirmed, Password::min(8) |
gender | required, in:male,female,none |
phone | required, string, min:7, max:15, regex:/^[0-9]+$/ |
rodo_agreement | required, boolean |
statute_agreement | required, boolean |
login_with_one_time_password | required, boolean |
- Response (sukces, HTTP 200):
{
"is_error": false,
"status_code": 200,
"data": "User has been successfully updated"
}
- Reguły biznesowe:
- Jeśli
password podano — jest hashowane bcrypt przed zapisem - Observer
UserObserver::updated obsługuje zmianę is_active: jeśli konto zostało dezaktywowane (is_active = false), wszystkie refresh tokeny są natychmiast unieważniane
PATCH /api/users/{userId}/change-state
- Opis: Przełącza stan aktywności konta użytkownika (
is_active). - Autoryzacja: Tylko rola:
admin - Parametry URL:
userId — UUID użytkownika - Body (Request): Brak
- Response (sukces, HTTP 200):
{
"is_error": false,
"status_code": 200,
"data": "..."
}
| Kod HTTP | Sytuacja |
| 409 | Próba zmiany własnego konta (ChangeStateUserException) |
| 404 | Użytkownik nie istnieje |
- Reguły biznesowe:
- Administrator nie może zmienić stanu swojego własnego konta
- Dezaktywacja konta (
is_active = false) powoduje unieważnienie wszystkich refresh tokenów (via UpdatedUserObserver)
PATCH /api/users/{userId}/roles
- Opis: Przypisuje rolę użytkownikowi (zastępuje istniejącą).
- Autoryzacja: Tylko rola:
admin - Parametry URL:
userId — UUID użytkownika - Body (Request):
{
"role_id": "role-uuid-supervisor"
}
Walidacja (AssignRoleUserRoleControllerRequest):
| Pole | Reguły |
role_id | required, uuid, exists:roles,id |
- Response (sukces, HTTP 200):
{
"is_error": false,
"status_code": 200,
"data": "The role was successfully assigned"
}
| Kod HTTP | Sytuacja |
| 409 | Próba zmiany roli własnego konta (ChangeUserRoleException) |
| 404 | Użytkownik nie istnieje |
- Reguły biznesowe:
- Stare role są usuwane przez
syncRoles([]) przed przypisaniem nowej - Zmiana jest logowana do ActivityLog (stara rola vs. nowa rola)
- Administrator nie może zmienić własnej roli
DELETE /api/users/{userId}/roles
- Opis: Usuwa rolę z użytkownika.
- Autoryzacja: Tylko rola:
admin - Parametry URL:
userId — UUID użytkownika - Body (Request): Brak
- Response (sukces, HTTP 200):
{
"is_error": false,
"status_code": 200,
"data": "Role was successfully removed from user"
}
| Kod HTTP | Sytuacja |
| 409 | Próba usunięcia roli z własnego konta |
| 404 | Użytkownik nie istnieje |
GET /api/user-refresh-tokens
- Opis: Lista refresh tokenów bieżącego użytkownika (wszystkich urządzeń). Pole
is_current wskazuje, który token odpowiada bieżącej sesji. - Autoryzacja: Role:
admin, supervisor, employee - Query Parameters:
| Parametr | Opis |
filter[id] | Filtrowanie po ID (exact) |
filter[user_id] | Filtrowanie po user_id (exact) |
filter[device] | Filtrowanie po urządzeniu (exact) |
filter[ip_address] | Filtrowanie po adresie IP (exact) |
page | Paginacja |
per_page | Wyniki na stronę |
- Response (sukces, HTTP 200):
{
"is_error": false,
"status_code": 200,
"data": [
{
"id": "token-uuid",
"user_id": "user-uuid",
"device": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
"ip_address": "192.168.1.100",
"expires_at": "2026-03-05 10:00:00",
"revoked_at": null,
"created_at": "2026-02-26 10:00:00",
"updated_at": "2026-02-26 10:00:00",
"is_current": true
}
]
}
- Reguły biznesowe:
- Wyniki sortowane malejąco po
created_at is_current jest obliczane dynamicznie — porównywany jest SHA-256 hash tokenu z cookie z tokenem w bazie
DELETE /api/user-refresh-tokens/{userRefreshTokenId}
- Opis: Unieważnia konkretny refresh token (sesję).
- Autoryzacja: Role:
admin, supervisor, employee - Parametry URL:
userRefreshTokenId — UUID tokenu - Body (Request): Brak
- Response (sukces, HTTP 200):
{
"is_error": false,
"status_code": 200,
"data": "User refresh token has been revoked successfully"
}
| Kod HTTP | Sytuacja |
| 404 | Token nie istnieje lub już unieważniony (revoked_at IS NOT NULL) |
4. Logika biznesowa
4.1 Główne procesy
Proces tworzenia użytkownika (klasa CreateUser)
flowchart TD
A[POST /api/users] --> B[Walidacja StoreUserControllerRequest]
B --> C[CreateUser::__invoke]
C --> D{Hasło podane?}
D -->|Tak| E[bcrypt hasło]
D -->|Nie| F[Hash::make losowe 8 znaków]
E --> G[User::create]
F --> G
G --> H[Role::find + assignRole]
H --> I[sendWelcomeNotification - ważność 3 dni]
I --> J[UserObserver::created]
J --> K[Password::sendResetLink]
K --> L[Wysłanie emaila z linkiem do ustawienia hasła]
Proces logowania z OTP (klasy LoginUserAuthControllerRequest + LoginUserAuthController)
sequenceDiagram
participant K as Klient
participant R as LoginRequest
participant C as LoginController
participant E as Email
K->>R: POST /login {email, password}
R->>R: ensureIsNotRateLimited (max 5 prób)
R->>R: User::where(email) — znajdź użytkownika
R->>R: Sprawdź is_active
R->>R: Hash::check(password)
R->>R: Czy login_with_one_time_password?
alt OTP wymagane
R->>R: Czy OTP istnieje i nie wygasł?
alt OTP brak lub wygasł
R->>E: SendUserOneTimePassword
end
R->>R: Czy one_time_password podany?
alt OTP nie podany
R-->>K: 422 OTP required
end
R->>R: Hash::check(one_time_password, stored_hash) + expired_at
alt OTP nieprawidłowy
R-->>K: 422 Invalid OTP
end
end
R->>C: authenticate() OK
C->>C: IsNewDeviceOrIpUserRefreshToken (30 dni historii)
alt Nowe urządzenie/IP
C->>E: UserNewLoginNotification (async job)
end
C->>C: JWTAuth::fromUser → access token
C->>C: CreateUserRefreshToken → refresh token + cookie
C->>C: RemoveUserOneTimePassword
C-->>K: 200 {access_token, ...} + cookie refresh_token
Proces rotacji refresh tokenu (klasa RefreshTokenUserAuthController)
flowchart TD
A[POST /refresh-token] --> B{Cookie refresh_token?}
B -->|Nie| C[InvalidRefreshTokenException - 401]
B -->|Tak| D[ValidateUserRefreshToken]
D --> E[hash SHA-256 tokenu]
E --> F[FindByTokenHashUserRefreshToken]
F --> G{Token znaleziony?}
G -->|Nie| C
G -->|Tak| H{revoked_at IS NULL AND expires_at > now?}
H -->|Nie| I{Był unieważniony?}
I -->|Tak| J[RevokeAllUserRefreshTokens + SecurityAlert email: token_reuse]
I -->|Nie| C
H -->|Tak| K{Fingerprint istnieje?}
K -->|Tak| L{SHA-256 UserAgent match?}
L -->|Nie| M[RevokeAll + SecurityAlert email: fingerprint_mismatch]
L -->|Tak| N[JWTAuth payload check: refresh=true]
K -->|Nie| N
N --> O[RotateUserRefreshToken]
O --> P[RevokeOneUserRefreshToken stary]
P --> Q[CreateUserRefreshToken nowy]
Q --> R[JWTAuth::fromUser nowy access token]
R --> S[200 {access_token} + nowy cookie]
Proces dezaktywacji konta (observer UpdatedUserObserver)
flowchart TD
A[PATCH /users/id/change-state] --> B{userId == Auth::id?}
B -->|Tak| C[ChangeStateUserException - 409]
B -->|Nie| D[changeStateXController - przełącz is_active]
D --> E[UserObserver::updated]
E --> F{is_active zmieniło się?}
F -->|Tak i nowa wartość = false| G[RevokeAllUserRefreshTokens]
F -->|Inne zmiany| H[Koniec]
G --> H
4.2 Reguły biznesowe
- Każdy użytkownik ma dokładnie jedną rolę systemową (
admin, supervisor, employee). - Rola jest przypisywana przy tworzeniu konta — pole
role_id jest wymagane. - Zmiana roli zastępuje wszystkie poprzednie role użytkownika (
syncRoles([])). - Administrator nie może zmienić stanu aktywności ani roli własnego konta.
- Dezaktywacja konta natychmiast unieważnia wszystkie aktywne refresh tokeny.
- Kod OTP jest 6-cyfrową liczbą całkowitą (111111–999999), hashowaną bcrypt, ważną 5 minut.
- Jeśli aktywny OTP jeszcze nie wygasł, ponowne żądanie jest odrzucane z informacją o czasie oczekiwania.
- Refresh token jest przechowywany jako SHA-256 hash — nigdy w postaci jawnej.
- Każdy refresh token jest powiązany z urządzeniem (User-Agent) przez SHA-256 fingerprint.
- Próba użycia unieważnionego tokenu traktowana jest jako atak — wszystkie tokeny użytkownika są unieważniane.
- Niezgodność fingerprinta przy odświeżaniu tokenu traktowana jest jako kradzież tokenu — wszystkie tokeny są unieważniane.
- Logowanie z nowego urządzenia lub IP (w oknie 30 dni) wyzwala powiadomienie e-mail.
- Wygasłe OTP-y są usuwane przez komendę CRON
user-one-time-password:remove-expired (codziennie 00:40). - Wygasłe i unieważnione refresh tokeny (starsze niż 7 dni) są usuwane przez komendę CRON
user-refresh-token:cleanup-expired (codziennie 00:50). - Po przekroczeniu 5 nieudanych prób logowania (klucz
{email}|{ip}) konto jest tymczasowo blokowane (rate limiting). - Przy tworzeniu użytkownika: jeśli hasło nie jest podane, generowane jest losowe 8-znakowe, a link do ustawienia hasła jest wysyłany automatycznie.
4.3 Walidacje
Logowanie (LoginUserAuthControllerRequest)
| Pole | Reguły | Uwagi |
email | required, string, email | |
password | required, string | |
one_time_password | sometimes, nullable, string | Wymagany tylko gdy login_with_one_time_password = true |
Tworzenie użytkownika (StoreUserControllerRequest)
| Pole | Reguły |
name | required, string, min:5 |
email | required, email, unique:users,email |
password | nullable, sometimes, confirmed, Password::min(8) |
gender | required, in:male,female,none |
phone | required, string, min:7, max:15, regex:/^[0-9]+$/ |
rodo_agreement | required, boolean |
statute_agreement | required, boolean |
login_with_one_time_password | required, boolean |
role_id | required, uuid, exists:roles,id |
Aktualizacja użytkownika (UpdateUserControllerRequest)
| Pole | Reguły | Różnica vs. tworzenie |
name | required, string, min:5 | — |
email | sometimes, nullable, email, unique:users,email,{userId} | Ignoruje własny email |
password | sometimes, confirmed, Password::min(8) | Opcjonalne |
gender | required, in:male,female,none | — |
phone | required, string, min:7, max:15, regex:/^[0-9]+$/ | — |
rodo_agreement | required, boolean | — |
statute_agreement | required, boolean | — |
login_with_one_time_password | required, boolean | — |
role_id | Brak — edycja roli przez osobny endpoint | — |
Reset hasła (NewPasswordUserAuthControllerRequest)
| Pole | Reguły |
token | required |
email | required, email |
password | required, confirmed, Password::defaults() |
Żądanie OTP (StoreUserOneTimePasswordControllerRequest)
| Pole | Reguły |
email | required, email, exists:users,email |
phone | required, exists:users,phone |
Przypisanie roli (AssignRoleUserRoleControllerRequest)
| Pole | Reguły |
role_id | required, uuid, exists:roles,id |
5. Autoryzacja i uprawnienia
| Akcja | Admin | Supervisor | Employee | Warunek dodatkowy |
GET /me | Tak | Tak | Tak | — |
POST /logout | Tak | Tak | Tak | — |
POST /logout-everywhere | Tak | Tak | Tak | — |
POST /forgot-password | Tak | Tak | Nie | — |
GET /users | Tak | Tak | Tak | — |
GET /users/{id} | Tak | Tak | Tak | — |
POST /users | Tak | Nie | Nie | — |
PATCH /users/{id} | Tak | Tak | Tak | — |
PATCH /users/{id}/change-state | Tak | Nie | Nie | Nie można zmienić własnego konta |
PATCH /users/{id}/roles | Tak | Nie | Nie | Nie można zmienić własnej roli |
DELETE /users/{id}/roles | Tak | Nie | Nie | Nie można zmienić własnej roli |
GET /user-refresh-tokens | Tak | Tak | Tak | — |
DELETE /user-refresh-tokens/{id} | Tak | Tak | Tak | — |
Publiczne (no-auth):
| Akcja | Opis |
POST /login | Logowanie |
POST /refresh-token | Odświeżenie tokenu |
POST /reset-password | Ustawienie nowego hasła przez token |
POST /store-user-one-time-password | Wysłanie kodu OTP |
6. Eventy i efekty uboczne
Zdarzenia w obserwatorach (UserObserver)
| Zdarzenie modelu | Klasa obsługująca | Efekt |
User::created | CreatedUserObserver | Password::sendResetLink — wysłanie e-maila z linkiem do ustawienia hasła |
User::updated | UpdatedUserObserver | Jeśli is_active zmieniło się na false → RevokeAllUserRefreshTokens |
Eventy wyzwalane w kodzie
| Zdarzenie | Gdzie wyzwalane | Efekt |
Lockout (Illuminate) | LoginUserAuthControllerRequest::ensureIsNotRateLimited | Rate limiting lockout |
PasswordReset (Illuminate\Auth\Events) | SaveResetNewUserPassword | Wewnętrzny event Laravel po udanym resecie hasła |
Asynchroniczne powiadomienia (via SimpleSendNotificationJob)
| Sytuacja | Klasa notification | Kolejka |
| Logowanie z nowego urządzenia/IP | UserNewLoginNotification | notifications |
Wykrycie token_reuse | UserSecurityAlertNotification | notifications |
Wykrycie fingerprint_mismatch | UserSecurityAlertNotification | notifications |
| Wysłanie OTP | UserOneTimePasswordNotification | notifications |
Logowanie aktywności (ActivityLog)
- Zmiany roli użytkownika są logowane przez
AssignRemoveUserRole przez createCustomActivityLog z typem UPDATED_EVENT - Model
User implementuje LogsActivity (Spatie) z konfigurowanymi polami (przez getActivitylogOptionsConfig())
7. Notyfikacje
| Notification | Kiedy wysyłana | Odbiorca | Kanał | Treść |
UserWelcomeNotification | Po User::create | Nowy użytkownik (user->email) | mail | "Konto administracyjne zostało utworzone" — informuje o założeniu konta |
UserResetPasswordNotification | Przez Laravel Password Broker (sendResetLink) | Użytkownik | mail | Link do resetu hasła, ważny przez czas z auth.passwords.*.expire |
UserOneTimePasswordNotification | Przy logowaniu OTP lub żądaniu OTP | Użytkownik | mail | Kod 6-cyfrowy + data wygaśnięcia |
UserNewLoginNotification | Logowanie z nowego urządzenia lub IP (okno 30 dni) | Użytkownik | mail | Czas logowania, IP, urządzenie (max 100 znaków) |
UserSecurityAlertNotification | token_reuse lub fingerprint_mismatch | Użytkownik | mail | Alert bezpieczeństwa z IP i User-Agent; zalecenia działań |
Wszystkie powiadomienia rozszerzają CustomNotification z Infrastructure\Vendor\CustomNotification. Powiadomienia UserNewLoginNotification, UserOneTimePasswordNotification i UserSecurityAlertNotification są wysyłane asynchronicznie przez SimpleSendNotificationJob.
Powiadomienie powitalne (UserWelcomeNotification) jest wysyłane synchronicznie przez User::sendWelcomeNotification z użyciem metody simpleSendNotification('mail', [$this->email], ...) z TraitSupport.
8. Powiązania z innymi domenami
| Domena | Klasa/Model | Typ powiązania | Opis |
Role (Domain\Role) | Role, ModelHasRoles, RoleEnumService | HasOneThrough via ModelHasRoles | Każdy użytkownik ma przypisaną rolę (admin, supervisor, employee) |
Organization (Domain\Organization) | OrganizationEmployee | HasMany | Użytkownik może być pracownikiem jednej lub wielu organizacji |
Item (Domain\Item) | ItemManager | Pośrednie przez managedItemIds() | Użytkownik może zarządzać obiektami (item managers) |
Complaint (Domain\Complaint) | ComplaintMessage | HasMany | Użytkownik jest autorem wiadomości w reklamacjach |
ActivityLog (Domain\ActivityLog) | TraitActivityLog | Trait | Logowanie zmian w domenie User (w tym zmiany ról) |
Support (Support) | TraitSupport, CanResetPassword | Trait | Wspólna logika kontrolerów, reset hasła |
Notifiable (Infrastructure\Vendor\Notifiable) | — | Trait | Wysyłanie powiadomień |
9. Konfiguracja
Parametry JWT (plik config/jwt.php, zmienne środowiskowe)
| Parametr | Zmienna ENV | Wartość domyślna | Opis |
jwt.ttl | JWT_TTL | 15 | Czas życia access tokenu (minuty) |
jwt.refresh_ttl | JWT_REFRESH_TTL | 10080 | Czas życia refresh tokenu (minuty = 7 dni) |
jwt.refresh_cookie_name | JWT_REFRESH_COOKIE_NAME | refresh_token | Nazwa HttpOnly cookie |
jwt.refresh_cookie_minutes | JWT_REFRESH_COOKIE_MINUTES | 10080 | TTL cookie refresh tokenu (minuty = 7 dni) |
Parametry OTP (UserEnumService)
| Stała | Wartość | Opis |
USER_OTP_TTL_MINUTES | 5 | Czas ważności kodu OTP (minuty) |
ONE_TIME_PASSWORD_CHANNEL_MAIL | mail | Kanał dostarczenia OTP |
Dozwolone wartości płci (UserEnumService)
| Wartość | Opis |
male | Mężczyzna |
female | Kobieta |
none | Nie podano |
Wymagane zgody (UserEnumService::getUserNecessaryAgreements)
| Kolumna | Opis |
rodo_agreement | Zgoda RODO (wymagana przez middleware user_accepted_agreements) |
statute_agreement | Zgoda na regulamin systemu (wymagana przez middleware user_accepted_agreements) |
10. Komendy CRON
| Komenda | Sygnatura artisan | Harmonogram | Opis |
CleanupExpiredUserRefreshTokensCommand | user-refresh-token:cleanup-expired | Codziennie 00:50 | Usuwa tokeny: wygasłe (expires_at < now()) lub unieważnione starsze niż 7 dni |
RemoveExpiredUserOneTimePasswordsCommand | user-one-time-password:remove-expired | Codziennie 00:40 | Usuwa kody OTP, których expired_at < now() |
Logika czyszczenia refresh tokenów (CleanupExpiredUserRefreshTokensCommandAction):
UserRefreshToken::where('expires_at', '<', now())
->orWhere(function ($query) {
$query->whereNotNull('revoked_at')->where('updated_at', '<', now()->subDays(7));
})
->delete();
11. Znane ograniczenia i TODO
- Brak soft-delete — usunięcie użytkownika jest trwałe (cascade delete na sesje, OTP, refresh tokeny, pracownicy organizacji).
- Brak weryfikacji e-mail — kolumna
email_verified_at istnieje, ale logika weryfikacji e-mail nie jest w pełni zaimplementowana w domenie User. - Fingerprint backward compatibility — tokeny utworzone przed migracją
2026_02_13_160000_add_fingerprint_to_user_refresh_tokens.php nie mają pola fingerprint, co skutkuje pominięciem walidacji fingerprinta dla starych tokenów. - OTP tylko przez e-mail —
UserEnumService::ONE_TIME_PASSWORD_CHANNEL_MAIL jest jedynym obsługiwanym kanałem; kod w SendUserOneTimePassword rzuca SendUserOneTimePasswordException dla innych kanałów. - Brak obsługi 2FA przez TOTP/HOTP — system OTP oparty jest wyłącznie na e-mailu, bez wsparcia dla aplikacji uwierzytelniających.
- Brak endpointu DELETE /users/{id} — nie ma możliwości usunięcia użytkownika przez API.
- Cache organizationIds i managedItemIds — cache jest scope'owany do żądania (
array store, rememberForever), co oznacza brak inwalidacji między żądaniami; przy zmianie przypisań organizacji cache jest automatycznie odświeżany przy kolejnym żądaniu. - Throttle: login — limit 5 prób logowania zdefiniowany wewnątrz
LoginUserAuthControllerRequest::ensureIsNotRateLimited; konfiguracja rate limitera nie jest widoczna w routach (brak jawnego throttle:X,Y).