--- phase: 93-remember-me-login plan: 01 type: execute wave: 1 depends_on: [] files_modified: - database/migrations/20260410_000081_add_remember_token_to_users.sql - src/Modules/Auth/AuthService.php - src/Modules/Auth/AuthController.php - src/Modules/Auth/AuthMiddleware.php - src/Modules/Users/UserRepository.php - resources/views/auth/login.php - resources/scss/login.scss - resources/lang/pl.php autonomous: true delegation: off --- ## Goal Dodanie checkboxa "Zapamiętaj mnie" na stronie logowania z persistent cookie (30 dni) oraz uruchomienie działającego komunikatu błędu logowania (zamiast zaślepki placeholder). ## Purpose Użytkownicy muszą logować się przy każdej sesji przeglądarki. "Zapamiętaj mnie" pozwala na trwałe logowanie na danym urządzeniu przez 30 dni. Jednocześnie placeholder błędu logowania staje się funkcjonalny — wyświetla rzeczywiste komunikaty. ## Output - Migracja: kolumna `remember_token` w tabeli `users` - Backend: generowanie/walidacja tokena, cookie, auto-login z middleware - Frontend: checkbox w formularzu + usunięcie zaślepki błędu ## Project Context @.paul/PROJECT.md @.paul/ROADMAP.md @.paul/STATE.md ## Source Files @src/Modules/Auth/AuthService.php @src/Modules/Auth/AuthController.php @src/Modules/Auth/AuthMiddleware.php @src/Modules/Users/UserRepository.php @resources/views/auth/login.php @resources/scss/login.scss @resources/lang/pl.php @database/migrations/20260221_000001_create_users_table.sql ## AC-1: Checkbox "Zapamiętaj mnie" widoczny na formularzu logowania ```gherkin Given strona logowania /login jest wyświetlona When użytkownik widzi formularz logowania Then pomiędzy polem hasła a przyciskiem "Zaloguj" widoczny jest checkbox "Zapamiętaj mnie" ``` ## AC-2: Persistent login przez 30 dni po zaznaczeniu checkboxa ```gherkin Given użytkownik zaznaczył checkbox "Zapamiętaj mnie" When loguje się poprawnymi danymi Then przeglądarka otrzymuje cookie `remember_token` z max-age 30 dni (httponly, secure, samesite=lax) And token jest zapisany w bazie danych (users.remember_token jako hash) And po zamknięciu i otwarciu przeglądarki użytkownik jest nadal zalogowany ``` ## AC-3: Brak persistent login bez zaznaczenia checkboxa ```gherkin Given użytkownik NIE zaznaczył checkboxa "Zapamiętaj mnie" When loguje się poprawnymi danymi Then cookie `remember_token` NIE jest ustawiane And sesja wygasa po zamknięciu przeglądarki (standardowe zachowanie) ``` ## AC-4: Komunikat błędu logowania działa prawidłowo ```gherkin Given strona logowania /login jest wyświetlona When nie ma błędu logowania Then placeholder błędu jest ukryty (display:none, nie opacity) When użytkownik podaje złe dane i submittuje formularz Then wyświetla się rzeczywisty komunikat błędu (np. "Nieprawidłowy email lub hasło") And placeholder nie jest widoczny ``` ## AC-5: Wylogowanie czyści remember token ```gherkin Given użytkownik jest zalogowany z "Zapamiętaj mnie" When klika "Wyloguj" Then cookie `remember_token` jest usuwane And token w bazie danych jest kasowany (NULL) And użytkownik musi zalogować się ponownie ``` ## AC-6: Wielourządzeniowe logowanie działa niezależnie ```gherkin Given użytkownik zalogował się z "Zapamiętaj mnie" na urządzeniu A When loguje się z "Zapamiętaj mnie" na urządzeniu B Then oba urządzenia mają niezależne tokeny And wylogowanie na urządzeniu A nie wylogowuje z urządzenia B ``` Task 1: Migracja DB + UserRepository + remember token backend database/migrations/20260410_000081_add_remember_token_to_users.sql, src/Modules/Users/UserRepository.php 1. Utworzyć migrację dodającą kolumnę `remember_token VARCHAR(255) NULL` do tabeli `users`. - Kolumna przechowuje HASH tokena (nie plaintext) — `hash('sha256', $token)` - NULL = brak aktywnego remember me 2. W `UserRepository` dodać metody: - `updateRememberToken(int $userId, ?string $tokenHash): void` — UPDATE users SET remember_token = :token WHERE id = :id - `findByRememberToken(string $tokenHash): ?array` — SELECT id, name, email FROM users WHERE remember_token = :token LIMIT 1 Avoid: NIE przechowywać plaintext tokena w DB — zawsze hash('sha256', $token) Migracja wykonuje się bez błędów; metody repozytorium istnieją i mają prepared statements AC-2 (baza), AC-6 (wielotoken) — infrastruktura DB gotowa Task 2: AuthService + AuthController + AuthMiddleware — logika remember me + error fix src/Modules/Auth/AuthService.php, src/Modules/Auth/AuthController.php, src/Modules/Auth/AuthMiddleware.php **AuthService:** 1. Dodać stałą `REMEMBER_COOKIE = 'remember_token'` i `REMEMBER_DAYS = 30` 2. Metoda `createRememberToken(int $userId): string`: - Generuje losowy token: `bin2hex(random_bytes(32))` - Zapisuje hash w DB: `$this->users->updateRememberToken($userId, hash('sha256', $token))` - Ustawia cookie: `setcookie('remember_token', $token, [opcje 30 dni, httponly, secure, samesite=lax, path=/])` - Zwraca token (do ewentualnego użycia) 3. Metoda `loginFromRememberToken(): bool`: - Odczytuje `$_COOKIE['remember_token']` - Jeśli brak — return false - Hashuje: `hash('sha256', $cookieToken)` - Szuka usera: `$this->users->findByRememberToken($hash)` - Jeśli znaleziony — regeneruje sesję, ustawia $_SESSION['auth_user'], return true - Jeśli nie — usuwa cookie, return false 4. Metoda `clearRememberToken(int $userId): void`: - `$this->users->updateRememberToken($userId, null)` - Usuwa cookie (setcookie z max-age 0) 5. W istniejącej `logout()`: wywołać `clearRememberToken` dla aktualnego usera przed unset sesji **AuthController:** 1. W `login()`: po udanym `$this->auth->attempt()`: - Sprawdzić `$request->input('remember')` - Jeśli truthy → `$this->auth->createRememberToken($userId)` - Pobrać userId z `$this->auth->user()['id']` po attempt 2. Przekazywać `remember` checkbox state z powrotem do formularza w razie błędu (Flash::set('old_remember')) 3. W `showLogin()`: przekazać `oldRemember` z Flash do widoku **AuthMiddleware:** 1. W `__invoke()`: jeśli `$this->auth->check()` zwraca false: - Przed redirect na /login, spróbować `$this->auth->loginFromRememberToken()` - Jeśli sukces → kontynuować normalnie ($next) - Jeśli porażka → redirect /login jak dotychczas Avoid: - NIE przechowywać plaintext tokena w DB - NIE ustawiać cookie bez httponly i samesite - W logout() NAJPIERW pobrać user ID, POTEM czyścić sesję 1. Login z remember=on → cookie `remember_token` w przeglądarce (30 dni) 2. Login bez remember → brak cookie 3. Po zamknięciu przeglądarki i otwarciu → auto-login z cookie 4. Logout → cookie usunięte + token NULL w DB 5. Błąd logowania → wyświetla komunikat (nie placeholder) AC-2, AC-3, AC-5, AC-6 satisfied Task 3: Frontend — checkbox, error placeholder fix, style, tłumaczenia resources/views/auth/login.php, resources/scss/login.scss, resources/lang/pl.php **login.php:** 1. Usunąć blok placeholder błędu (linie 13-16: `...`). Zamienić na: jeśli `$errorMessage` nie pusty → wyświetl alert; w przeciwnym razie — NIC (brak zaślepki). 2. Dodać checkbox "Zapamiętaj mnie" między polem hasła a przyciskiem submit: ```php ``` **login.scss:** 1. Usunąć regułę `.login-alert-placeholder` (opacity: 0.56 — już niepotrzebna) 2. Dodać style dla `.remember-field`: ```scss .remember-field { display: flex; align-items: center; gap: 8px; input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--c-primary, #4f6ef7); cursor: pointer; } .field-label { font-weight: 400; cursor: pointer; user-select: none; } } ``` **pl.php:** 1. Dodać klucz `'remember_me' => 'Zapamiętaj mnie'` w sekcji `auth.login` 2. Usunąć lub zostawić klucz `error_placeholder` (nie jest już używany w widoku) Po zmianach SCSS: zbudować CSS komendą projektu (jeśli build pipeline istnieje) lub skopiować do public/assets/css/ Avoid: NIE dodawać nowych natywnych alert()/confirm() — formularz działa przez POST redirect 1. Strona /login wyświetla checkbox "Zapamiętaj mnie" 2. Bez błędu — brak żadnego komunikatu (nie ma zaślepki) 3. Po błędnym logowaniu — wyświetla się czerwony alert z treścią błędu 4. Checkbox zachowuje stan po błędnym logowaniu (old_remember) AC-1, AC-4 satisfied ## DO NOT CHANGE - src/Core/Support/Session.php (zarządzanie sesją — stabilne) - src/Core/Security/Csrf.php (token CSRF — stabilne) - resources/views/layouts/auth.php (layout auth — bez zmian) - Inne moduły (Orders, Settings, Accounting itp.) ## SCOPE LIMITS - Nie implementujemy resetowania hasła - Nie implementujemy "wyloguj ze wszystkich urządzeń" (poza scope) - Nie zmieniamy struktury sesji (SESSION_USER_KEY format) - Nie dodajemy nowych zależności npm/composer Before declaring plan complete: - [ ] Migracja dodaje kolumnę `remember_token` do `users` - [ ] Login z checkbox → cookie 30-dniowe + hash w DB - [ ] Login bez checkbox → brak cookie - [ ] Zamknięcie/otwarcie przeglądarki → auto-login działa - [ ] Logout → cookie usunięte + DB token NULL - [ ] Błąd logowania → widoczny komunikat - [ ] Brak błędu → brak zaślepki/placeholdera - [ ] SCSS zbudowane do CSS - [ ] Tłumaczenie pl.php zaktualizowane - [ ] All acceptance criteria met - All tasks completed - All verification checks pass - No errors or warnings introduced - Użytkownik może logować się z persistent cookie na wielu urządzeniach niezależnie - Komunikaty błędów logowania działają poprawnie After completion, create `.paul/phases/93-remember-me-login/93-01-SUMMARY.md`