"""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}, )