"""Katalog funkcji i uprawnień (PRE-27). DWIE ZASADY, KTÓRE RZĄDZĄ TYM MODUŁEM 1. Konto ograniczone ma widzieć program KOMPLETNY — tylko mniejszy. Nic nie może sugerować, że istnieje coś więcej: żadnych wyszarzonych pozycji, żadnego „brak uprawnień", żadnego 403. Ścieżka bez uprawnienia zwraca **404**, bo 403 jest samo w sobie informacją, że coś tam jest. 2. Mapa trasa → uprawnienie jest JEDNA i leży tutaj. Rozproszenie jej po dekoratorach kończy się trasą, o której ochronie ktoś zapomniał — a taka dziura jest niewidoczna, dopóki ktoś jej nie znajdzie. Test przechodzi po wszystkich trasach aplikacji i wymaga, żeby każda była tu wymieniona. PODZIAŁ NA GRUPY. Ekrany odpowiadają zakładkom — to naturalna jednostka, bo zakładka jest tym, co widać w nawigacji. Rozszerzenia to POZIOMY ZŁOŻONOŚCI wewnątrz ekranów: ktoś może liczyć horoskopy, nie mając dostępu do porównywania systemów domów ani do generowania tekstu przez model (co kosztuje pieniądze). """ from __future__ import annotations from dataclasses import dataclass ADMIN = "admin" # zakładanie kont i nadawanie uprawnień — nie do nadania z UI @dataclass(frozen=True) class Feature: key: str label: str hint: str group: str href: str = "" # tylko ekrany; rozszerzenia nie mają własnej zakładki # ── ekrany (zakładki) ──────────────────────────────────────────────────── SCREENS: tuple[Feature, ...] = ( Feature("chart", "Horoskop", "Pozycje, osie, domy, aspekty, kosmogram.", "Ekrany", "/"), Feature("interpret", "Interpretacje", "Interpretacja natalna z baz i od modelu.", "Ekrany", "/interpret"), Feature("timeline", "Kalendarz", "Predykcje na wybrany zakres dat.", "Ekrany", "/timeline"), Feature("synastry", "Synastria", "Porównanie dwóch horoskopów.", "Ekrany", "/synastry"), Feature("significators", "Sygnifikatory", "Wyszukiwarka po bazach interpretacyjnych.", "Ekrany", "/significators"), Feature("compile", "Skompiluj", "Złożenie raportu z policzonych części.", "Ekrany", "/compile"), Feature("files", "Pliki", "Wybór baz, z których korzysta program.", "Ekrany", "/files"), Feature("settings", "Ustawienia", "Podgląd baz i konfiguracji modelu.", "Ekrany", "/settings"), ) # ── rozszerzenia (poziomy złożoności wewnątrz ekranów) ─────────────────── EXTRAS: tuple[Feature, ...] = ( Feature("houses_compare", "Porównanie systemów domów", "Wybór systemu innego niż domyślny i zestawienie kilku obok siebie.", "Rozszerzenia"), Feature("extra_charts", "Wykresy dodatkowe", "Aspektarian, wykres deklinacji, oś antyscji.", "Rozszerzenia"), Feature("advanced_calc", "Obliczenia zaawansowane", "Stacje planet, tabele żywiołów i faz, aspekty poboczne, zodiaki syderyczne.", "Rozszerzenia"), Feature("ai", "Generowanie tekstu przez model", "Horoskopy pisane przez model językowy. UWAGA: każde użycie kosztuje.", "Rozszerzenia"), Feature("files_input", "Wgrywanie i archiwizacja baz", "Dodawanie nowych plików baz i wycofywanie ich z użytku (plik zostaje, " "zamrożony, ze znacznikiem czasu). Kasować może wyłącznie administrator.", "Rozszerzenia"), Feature("export", "Eksport plików", "Pobieranie raportu jako PDF i wyników jako Excel.", "Rozszerzenia"), ) ALL: tuple[Feature, ...] = SCREENS + EXTRAS BY_KEY: dict[str, Feature] = {f.key: f for f in ALL} GRANTABLE: frozenset[str] = frozenset(BY_KEY) # ADMIN celowo poza tym zbiorem # ── mapa trasa → wymagane uprawnienie ──────────────────────────────────── # None = dostępne każdemu zalogowanemu. Dotyczy to pomocników (geokoder, strefa # czasowa, health), które same w sobie NICZEGO nie zdradzają o funkcjach programu. ROUTES: dict[tuple[str, str], str | None] = { ("GET", "/"): None, # korzeń przekierowuje na pierwszy dostępny ekran ("POST", "/"): "chart", ("GET", "/interpret"): "interpret", ("POST", "/interpret"): "interpret", ("GET", "/timeline"): "timeline", ("POST", "/timeline"): "timeline", ("GET", "/synastry"): "synastry", ("POST", "/synastry"): "synastry", ("GET", "/significators"): "significators", ("POST", "/significators"): "significators", ("GET", "/compile"): "compile", ("POST", "/compile"): "compile", ("POST", "/compile/pdf"): "export", ("GET", "/settings"): "settings", # Zarządzanie plikami baz (DAN-27). Trzy poziomy: „files" wybiera, z czego # program korzysta; „files_input" dokłada wgrywanie i archiwizację; # kasowanie, przywracanie i REGUŁY WALIDACJI to wyłącznie administrator — # o istnieniu walidacji nikt poza nim nie ma skąd wiedzieć. ("GET", "/files"): "files", ("POST", "/files/use"): "files", ("POST", "/files/upload"): "files_input", ("POST", "/files/archive"): "files_input", ("POST", "/files/restore"): ADMIN, ("POST", "/files/delete"): ADMIN, ("POST", "/files/rules"): ADMIN, ("POST", "/horoscope/stream"): "ai", ("GET", "/accounts"): ADMIN, ("POST", "/accounts/create"): ADMIN, ("POST", "/accounts/update"): ADMIN, ("POST", "/accounts/delete"): ADMIN, ("GET", "/geocode"): None, ("GET", "/reverse"): None, ("GET", "/timezone"): None, ("GET", "/health"): None, } def required(method: str, path: str) -> str | None: """Uprawnienie wymagane przez trasę. Nieznana trasa → wymaga admina. Domyślną odpowiedzią jest NAJWĘŻSZE uprawnienie, nie najszersze: dopisanie trasy bez wpisu w ROUTES ma ją zamknąć, a nie otworzyć. Odwrotny domyślny wybór oznaczałby, że każde przeoczenie tworzy dziurę.""" return ROUTES.get((method.upper(), path), ADMIN) def can(perms: frozenset[str], feature: str | None) -> bool: return feature is None or feature in perms def screens_for(perms: frozenset[str]) -> list[Feature]: """Zakładki do pokazania w nawigacji — w stałej kolejności katalogu.""" return [f for f in SCREENS if f.key in perms] def normalise(keys) -> frozenset[str]: """Zbiór uprawnień z wejścia formularza: tylko znane klucze, bez ADMIN-a. ADMIN odsiewamy TUTAJ, a nie w handlerze: konto administracyjne pochodzi wyłącznie z konfiguracji środowiska, więc żadne wysłanie formularza — nawet spreparowane — nie może go nadać.""" return frozenset(k for k in (keys or ()) if k in GRANTABLE)