fd79513ce2
Testy / Testy warstwy logicznej (silnik) (pull_request) Failing after 4s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Failing after 3s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Failing after 3s
Testy / Testy astrodemo (pull_request) Failing after 3s
Testy / Build obrazu silnika B (swisseph) (pull_request) Failing after 2s
Testy / Kontrola składni wszystkich warstw (pull_request) Failing after 3s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Failing after 3s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Failing after 3s
Testy / Testy astrodemo (push) Failing after 3s
Testy / Build obrazu silnika B (swisseph) (push) Failing after 2s
Testy / Kontrola składni wszystkich warstw (push) Failing after 3s
build / build (push) Successful in 9s
Trzeci z pięciu kroków. Zmieniłem jego zakres wobec planu i warto wiedzieć dlaczego: pierwotnie miała to być deduplikacja pięciu kopii link_crypto.py, ale po zrobieniu PR 2 widać, że astroklienta blokuje co innego — ekrany, których nie ma mieć, siedzą wewnątrz jednego main.py. Deduplikacja kryptografii jest realnym długiem, ale niczego nie blokuje. PODZIAŁ. main.py (1023 linie) rozpadł się na podstawa.py (wspólne obiekty i pomocnicy), jedenaście modułów w app/ekrany/ i main.py, który jest już samym ZŁOŻENIEM: lista importów JEST definicją produktu. Podział zrobiony mechanicznie, z osobnym sprawdzeniem, że żadna sekcja nie wołała pomocnika z innej (nie wołała). KATALOG Z REJESTRACJI. Dotąd wszystkie funkcje były wypisane w features.py, więc obraz produktu, który części z nich nie ma, i tak niósł ich nazwy — spis funkcji, których nie ma jak włączyć. Teraz ekran zgłasza siebie, swoje trasy i swoje zasoby przy imporcie własnego modułu, a features.py nie wymienia ani jednego ekranu. Kolejność w nawigacji jest jawna (`kolejnosc`), żeby nie rządziła nią kolejność importów. To samo dotyczy nawigacji administratora: odsyłacz do ekranu kont był wpisany na sztywno w base.html, więc w węższym produkcie zostawał martwy link i nazwa ekranu, którego nie ma. EKSPORT JAKO MODUŁ. Zgodnie z ustaleniem eksport jest funkcją administracyjną, więc musi dać się usunąć. app/moduly/eksport/ zabiera arkusz, trasę PDF-a i akcję formularza. „Można, ale nie temu kontu" i „nie ma takiej możliwości" to dwie różne gwarancje, a eksport wynosi najwięcej treści baz naraz. MOST ODKRYWA MODUŁY. Skoro modułów jest więcej niż jeden, most nie może ich znać z nazwy — nazwa nieobecnego modułu jechałaby do obrazu, w którym go nie ma. Przechodzi więc po podkatalogach app/moduly/ i pyta każdy, co wnosi. Katalog generowania przeniesiony z app/dodatki na app/moduly/dodatki. ZNALEZIONE PRZY OKAZJI. Po wydzieleniu eksportu okazało się, że jego ścieżki SZCZĘŚLIWEJ nie sprawdzał żaden test — badano wyłącznie odmowę dla konta bez uprawnienia. Moduł dostaje zależności z wywołania montującego, więc brak jednej z nich wyszedłby dopiero przy pierwszym kliknięciu. Dopisany test funkcjonalny (realny arkusz, sprawdzany aż do nagłówka ZIP-a) i brakujące zależności. Cztery komentarze w plikach współdzielonych wymieniały zakładkę „Skompiluj", w tym wheelzoom.js ze wzmianką o „przyszłej zakładce" — to samo zgłoszenie, które audyt podnosił wcześniej. TEST ZŁOŻENIA. Buduje węższy produkt NAPRAWDĘ: kopiuje drzewo, usuwa cztery ekrany i oba moduły, uruchamia aplikację w OSOBNYM PROCESIE (importy są zapamiętywane, więc sprawdzanie tego w procesie, który moduł już zaimportował, dawałoby wynik fałszywie pozytywny) i sprawdza, że wstaje, że zachowane ekrany oddają 200, że usunięte oddają 404 (nie 403 i nie 500), że katalog opisuje ten obraz, i że w nawigacji nie ma martwych odsyłaczy. Napisałem najpierw ostrzejszy test — „nazwa ekranu nie pada poza jego modułem" — i go wyrzuciłem: zgłaszał wzmianki o Horoskopie w plikach współdzielonych, choć astroklient Horoskop MA. Ślad ma znaczenie wyłącznie wobec konkretnego złożenia, więc sprawdzenie należy do produktu, nie do mechanizmu. Testy: presentation 368, logic 342, data 37, render 41. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
80 lines
3.6 KiB
Python
80 lines
3.6 KiB
Python
"""Logowanie i wylogowanie (LOG-34)."""
|
|
from __future__ import annotations
|
|
|
|
from fastapi import Form, Request
|
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
|
|
from app.features import Feature
|
|
from app.podstawa import (features, app, templates, security, session)
|
|
|
|
# ---------------- Logowanie i wylogowanie (LOG-34) ----------------
|
|
# Zastępuje HTTP Basic, który nie miał wylogowania: przeglądarka zapamiętywała
|
|
# hasło i dosyłała je sama, więc serwer nie miał czego zapomnieć. Teraz to serwer
|
|
# decyduje, czy dana przeglądarka jest w środku — i może to cofnąć.
|
|
|
|
def _bezpieczne_dokad(dokad: str) -> str:
|
|
"""Adres powrotu po zalogowaniu, przepuszczony przez sito.
|
|
|
|
Bez tego `?dokad=https://obcy.pl` zamieniłby nasz ekran logowania w narzędzie
|
|
do przekierowywania ludzi gdzie indziej — klasyczna droga do wyłudzenia hasła,
|
|
bo odsyłacz prowadzi z zaufanego adresu. Wpuszczamy wyłącznie ścieżki
|
|
wewnętrzne: jeden ukośnik na początku i ani jednego znaku, który mógłby
|
|
zacząć adres obcego serwera."""
|
|
dokad = (dokad or "").strip()
|
|
if not dokad.startswith("/") or dokad.startswith("//") or "\\" in dokad:
|
|
return "/"
|
|
return dokad.split("?", 1)[0].split("#", 1)[0] or "/"
|
|
|
|
|
|
@app.get("/logowanie", response_class=HTMLResponse)
|
|
def logowanie_form(request: Request, dokad: str = "/", blad: str = ""):
|
|
# Zalogowanego nie ma po co pytać o hasło jeszcze raz.
|
|
if security.auth_enabled() and security.principal(request) is not None:
|
|
return RedirectResponse(_bezpieczne_dokad(dokad), status_code=303)
|
|
return templates.TemplateResponse(request, "logowanie.html",
|
|
{"dokad": _bezpieczne_dokad(dokad), "blad": blad})
|
|
|
|
|
|
@app.post("/logowanie")
|
|
def logowanie(request: Request, login: str = Form(""), haslo: str = Form(""),
|
|
dokad: str = Form("/")):
|
|
cel = _bezpieczne_dokad(dokad)
|
|
who = security.verify(login.strip(), haslo)
|
|
if who is None:
|
|
# JEDEN komunikat na złe hasło i nieznany login. Rozróżnienie mówiłoby,
|
|
# które konta istnieją — a to informacja warta czegoś dla zgadującego.
|
|
security.audit_login(request, login.strip(), udane=False)
|
|
return templates.TemplateResponse(
|
|
request, "logowanie.html",
|
|
{"dokad": cel, "blad": "Nieprawidłowy login lub hasło.", "login": login},
|
|
status_code=401)
|
|
|
|
security.audit_login(request, who.login, udane=True)
|
|
odpowiedz = RedirectResponse(cel, status_code=303)
|
|
odpowiedz.set_cookie(session.COOKIE, security.issue_session(who.login),
|
|
max_age=session.max_age(),
|
|
**session.cookie_params(secure=security.cookies_secure()))
|
|
return odpowiedz
|
|
|
|
|
|
@app.post("/wyloguj")
|
|
def wyloguj(request: Request):
|
|
"""Kasuje ciasteczko sesji. To jest całe wylogowanie — natychmiastowe
|
|
i bez żadnego magazynu, bo przeglądarka nie ma już czego dosyłać.
|
|
|
|
POST, nie GET: pod adresem GET wystarczyłby obrazek na obcej stronie, żeby
|
|
wylogować kogoś bez jego wiedzy. Uciążliwość, nie włamanie — ale darmowa
|
|
do uniknięcia."""
|
|
security.audit_logout(request)
|
|
odpowiedz = RedirectResponse("/logowanie?wylogowano=1", status_code=303)
|
|
odpowiedz.delete_cookie(session.COOKIE, **session.cookie_params(
|
|
secure=security.cookies_secure()))
|
|
return odpowiedz
|
|
|
|
# Wyjście musi być dostępne dla każdego zalogowanego, niezależnie od tego, co mu
|
|
# przyznano — konto bez ani jednego ekranu też ma prawo wyjść.
|
|
features.zarejestruj(
|
|
trasy={("GET", "/logowanie"): None, ("POST", "/logowanie"): None,
|
|
("POST", "/wyloguj"): None},
|
|
)
|