Rozwiązywanie Kontekstu (Tenant Resolver)
1. Architektura Bazy Danych i Hybrydowe Połączenia
Centralny punkt systemu to baza katalogowa (TenantCatalog), która przechowuje metadane klientów, ich NIP, plany subskrypcyjne oraz unikalną nazwę bazy danych (DbName).
System nie przechowuje haseł do serwerów SQL w bazie katalogowej, a jedynie unikatowe nazwy baz; przyjęto, że nazwy zawierają nip oraz fragment ID danego wpisu, ale możliwe jest też adhocowe przypisanie dowolnej nazwy bazy. Domyślnie baza główna może być jak najbardziej w innym miejscu, niż API, natomiast bazy klientów powinny być na tym samym serwerze co API, po pierwsze ze względu na szybkość odczytu i brak przesyłania żądań SQL po sieci, a po drugie ze względu
Połączenie z bazą klienta jest składane w locie: system pobiera bezpieczny "szablon" z pliku appsettings.json i wstrzykuje do niego nazwę bazy pobraną z katalogu.
System cachuje DbContextOptions - obiekty tworzące zapytania ale nie sam obiekt DbContext, który musi być tworzony i usuwany per request
2. Rola TenantMiddleware (Izolacja Żądań)
- Wszystkie requesty HTTP do serwera przechodzą przez następujące Middleware'y: UseAuthentication (tu następuje walidacja JWT), TenantMiddleware, UseAuthorization i na końcu - wszelkie kontrolery
- Middleware to pierwszy strażnik w potoku. Przechwytuje absolutnie każde żądanie HTTP skierowane do serwera.
- Odpowiada wyłącznie za rozwiązanie kontekstu, czyli bezbłędne ustalenie, do jakiej firmy należy dane żądanie.
- Obsługuje trzy ścieżki przepływu:
1. Anonimowa: Jeśli użytkownik nie ma tokenu, middleware go przepuszcza bez przypisywania tenanta (pozwala to na załadowanie plików interfejsu Blazor czy formularza logowania).
2. Logowanie: Wyciąga identyfikator firmy (NIP) bezpośrednio z ciała zapytania JSON, by wiedzieć, gdzie szukać konta użytkownika.
3. Zalogowana: Rygorystycznie wymaga obecności tenanta, priorytetowo traktując kryptograficznie podpisany token JWT.
3. Proces Logowania i Generowanie Tokenu (AuthController)
- Kontroler przyjmuje login, hasło i NIP.
- Najpierw odpytuje bazę centralną (Katalog), by sprawdzić, czy firma o danym NIP istnieje i jak nazywa się jej fizyczna baza danych.
- Tworzy tymczasowe połączenie z tą konkretną bazą klienta, omijając standardowy system wstrzykiwania zależności.
- Weryfikuje poprawność hasła w tabeli użytkowników danej firmy.
- Po sukcesie generuje token JWT, w którym "zaszywa" identyfikator tenanta. Od tego momentu to token jest dowodem tożsamości i przynależności firmowej.
4. Wstrzykiwanie Zależności (DI) i Fabryki
- Rejestracja ApplicationDBContext w pliku Program.cs oraz w TenantDbContextFactory odbywa się dynamicznie (per-request).
- System pobiera rozwiązanego wcześniej tenanta z ITenantAccessor.
- Złożony z szablonu connection string trafia do bufora w pamięci RAM (DbContextOptionsCache), co zapobiega ciągłemu przebudowywaniu opcji Entity Frameworka przy każdym zapytaniu i drastycznie zwiększa wydajność.
5. Bezpieczeństwo i Obrona w Głąb (Defense in Depth)
- System opiera się na rozdzieleniu obowiązków: atrybut [Authorize] chroni dostęp do samych funkcji (endpointów API), a TenantMiddleware chroni dostęp do danych.
- Zapobieganie podszywaniu się: Ponieważ TenantResolver czyta identyfikator firmy w pierwszej kolejności z tokenu JWT (podpisanego kluczem serwera), zalogowany haker nie może wymusić zmiany bazy poprzez modyfikację nagłówków HTTP.
- Bezpieczeństwo w przypadku błędu dewelopera: Nawet jeśli programista zapomni nałożyć atrybut [Authorize] na endpoint, brak tokenu spowoduje brak przypisanego tenanta. Entity Framework nie otrzyma connection stringa i przerwie operację (zwracając błąd serwera 500), całkowicie chroniąc dane przed wyciekiem.