Files
orderPRO/.paul/phases/93-remember-me-login/93-01-PLAN.md
2026-04-12 01:35:19 +02:00

287 lines
11 KiB
Markdown

---
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
---
<objective>
## 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
</objective>
<context>
## 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
</context>
<acceptance_criteria>
## 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
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Migracja DB + UserRepository + remember token backend</name>
<files>
database/migrations/20260410_000081_add_remember_token_to_users.sql,
src/Modules/Users/UserRepository.php
</files>
<action>
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)
</action>
<verify>Migracja wykonuje się bez błędów; metody repozytorium istnieją i mają prepared statements</verify>
<done>AC-2 (baza), AC-6 (wielotoken) — infrastruktura DB gotowa</done>
</task>
<task type="auto">
<name>Task 2: AuthService + AuthController + AuthMiddleware — logika remember me + error fix</name>
<files>
src/Modules/Auth/AuthService.php,
src/Modules/Auth/AuthController.php,
src/Modules/Auth/AuthMiddleware.php
</files>
<action>
**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ę
</action>
<verify>
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)
</verify>
<done>AC-2, AC-3, AC-5, AC-6 satisfied</done>
</task>
<task type="auto">
<name>Task 3: Frontend — checkbox, error placeholder fix, style, tłumaczenia</name>
<files>
resources/views/auth/login.php,
resources/scss/login.scss,
resources/lang/pl.php
</files>
<action>
**login.php:**
1. Usunąć blok placeholder błędu (linie 13-16: `<?php else: ?>...<?php endif; ?>`).
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
<label class="form-field form-field--inline remember-field">
<input type="checkbox" name="remember" value="1" <?= !empty($oldRemember) ? 'checked' : '' ?>>
<span class="field-label"><?= $e($t('auth.login.remember_me')) ?></span>
</label>
```
**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
</action>
<verify>
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)
</verify>
<done>AC-1, AC-4 satisfied</done>
</task>
</tasks>
<boundaries>
## 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
</boundaries>
<verification>
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
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.paul/phases/93-remember-me-login/93-01-SUMMARY.md`
</output>