Dokumentacja techniczna
Jak portal jest zbudowany, gdzie mieszkają dane i co przełącza się z panelu
Portal działa na maszynie deweloperskiej pod adresem naszebanino.syn.test, na bazie SQLite wypełnionej danymi startowymi z seedera. Konta, ogłoszenia, oceny i wpisy na forum są przykładowe — służą do sprawdzenia wyglądu i działania, nie są prawdziwą treścią portalu.
Przed uruchomieniem publicznym manage.py seed_portal
— Trzeba zmienić hasła kont startowych, wpisać prawdziwe dane bankowe i klucze bramki płatności, a dane demonstracyjne usunąć.
Architektura
Nasze Banino to aplikacja wielostronicowa (MPA) w Django. Każda treść ma własny adres, jest renderowana na serwerze i zapisana w bazie danych. Poprzednia wersja była szablonem frontendowym bez backendu, w którym konta, oceny i ogłoszenia leżały w pamięci przeglądarki.
Django 6
Widoki klasowe, szablony serwerowe, sesje w ciasteczku HttpOnly.
SQLite → PostgreSQL
Przełączenie zmienną DB_ENGINE, bez zmian w kodzie.
HTMX + Alpine + TypeScript
Bez luźnych skryptów, bez typu any, strażniki typów na każdej odpowiedzi.
Tailwind CSS 4
Motyw i klasy przeniesione 1:1 z szablonu.
Gdzie mieszkają dane
Nic nie jest trzymane w przeglądarce. Wszystko, co dawniej leżało w pamięci przeglądarki, ma teraz model w bazie i migrację.
accounts.User
konta i sesje
hasła: PBKDF2
User username CharField, unikalne email EmailField account_type 'client' | 'staff' display_name CharField, nazwa publiczna is_staff wymuszone False dla klienta is_superuser wymuszone False dla klienta
Hasła hashuje Django. Sesja siedzi w ciasteczku HttpOnly, więc kod w przeglądarce jej nie odczyta.
Rodzaj konta ustawia wyłącznie serwer. Ograniczenie CHECK „klient_bez_dostepu_do_panelu” odrzuca w bazie każdą próbę nadania klientowi uprawnień panelu — także zapytaniem UPDATE.
listings.Listing
ogłoszenia
czas życia z pakietu
Listing owner FK → User package FK → ListingPackage categories M2M → ListingCategory status draft | awaiting_payment | published | expired published_at DateTimeField expires_at published_at + package.duration_days highlight_until published_at + package.highlight_days
Widoczność liczy zapytanie `Listing.objects.visible()`: status opublikowany oraz data wygaśnięcia w przyszłości. Wygasanie dzieje się po stronie serwera, nie przy odczycie w przeglądarce.
Limity pakietu (kategorie, zdjęcia, znaki opisu) sprawdza formularz na serwerze. Obejście podpowiedzi w przeglądarce kończy się błędem walidacji, nie opublikowanym ogłoszeniem.
payments.Payment
zamówienia płatności
numer nieprzewidywalny
Payment reference NB-RRRRMMDD-HEX, unikalny user FK → User listing FK → Listing package FK → ListingPackage method tpay | bank_transfer status new | pending | paid | refunded | cancelled amount Decimal
Numer zamówienia zawiera losowy fragment, więc nie da się odgadnąć cudzego. Dodatkowo widok pokazuje zamówienie tylko jego właścicielowi.
Powrót przeglądarki z bramki nie jest dowodem zapłaty. Dowodem jest powiadomienie serwerowe ze zgodną sumą md5sum i podpisem X-JWS-Signature. Zwrot pieniędzy zdejmuje ogłoszenie z portalu.
core.FeatureFlag
przełączniki funkcji
źródło: features.py
FeatureFlag key klucz z rejestru w kodzie is_enabled BooleanField
Panel przełącza istniejące klucze, ale nie wymyśla nowych — rejestr jest w kodzie, więc nie powstanie flaga, której nikt nie obsługuje.
Wyłączona sekcja zwraca 404, znika z menu i z wyników wyszukiwania. Świadomie nie 403 — z zewnątrz ma wyglądać, jakby jej nie było.
Rozdział panelu administratora i panelu klienta
Dwa rozłączne światy. Konto klienta nigdy nie wejdzie do panelu administratora, a konto administracyjne nie ma panelu klienta. Ten niezmiennik jest wymuszony w trzech miejscach naraz.
Panel administratora
/panel/
Wymaga konta z rodzajem „administracyjne”. Pełna szerokość ekranu.
Panel klienta
/konto/
Wymaga konta z rodzajem „klient”. W pełni responsywny.
Wymuszenie w modelu
User.save()
Konto klienta ma zerowane uprawnienia panelu przy każdym zapisie.
Wymuszenie w bazie
klient_bez_dostepu_do_panelu
Ograniczenie CHECK odrzuca eskalację omijającą model.
Wymuszenie w widokach
ClientRequiredMixin
Konto administracyjne dostaje odmowę w panelu klienta.
Logowanie bez wyroczni
ClientLoginForm
Poprawne hasło konta administracyjnego daje w panelu klienta ten sam komunikat co złe hasło, a po dziesięciu próbach wchodzi blokada na kwadrans.
Pakiety ogłoszeń i płatności
Pakiety są danymi, nie kodem. Administrator zakłada własne, ustala ceny i limity. Wyłączenie jednego przełącznika sprawia, że portal działa w całości bezpłatnie.
| Mechanizm | Gdzie | Jak działa | O tym pamiętać |
|---|---|---|---|
| Przełącznik modułu płatnego | premium_packages |
Wyłączony — każde ogłoszenie dostaje pakiet bezpłatny. | portal w 100% darmowy |
| Limity pakietu | ListingPackage |
Liczba kategorii, zdjęć, znaków opisu, dni publikacji i wyróżnienia. | sprawdzane na serwerze |
| Wyróżnienie | highlight_days |
Ile dni ogłoszenie trzyma się na górze listy wyników. | promoted_first() |
| Płatność online | Tpay Open API |
Autoryzacja POST /oauth/auth, transakcja POST /transactions. | czeka na klucze |
| Powiadomienie o zapłacie | md5sum + X-JWS-Signature |
Suma kontrolna z kodu bezpieczeństwa i podpis odłączony RFC 7515. | powtórki odrzucane |
| Przelew tradycyjny | PaymentSettings |
Odbiorca, rachunek i tytuł ustawiane w konfiguracji płatności. | dane z panelu |
| Dane karty | formularz Tpay |
Numeru karty nie przyjmujemy na własnym serwerze. | poza zakresem PCI |
Kontrakt między serwerem a przeglądarką
Front nigdy nie dostaje pola, którego nie zna, i nie traci pola, którego oczekuje. Kontrakty są opisane raz, w Pythonie, i z nich generowane są interfejsy TypeScript.
| Element | Miejsce | Rola |
|---|---|---|
| Źródło prawdy | apps/core/api_contracts.py |
Dataklasy opisujące każdą odpowiedź i każdy zestaw danych dla przeglądarki. |
| Generowanie typów | manage.py export_api_types |
Zapisuje frontend/src/types/api.ts. Uruchamiane przed budową frontu. |
| Strażnicy typów | isRecord, hasExactKeys |
Odrzucają odpowiedź z brakującym albo nadmiarowym polem. |
| Skutek rozjazdu | błąd sprawdzania typów |
Niezgodność wychodzi w budowie, nie w przeglądarce użytkownika. |
Testy
Każda funkcjonalność ma testy jednostkowe i penetracyjne, z asercjami pozytywnymi i negatywnymi.
Testy backendu
manage.py test
Modele, formularze, usługi, widoki, kontrola dostępu, komendy.
Testy frontu
npx vitest run
Strażniki typów i logika modułów przeglądarki.
Testy penetracyjne
tests/test_security.py
ACL, eskalacja uprawnień, IDOR, XSS, CSRF, weryfikacja powiadomień bramki.
Zakaz typu any
npx oxlint
Linter wywala budowę, jeśli w kodzie pojawi się typ any.
Django 6 · SQLite (docelowo PostgreSQL) · HTMX · Alpine · TypeScript · Tailwind CSS 4
Ta strona jest treścią z bazy danych. Rozdziały, wpisy i ich układ zmienia się w panelu administratora, bez dotykania kodu.