feat(astroklient-demo): sesje logowania zamiast HTTP Basic (LOG-34)
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m21s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m30s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Testy astroklienta-demo (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 5s
Testy / Kontrola składni wszystkich warstw (push) Successful in 4s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m17s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m25s
Testy / Testy astroklienta-demo (pull_request) Successful in 9m26s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 5s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 4s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m21s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m30s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Testy astroklienta-demo (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 5s
Testy / Kontrola składni wszystkich warstw (push) Successful in 4s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m17s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m25s
Testy / Testy astroklienta-demo (pull_request) Successful in 9m26s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 5s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 4s
Demo idzie szeroko i do różnych osób, często na cudzych komputerach — więc
wyjście z aplikacji jest tu potrzebne bardziej niż w pełnej wersji, a Basic go
nie miał: przeglądarka zapamiętuje hasło i dosyła je sama przy każdym żądaniu.
Pierwszy klient zostawiał otwartą sesję drugiemu.
Ta sama konstrukcja co w pełnej aplikacji: własny ekran logowania, podpisane
ciasteczko (HMAC-SHA256), HttpOnly + SameSite=Strict, wylogowanie POST-em, kres
bezczynności i twardy, sito na adres powrotu, zdarzenia w dzienniku bez haseł.
Moduł session.py skopiowany, tak samo jak link_crypto — usługi są osobnymi
obrazami i nie importują się nawzajem.
DWIE RÓŻNICE WOBEC PEŁNEJ WERSJI, obie wynikające z tego, że demo nie ma
własnego wolumenu:
* nazwa ciasteczka jest inna. Gdyby obie aplikacje stanęły kiedyś pod jedną
domeną, ciasteczka o tej samej nazwie nadpisywałyby się i człowiek wypadałby
z jednej, logując się do drugiej.
* nie ma licznika pokolenia sesji, bo nie ma go gdzie zapisać. Zdalne
unieważnienie robi się przez DEMO_USERS: usunięcie konta albo zmiana hasła
NATYCHMIAST ubija jego otwarte sesje, bo odcisk poświadczenia w ciasteczku
przestaje pasować. Osobny test tego pilnuje. Wylogowanie i tak działa
natychmiast, bo polega na skasowaniu ciasteczka.
Klucz podpisu jest WŁASNY, nie ten z pełnej aplikacji: demo i produkcja nie mają
powodu uznawać nawzajem swoich sesji, a wspólny klucz znaczyłby, że sesja z demo
bywa ważna tam, gdzie nie powinna.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,7 +27,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from app import link_crypto, security
|
||||
from app import link_crypto, security, session
|
||||
from app.clients.logic_client import LogicClient
|
||||
|
||||
# Automatyczna dokumentacja WYŁĄCZONA — tak samo jak w pełnej aplikacji. /docs
|
||||
@@ -37,6 +37,8 @@ security.install(app)
|
||||
|
||||
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
||||
templates = Jinja2Templates(directory="app/templates")
|
||||
templates.env.globals["current_user"] = lambda request: getattr(request.state, "user", "") or ""
|
||||
templates.env.globals["auth_on"] = security.auth_enabled
|
||||
|
||||
def _logic(request: Request) -> LogicClient:
|
||||
"""Klient związany z pulą ZALOGOWANEGO konta.
|
||||
@@ -150,6 +152,59 @@ def _wroc(error: str = "", done: str = "") -> RedirectResponse:
|
||||
return RedirectResponse(f"/{'?' + q if q else ''}", status_code=303)
|
||||
|
||||
|
||||
# ---------------- Logowanie i wylogowanie (LOG-34) ----------------
|
||||
# Ta sama konstrukcja co w pełnej aplikacji. Różnica jest jedna i wynika z tego,
|
||||
# że demo nie ma własnego wolumenu: nie ma licznika pokolenia sesji, więc
|
||||
# „wyrzuć zewsząd" robi się zmianą hasła w DEMO_USERS. Wylogowanie i tak działa
|
||||
# natychmiast, bo polega na skasowaniu ciasteczka.
|
||||
|
||||
def _bezpieczne_dokad(dokad: str) -> str:
|
||||
"""Adres powrotu przepuszczony przez sito — bez tego `?dokad=https://obcy.pl`
|
||||
zamieniłby ekran logowania w narzędzie do wyłudzania haseł."""
|
||||
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 = ""):
|
||||
if security.auth_enabled() and security.principal(request):
|
||||
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)
|
||||
kto = security.verify(login.strip(), haslo)
|
||||
if kto is None:
|
||||
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, kto, udane=True)
|
||||
odpowiedz = RedirectResponse(cel, status_code=303)
|
||||
odpowiedz.set_cookie(session.COOKIE, security.issue_session(kto),
|
||||
max_age=session.max_age(),
|
||||
**session.cookie_params(secure=security.cookies_secure()))
|
||||
return odpowiedz
|
||||
|
||||
|
||||
@app.post("/wyloguj")
|
||||
def wyloguj(request: Request):
|
||||
"""POST, nie odsyłacz: pod adresem GET wystarczyłby obrazek na obcej stronie,
|
||||
żeby wylogować kogoś bez jego wiedzy."""
|
||||
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
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health() -> dict:
|
||||
return {"status": "ok", "layer": "astroklient-demo"}
|
||||
|
||||
Reference in New Issue
Block a user