W boilerplacie, którego używam do zaczynania projektów, commit integrujący pierwszego dostawcę auth i ten, który zastępuje go drugim, dzielą dwa dni. Dwa dni. Pierwszy wstawiał middleware dla chronionych tras i ekran logowania; drugi wyrywał to w całości i wstawiał inny SDK z jego własnymi stronami sign-in i sign-up. Od tamtej pory ten sam szablon przeszedł przez dwóch kolejnych dostawców i skończył, prawie rok później, z middlewarem napisanym ręcznie.
To nie pech. To wzorzec i powtórzyłem go w co najmniej czterech repozytoriach.
Zawsze zaczynam od zarządzanego dostawcy i zawsze z tego samego powodu
Pierwszego dnia projektu nie chcę pisać hashowania haseł, weryfikacji maila ani flow OAuth z Google. Instaluję SDK, ustawiam dwie zmienne środowiskowe i w jedno popołudnie jest logowanie. Tego dnia to właściwa decyzja: projekt jeszcze nie wie, czy będzie istniał za miesiąc.
Problem w tym, że ten SDK nie siedzi grzecznie na brzegu aplikacji. Wchodzi w layout, w kontekst użytkownika, w każdą server action, która musi wiedzieć, kto woła, i w schemat bazy danych, bo user_id z jego tabeli kończy jako klucz obcy we wszystkich twoich.
Cztery migracje i co wywołało każdą z nich
- Dostawca pierwszy na drugiego, w dwa dni. Pierwszy działał, ale model cenowy i sprzężenie z jego własnym panelem nie pasowały. Żadna linia kodu auth nie przetrwała zmiany.
- Dostawca drugi, sześć miesięcy współżycia. Tu było wszystko: notatki w README o jego dziwnym zachowaniu z Suspense, refaktory middleware'u, żeby czytać chronione domeny z pliku konfiguracyjnego, i zmiana
useProfilenauseUser, żeby przestać utrzymywać własną warstwę na jego wierzchu. - Dostawca drugi na trzeciego. Cały PR nazwany "migrate from" plus nazwa poprzedniego. Trzydzieści dotkniętych plików. Znikają
stack.tsi trasa catch-allapp/handler/[...stack]/page.tsx, a pojawiają sięauth.tspo stronie klienta iapp/api/auth/[...path]/route.tspo stronie serwera. - Dostawca trzeci na własny middleware. Commit mówi dosłownie "replace neonAuth with custom auth middleware and update session handling". Z zarządzanego SDK zostaje mi klient; resztę kontroluję sam.
Co pęka zawsze: URL-e
Przy drugiej migracji są dwa commity tego samego dnia, jeden za drugim. Pierwszy zostawia plik konfiguracyjny w takim stanie:
export const stackServerApp = new StackServerApp({
tokenStore: "nextjs-cookie",
urls: {
signIn: "/ass",
signUp: "/asdds",
},
});
Tak to wygląda, kiedy sprawdzasz ręcznie, dokąd co przekierowuje, i zostawiasz testowe klepnięcie w klawiaturę. Drugi commit przywraca /auth w obu miejscach. To śmieszne i to dokładnie ten błąd, który zjesz przy każdej migracji auth: dostawca nie wie, gdzie mieszka twój ekran logowania, więc w jego konfiguracji jest zadeklarowany URL, który musi zgadzać się z prawdziwą trasą routera, a nie ma typów, które by to sprawdziły. Przekierowuje na 404, a objawem nie jest "zły URL", tylko "logowanie nie działa".
Druga połowa tego, co pęka, to kształt sesji. Przy przejściu na własny middleware getSession przestał zwracać obiekt SDK i zaczął zwracać coś jawnego:
type SessionData = NonNullable<Awaited<ReturnType<typeof auth.getSession>>["data"]>;
export type Session = SessionData | { session: null; user: null };
To | { session: null; user: null } to ustępstwo, które trzeba zrobić. Poprzedni SDK zwracał goły nullable i połowa kodu robiła session?.user. Po znormalizowaniu wszystkie miejsca zakładające, że sesja może być undefined, przestały się kompilować. To jest droga część migracji, a nie logowanie.
Co zyskuję, a co tracę, pisząc to sam
Zysk jest konkretny: kontrola nad obiektem sesji, nad tym, gdzie trzymana jest cookie, i nad tym, co dokładnie dzieje się, kiedy sesji nie ma. Oraz rzecz, której nie doceniałem, dopóki jej nie potrzebowałem: możliwość wstawienia reguł autoryzacji w tym samym miejscu.
W narzędziu BI dla dużej firmy autoryzacja skończyła jako lista adresów mailowych w samym middlewarze:
const authorizedEmails = [ /* ... */ ];
const hasFullAccess = userEmail && authorizedEmails.includes(userEmail);
Z dwoma różnymi przekierowaniami: niezalogowany idzie do logowania, zalogowany bez uprawnień idzie na stronę główną. Przy zarządzanym dostawcy robi się to, walcząc z jego claimami albo panelem. Tam napisałem to w dziesięciu liniach. Zostawiłem też console.log("User email:", userEmail), które trzeba było usunąć w późniejszym commicie, a ta aplikacja przepisywała auth trzy razy w miesiąc.
Tracę wszystko to, co dostawca robił, a czego nie widziałeś: rotację tokenów, unieważnianie sesji, mail odzyskiwania hasła, rate limit logowania. Nic z tego nie jest niemożliwe, ale staje się moim kodem, bez testów, których sam nie napisałem.
Moje kryterium dzisiaj
To nie jest "napisz własne auth". Jest nudniejsze: załóż od pierwszego dnia, że zmienisz dostawcę. Co robię dziś, zanim cokolwiek zainstaluję:
- Jedna funkcja
getSession()w moim kodzie. Żaden komponent nie importuje SDK. Typ zwracany definiuję ja, nie wnioskuję go od dostawcy. To różnica między migrowaniem trzydziestu plików a migrowaniem jednego. user_idw mojej bazie jest mój. Ten od dostawcy idzie do osobnej kolumny. Inaczej migracja przestaje być zmianą biblioteki i staje się migracją danych.- URL-e logowania w stałej, nie w konfiguracji dostawcy. Żeby
/assnie miało się gdzie schować. - Autoryzacja nigdy wewnątrz dostawcy. Kto może wejść, należy do dostawcy; co może zrobić, należy do mojego kodu.
Niedawno zacząłem nowy projekt i znów wystartowałem od zarządzanego dostawcy. Pierwszego dnia nie dało się wejść: bazowy URL auth wskazywał na wypełniacz z boilerplate'u. Cykl zaczyna się od nowa, tylko tym razem wiem dokładnie, gdzie zaboli.
