Rejestracja użytkowników - architektura

0

Mam aplikację w której rejestracja użytkowników wygląda mniej wiecej w uproszczeniu tak:

  1. Użytkownik zakłada konto.
  2. Konto jest rejestrowane w bazie danych a flaga weryfikacji konta jest ustawiana na false (wiem wtedy że użytkownik nie może się zalogować na konto).
  3. Podczas rejestracji generowany jest także token (UUID) który zapisywany jest w bazie razem z ID użytkownika + data wygaśnięcia tokenu.
  4. Token w URL jest wysyłany mailem do użytkownika. Po kliknięciu przez usera sprawdzam czy token nie wygasł i jeśli nie to aktywuje konto.
  5. Token zostaje usunięty z DB.

Generalnie to działa ale mam z tym kilka problemów:

  1. Jeśli użytkownik się zarejestruje i nigdy nie aktywuje konta to tak naprawdę można takie konto z db usunąć bo np. ktoś się pomylił podając zły email. Mogę to teraz zrobić ale muszę przeszukiwać całą tabelę z userami żeby znaleźć takie konta.
  2. Użytkownik który nie aktywował konta ma flagę aktywności na false. Jeśli będę chciał kolejne funkcjonalnosci wprowadzić typu ważność hasła wygasła itp to musiałbym kolejne flagi wprowadzić. Kiedyś słuchałem jakiegoś talku Jakuba Nabrdalika który mówił że zamiast robić takie flagi lepiej jest dodać dodatkową tabelę tymczasową (o czym napiszę niżej).
  3. Jeśli konto jest nieaktywne bo nie zostało jeszcze zweryfikowane to nie chcę go wyświetlać innym użytkownikom w liście użytkowników przez co moja logika w aplikacji czasami jest zawila bo muszę sprawdzać dodatkowe przypadki żeby wiedzieć czy konto jest w pełni aktywne.

Poza tym mam kilka mniejszych problemów i dlatego pomyślałem sobie żeby to przebudować ale chciałbym najpierw zapytać osoby bardziej doświadczone o radę. Po modyfikacji proces rejestracji wyglądałby tak:

  1. Użytkownik się rejestruje.
  2. Informacje o jego koncie trafiają do tabeli pomocniczej, np. Draft_User w której trzymam jeszcze niezweryfikowanych użytkowników.
  3. Tworzę token i wysylam go mailem.
  4. Jeśli kliknie w link i token nadal nie wygasł to konto jest w pełni rejestrowane, tzn informacje o koncie z np. Draft_User są przenoszone do tabeli w pełni funkcjonujących użytkowników.

Plusy:

  1. Logika w mojej aplikacji znacznie bardziej się uprości w niektórych miejscach (no jedynie przy rejestracji będę musiał sprawdzić unikatowość mejla w dwóch tabelach + po aktywności przenieść dane z jednej tabeli do drugiej).
  2. Nie potrzebuję flagi żeby wiedzieć czy konto zostało zweryfikowane.
  3. Jeśli będę chciał usuwać niezweryfikowane konta np. po tygodniu to będę miał znacznie mniejszą pulę rekordów do przejrzenia.

Co myślicie? Jak realizujecie w swoich projektach proces o którym piszę?

0

Bez sensu. Osobiście zamiast flagi stosuje zwykłego integera, który trzyma "stan" użytkownika np: nowy, oczekujący na weryfikację, aktywny, zablokowany. Nie ominiesz logiki, to jest normalne, ale co to za logika, jeden where? Bo jak dla mnie to kompletnie nic, a porządne orm`y pozwalają i takie rzeczy uprościć.

Po co chcesz ręcznie przeglądać konta niezweryfikowane? przecież tu wystarczy jedno zapytanie w postaci:

DELETE FROM users WHERE status = 3 AND created_at < NOW() - INTERVAL 7 DAY
0

@mr_jaro: Dzięki za komentarz :) Oczywiście wcale nie mam zamiaru nawet bronić na siłę swojego pomysłu. Chodzi o to że szukam rozwiązania na swoje problemy i zastanawiam sie jak je rozwiązać a to co przedstawiłem to tylko koncepcja którą celowo chciałem poddać ocenie.

Osobiście zamiast flagi stosuje zwykłego integera, który trzyma "stan" użytkownika np: nowy, oczekujący na weryfikację, aktywny, zablokowany.

O czymś takim nie pomyślałem ale faktycznie ma to sens. A ten integer to rozumiem że jakiś FK?

Po co chcesz ręcznie przeglądać konta niezweryfikowane?

Nie nie, nie chodzi mi o ręczne przeszukiwanie oczywiście. Miałem na myśli SQL'a którego sam tu przedstawiłeś (może jakaś procedura itp) tylko pisałem to w kontekście tego że baza będzie miała mniej rekordów do przeszukania żeby usunąć pewne rekordy. Chociaż zdrugiej strony ten projekt to nie żaden facebook z setkami milionow kont więc nie ma sensu się tym przejmować pod kątem wydajności więc o tym zapomnijmy.

0

Nie przejmuj się szybkością bazy na tym poziomie, serio, nawet na bazie mającej kilka mln rekordów wykonanie tego sqla nie było by wolne. Co do statusów może być osobna tabelka trzymająca te symbole albo po prostu klasa w kodzie z constami. Osobiście wole tę drugą metod bo wrzucam do whera przykładowo UserStatus::NEW

0
mr_jaro napisał(a):

Nie przejmuj się szybkością bazy na tym poziomie, serio, nawet na bazie mającej kilka mln rekordów wykonanie tego sqla nie było by wolne. Co do statusów może być osobna tabelka trzymająca te symbole albo po prostu klasa w kodzie z constami. Osobiście wole tę drugą metod bo wrzucam do whera przykładowo UserStatus::NEW

Okej, dzięki bardzo. W takim razie jeszcze to wszystko przemyślę ale myślę że ma to większy sens niż mój pomysł.

Przy okazji jeszcze jedno pytanie. Po rejestracji użytkownika dodaje także do db token przypisany do usera + date jego wygaśnięcia.
Co powinno się dziać z tymi tokenami po aktywacji użytkownika? Póki co je usuwam i nie widzę sensu dla którego miałbym je trzymać po aktywacji ale może jakiegoś case'u nie widze i powinienem je zachowywać?

0

tokeny usuwaj, tylko pamiętaj, żeby kolejna osoba nigdy nie dostała takiego samego tokenu, no i kwestia tego, że dajesz x czasu... ja osobiście nie wywalam starych kont, po prostu daje komuś możliwość ponownego otrzymania linku aktywacyjnego.

0
mr_jaro napisał(a):

tokeny usuwaj, tylko pamiętaj, żeby kolejna osoba nigdy nie dostała takiego samego tokenu,

A jak generujesz tokeny? Ja osobiście generuję UUID więc unikatowość mam zachowaną natomiast czytałem że można to zrobić z JWT i nie trzeba wówczas zapisywać nawet takich tokenów w bazie. Póki co się jeszcze nie interesowałem się zbyt mocno tematem bo moje rozwiązanie mi wystarczy choć myślę czy w dłuższej perspektywie się sprawdzi.

2

@eL @danek JWT nie służy do tworzenia tokenów tego typu! To jest token autoryzacyjny który zastępuje session cookie. Już kiedyś wywalałem takie rozwiązania z projektu po hindusach bo same fuckupy były przez to w projekcie. Token można tworzyć samemu i to bardzo prosto. To jest tylko token sprawdzający czy dany mail istnieje, więc zabezpieczeniem jego jest to, że dochodzi na konkretną skrzynkę i odczytać może go tylko właściciel skrzynki. Więc tutaj wystarczą 2 rzeczy w miarę unikalny token w stylu md5(losowy_ciag_znakow + id_usera). Nie potrzeba nic cięższego. Po aktywacji przekierowujesz usera na strone logowania więc nawet jakby komuś cudem udało się zgadnąć hasha to i tak jeszcze musi znać dane do konta. Zabezpieczenie kompletne. Tę samą zasadę stosuje się przy resecie haseł tylko dla bezpieczeństwa token jest powiązany z mailem więc nawet jakby ktoś zgadł token to musiałby jeszcze wiedzieć pod jaki mail ten token jest przypisany.

0

@mr_jaro: Super, dzięki za pomoc. Ma to oczywiście sens i dlatego myślę że pójdę za Twoimi wskazówkami i tak to zrobię. Na zakończenie jeszcze pozwolę sobie odbić lekko temat w inną stronę choć nadal ma to związek z rejestracją i architekturą.
Rejestrację użytkowników (i inne tematy związane z kontem) mam wydzielone do osobnego modułu w projekcie. Moja aplikacja ma też moduł z ogłoszeniami które są powiedzmy pewnego rodzaju ofertą składaną między 2 klientami.

Mimo iż oba moduły są w tym samym projekcie to celowo wydzieliłem je do osobnych modułów ponieważ są to całkowicie inne konteksty i staram się nie robić niepotrzebnych powiązań pomiędzy nimi. W przyszłości myślę o tym żeby każdy z nich stał się osobnym serwisem. Problem pojawia się kiedy dane pomiędzy 2 modułami są poniekąd współdzielone ponieważ (weźmy najłatwiejszy przykład) oferta musi mieć id użytkownika który ją utowrzył.

Osobiście najczęściej pracowałem z serwisami które miały całkowicie oddzielne bazy danych per service natomiast znalazłem taką propozycję:
https://microservices.io/patterns/data/shared-database.html
Teoretycznie rozwiązanie jest okej ponieważ nie ma tu żadnej magii, robienie rolbacków to banał i rozwiązuje mi to wiele problemów. Z tym że jakoś kompletnie nie kupuje pomysłu na współdzielenie bazy pomiędzy serwisami chociażby dlatego (ktoś w komentarzach też o tym wspomina) że mając więcej serwisów (których może też być kilka instancji) w końcu skończy nam się pula połączeń z taką share'owaną bazą danych. Dodatkowo zmiana schematu bazy wymusza modyfikację wszystkich serwisów które korzystają z tych tabel a przecież w samym założeniu serwisy powinny być niezależne i działać bez względu na modyfikację innych.

Szukając dalej trafiłem na artykuł na stronie Martina Fowlera:
https://martinfowler.com/articles/extract-data-rich-service.html
Prosty przykład refaktoryzacji monolitu na 2 serwisy a każdy z nich ma swoją bazę. Sęk w tym że mamy tu relację pomiędzy produktem a jego ceną która zostaje usunięta po stronie bazy danych. Dalej możemy przeczytać:

No table joins are permitted from tables that belong to the core product part of the system to the tables that belong to product pricing. Similarly, there should be no "hard" constraints in the database between the core product data and the product pricing data such as foreign keys or database triggers. All joins as well as constraints have to be moved up to the logic layer from the database layer. (...)

W mojej ocenie podejście to jest znacznie bardziej zgodne z architekturą mikroserwisów, natomiast ciężko mi wyobrazić sobię żeby całą spójnością danych zarządzać po stronie kodu. Co z transakcjami? Jeśli serwis z produktami zapisze info o produkcie ale coś się posypało podczas zapisywania informacji o cenie to we współdzielonej bazie zrobienie rollbacku nie byłoby żadnym problemem. W przypadku z 2 bazami widzę tylko 2 rozwiązania: 1. jeśli druga operacja się nie powiodła to robiony jest dodatkowy request do bazy o usunięcie produktu (z tym że nie jest to w żaden sposób bezpieczne bo jakiś inny serwis mógł pobrać już dane o tym produkcie) albo ewentualnie dodać jakąś kafkę z event sourcingiem i po prostu jak serwis z cenami zacznie działać poprawnie to sam z automatu dociągnie sobie wszystkie dane (choć to też nie jest idealne rozwiązanie bo coś pomogło się posypać nie przez problemy z działaniem serwisu ale np. posypało się coś podczas kalkulacji cen i nigdy takie dane do bazy nie zostaną zapisane).

Nie pytam więc o gotowe rozwiązanie bo wiadomo że dużo zależy od kontekstu ale z jakim podejściem Wy się częściej spotykacie i które z nich uważacie za lepsze (choć tu znowu dużo zalezy od kontekstu). Share'ujecie swoje bazy czy wolić podejście db per service a o spójność i poprawność danych sami dbacie?

0

Osobiście nie widzę sensu dzielenia tego w tak prostych apkach. Skomplikujesz sobie kod a zysku nie będziesz miał żadnego, a w zasadzie wszystko będzie wolniejsze. Dopóki się da a da się 99% przypadkach, nie warto dzielić bazy. Co do kodu strony można dzielić na moduły ALE zawsze logika między modułami będzie się mieszać. Utopia nie istnieje.

0
mr_jaro napisał(a):

Co do kodu strony można dzielić na moduły ALE zawsze logika między modułami będzie się mieszać. Utopia nie istnieje.

Jasne, zgadzam się ale dzielenie na moduły albo package scope + dodanie jakiegoś interface'u który pozwala na komunikację z modułem/pakietem przynajmniej pozwala mocno ograniczyć zależności pomiędzy klasami. Fakt że nie jest to proste ale przynajmniej każdy 2 razy zastanowi się co wystawia poza moduł zamiast chamsko importować wszystko jak leci co jest public.

Czyli dobrze rozumiem że nawet jeśli dzielę sobie to pomiędzy moduły to i tak nie ma sensu póki co robić oddzielnych baz tylko wszystko wrzucać do jednej a ewentualnie jeśli faktycznie obciążenie będzie naprawdę duże to wówczas zastanawiać się nad cięciem tego na serwisy?

0

Jak przeciążenie na jednej bazie bedzie za duże to prościej postawić load balancera i druga baze z replikacją. Bo to nie baza będzie nie wyrabiała tylko serwer.

0

Na początku możesz zacząć od jednej bazy danych, tyle że też ją podziel za pomocą schema na moduły odpowiadające modułom w kodzie. I staraj się tak pisać kod by transakcje pomiędzy oddzielnymi schema nie były potrzebne. Wtedy jak już przyjdzie ta chwila że baza danych nie będzie wyrabiać z zapisami, łatwiej będzie poszczególne schema przenieść do oddzielnych baz.
Aczkolwiek zwykle pierw następuje problem że baza nie wyrabia z odczytami, który rozwiązuje się właśnie replikami, cachem, czy przechowywaniem danych zdenormalizowanych w osobnych źródłach danych.

Swoją drogą pomył opisany w pierwszym poście z oddzielną tabelą bardzo mi się podoba, tylko żeby taka zabawa miał sens trzeba mieć odpowiednią ilość danych, rzędu wielkości allegro :)

1 użytkowników online, w tym zalogowanych: 0, gości: 1