Skip to main content

Ustawianie i resetowanie hasła

Wszystkie ścieżki, w których klient dostaje mailem link do ustawienia hasła: zaproszenie do nowego konta, konto bez hasła (rejestracja przez SMS lub Google) oraz zwykłe „nie pamiętam hasła".

👤 Instrukcja dla klienta​

Konto założone przez klub​

Po założeniu konta przez pracownika klient dostaje maila „Ustaw hasło do swojego konta". W środku jest przycisk Ustaw hasło, a pod nim ten sam link w postaci tekstowej — do skopiowania, gdy klient czyta pocztę w programie, który blokuje przyciski. Link jest ważny 24 godziny.

Gdyby program pocztowy w ogóle nie pokazał formatowanej wiadomości, ten sam adres jest w tekstowej wersji maila — klient nie zostaje bez działającego linku.

Konto bez hasła (rejestracja przez telefon lub Google)​

Klient wpisuje na /auth/login swój adres e-mail i klika Kontynuuj. Jeśli konto nie ma jeszcze własnego hasła, system od razu wysyła maila z linkiem i pokazuje ekran „Sprawdź skrzynkę". Gdy wysyłka się nie uda, ekran mówi o tym wprost i proponuje ponowienie (przycisk odblokowuje się po 60 sekundach) albo logowanie numerem telefonu lub kontem Google.

Nie pamiętam hasła​

Link „Nie pamiętasz hasła?" prowadzi na /auth/recover. Klient wybiera kanał:

  • Kod SMS — sześciocyfrowy kod na numer z profilu, hasło ustawia się na miejscu,
  • Link e-mail — mail „Prośba o zmianę hasła" z linkiem ważnym 24 godziny.

Jeśli maila nie udało się wysłać, klient dostaje czerwony komunikat zamiast ekranu „Sprawdź skrzynkę".

Po 24 godzinach link prowadzi na ekran z informacją o nieaktualnym linku. Klient wraca na ekran logowania i wpisuje adres e-mail jeszcze raz — nowy mail wychodzi automatycznie.

Zmiana hasła z własnego profilu​

Zalogowany klient zmienia hasło w Profil → Dane osobowe → Bezpieczeństwo. Sekcja pojawia się tylko wtedy, gdy konto ma własne hasło — konto założone wyłącznie przez Google lub Apple najpierw ustawia hasło linkiem z maila.

Formularz prosi o obecne hasło oraz dwukrotnie o nowe. Po zapisaniu klient zostaje zalogowany na bieżącym urządzeniu, a wszystkie pozostałe sesje są wylogowywane. Błędne obecne hasło zatrzymuje zapis i podświetla pole zamiast pokazywać potwierdzenie.

🛠️ Dokumentacja techniczna​

Przepływ​

Dlaczego requestResetMail​

Better Auth odpowiada na /request-password-reset zawsze status: true — nieznany adres i awaria SendGrida wyglądają identycznie jak sukces (callback sendResetPassword jest wołany przez runInBackgroundOrAwait, który łyka wyjątek do logu). Dlatego callback zapisuje faktyczny wynik przez recordResetMailDelivery, a lib/auth-reset-mail.ts odczytuje go zaraz po wywołaniu i zwraca:

reasonZnaczenie
sentSendGrid przyjął wiadomość
no_accountBetter Auth nie znalazł konta — nic nie poszło
send_failedWysyłka rzuciła wyjątkiem

Bez tego każdy ekran mówił „wysłaliśmy maila" niezależnie od tego, czy cokolwiek wyszło.

Wybór szablonu​

sendResetPassword (lib/auth.ts) rozpoznaje zaproszenie po redirectTo kończącym się na /auth/set-password albo po nagłówku x-is-invitation: true. Zaproszenie → mails/auth-invitation.html, reset → mails/auth-reset-password.html. Oba szablony zawierają przycisk oraz ten sam adres jako widoczny link. Obok każdego pliku .html leży .txt z tą samą treścią — to wersja text/plain wysyłana w tej samej wiadomości.

Treść zaproszenia jest celowo neutralna („ustaw swoje hasło"), bo ten sam mail trafia zarówno do świeżo założonych kont, jak i do kont, które istnieją od dawna, a nigdy nie miały własnego hasła.

Klienci zgłaszali, że w części skrzynek ani przycisk, ani link nie reagują na kliknięcie. Powód był w samym szablonie: nagłówek maila miał .header::before z position: absolute i rozmiarem top/left/right/bottom: 0, czyli warstwę przykrywającą nagłówek. Trzymało ją w ryzach wyłącznie position: relative na .header — a sanitizery webmaili (wp.pl, o2.pl, Interia) usuwają position z arkusza wiadomości. Bez tej deklaracji warstwa rozciągała się na całą wiadomość i przechwytywała każde kliknięcie: mail wyglądał normalnie, a przycisk i link były martwe. __tests__/lib/auth-emails.test.ts pilnuje, żeby ta klasa CSS nie wróciła do szablonów.

Przy okazji szablony przeszły na układ tabelaryczny z inline'owymi stylami (zamiast div + arkusz w <head>), bo tylko taki przechodzi przez sanitizery w całości. Zniknęły @import fontu, box-shadow, inline-flex, transition i overflow: hidden, doszedł przycisk VML dla Outlooka, a obrazek logo ładuje się po https. Usunięty został też link [unsubscribe] — w tych transakcyjnych mailach nie był podstawiany przez SendGrida, więc zostawał w treści jako nieprawidłowy adres, co podbija ocenę phishingową (a wiadomość z taką oceną potrafi mieć wyłączone wszystkie linki).

Wersja tekstowa​

lib/auth-emails.ts renderuje oba warianty i przekazuje tekst jako textContent; lib/email.ts wkłada go do content[] przed częścią HTML, bo SendGrid wymaga rosnącej kolejności MIME. Dzięki temu klient poczty, który nie renderuje naszego HTML-a albo wycina z niego kotwice, wciąż pokazuje goły adres.

Wartości podstawiane w szablony HTML są escapowane (&, <, >, ") — bez tego ampersand w linku rozjeżdżał atrybut href po sanitizacji. Wersja tekstowa dostaje wartości surowe.

Ważność linku​

resetPasswordTokenExpiresIn w lib/auth.ts to 24 godziny (domyślne Better Auth to 1 godzina — za mało dla zaproszenia, które klient otwiera przy najbliższym zaglądnięciu do skrzynki). Wartość obowiązuje oba szablony, więc zmiana wymaga poprawienia zdania o ważności w obu plikach mails/.

Punkty wejścia​

MiejsceFunkcja
lib/actions/users.ts → inviteUserzaproszenie po createUser
lib/set-password-invite.tskonto bez hasła (login, trial)
lib/actions/users.ts → resetPasswordkanał e-mail w /auth/recover

resendSetPasswordInvitation wysyła wyłącznie do kont bez konta credential — konto z hasłem musi przejść zwykłą ścieżką odzyskiwania, więc endpointu nie da się użyć do zasypywania maili istniejącym użytkownikom.

Zmiana hasła z profilu​

components/forms/userProfile/userPasswordForm.tsx woła authClient.changePassword({ currentPassword, newPassword, revokeOtherSessions: true }), czyli endpoint POST /api/auth/change-password. Hasło żyje w tabeli account (providerId = 'credential'), której updateUser ani updateUserInDb nie dotykają — formularz wysyłał wcześniej password w zwykłej aktualizacji profilu, więc pole było po cichu pomijane, a toast i tak mówił o sukcesie.

Widoczność sekcji steruje showPasswordSection w userProfileModern.tsx, wyliczone z hasPassword (lib/actions/users-db.ts → providery konta).

Błąd INVALID_PASSWORD z Better Auth oznacza złe obecne hasło i jest mapowany na komunikat przy polu; każdy inny błąd kończy się ogólnym komunikatem, bez czyszczenia formularza.