Unter Einstellungen gibt es jetzt eine Karte "Mealie (Rezepte)": Adresse, Benutzername, Passwort. Vorrania meldet sich damit EINMAL an (POST /api/auth/token), laesst sich von Mealie ueber POST /api/users/api-tokens ein Dauertoken ausstellen und verwirft das Passwort sofort - es steht in keiner Zeile der Datenbank, in keiner Antwort und in keinem Fehlertext. Wer sein Passwort gar nicht erst eintippen will, haekelt "Ich gebe lieber selbst ein Token ein" an. Das Token liegt NICHT in den Einstellungen: GET /settings liefert jedem angemeldeten Nutzer alle Zeilen im Klartext. Eine Ausschlussliste dort waere beim naechsten Geheimnis vergessen, deshalb eine eigene Tabelle ohne jeden Leseweg - setzen, loeschen, und die Auskunft "ist gesetzt". Was keinen Ausgang hat, kann nicht durchrutschen. Ein Test prueft genau das. Der Dienst folgt services/oidc.py, nicht off.py: Bei Open Food Facts ist "kein Treffer" der Normalfall und wird still zu None, hier ist "Mealie antwortet nicht" ein Fehler, den der Nutzer sehen muss. Die Kennungen trennen auch, was verschieden ist - ein falsches Passwort (400) ist etwas anderes als ein weggetretener Server (502). Mealie 1.x legt die Haushalts-Routen unter /api/groups/, 2.x unter /api/households/. Erkannt wird das zur Laufzeit mit einer Anfrage und dann eine Stunde gepuffert; ueber die Versionsnummer waere es unzuverlaessig, die melden Installationen uneinheitlich. Rezeptliste und -abgleich kommen als Naechstes; dies ist der Teil, mit dem sich der Server ueberhaupt erst verbinden laesst. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
150 lines
4.9 KiB
Python
150 lines
4.9 KiB
Python
"""Mealie einrichten und prüfen.
|
||
|
||
Die Zugangsdaten nimmt dieser Router entgegen, gibt sie aber nie zurück. Die
|
||
Oberfläche erfährt nur „eingerichtet ja/nein" — dasselbe Muster wie bei
|
||
``GET /auth/oidc/providers``, wo Client-Secrets ebenfalls nie in der Antwort
|
||
stehen.
|
||
|
||
Bewusst eigene Routen mit JSON-Rumpf statt ``PUT /settings/{key}?value=``:
|
||
Query-Parameter landen in Zugriffsprotokollen, und ein Passwort hat dort
|
||
nichts zu suchen.
|
||
"""
|
||
|
||
# ACHTUNG: Kein ``from __future__ import annotations`` – siehe package_types.py.
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, status
|
||
from sqlalchemy.orm import Session
|
||
|
||
from ..database import get_db
|
||
from ..deps import get_current_user, require_admin
|
||
from ..models import Setting, User
|
||
from ..schemas import (
|
||
MealieAvailable,
|
||
MealieConfigIn,
|
||
MealieStatus,
|
||
MealieTokenIn,
|
||
)
|
||
from ..services import mealie
|
||
|
||
router = APIRouter(prefix="/mealie", tags=["mealie"])
|
||
|
||
|
||
def _setze_adresse(db: Session, base_url: str) -> str:
|
||
sauber = base_url.strip().rstrip("/")
|
||
row = db.get(Setting, mealie.BASE_URL_KEY)
|
||
if row is None:
|
||
db.add(Setting(key=mealie.BASE_URL_KEY, value=sauber))
|
||
else:
|
||
row.value = sauber
|
||
db.commit()
|
||
return sauber
|
||
|
||
|
||
def _status(db: Session, pruefen: bool) -> MealieStatus:
|
||
adresse = mealie.basis_adresse(db)
|
||
hat_token = bool(mealie.token(db))
|
||
status_out = MealieStatus(
|
||
configured=bool(adresse) and hat_token,
|
||
base_url=adresse,
|
||
token_set=hat_token,
|
||
)
|
||
if not pruefen or not status_out.configured:
|
||
return status_out
|
||
try:
|
||
status_out.version = mealie.version(db)
|
||
# Eine echte Abfrage – „erreichbar" soll heissen, dass auch das Token gilt.
|
||
mealie.rezepte(db, pro_seite=1)
|
||
status_out.reachable = True
|
||
except mealie.MealieFehler as fehler:
|
||
status_out.reachable = False
|
||
status_out.error = str(fehler)
|
||
return status_out
|
||
|
||
|
||
@router.get("/status", response_model=MealieStatus)
|
||
def mealie_status(
|
||
pruefen: bool = False,
|
||
db: Session = Depends(get_db),
|
||
_: User = Depends(require_admin),
|
||
) -> MealieStatus:
|
||
"""Was eingerichtet ist – ohne das Token. ``pruefen=true`` fragt Mealie an."""
|
||
return _status(db, pruefen)
|
||
|
||
|
||
@router.get("/available", response_model=MealieAvailable)
|
||
def mealie_available(
|
||
db: Session = Depends(get_db),
|
||
_: User = Depends(get_current_user),
|
||
) -> MealieAvailable:
|
||
"""Ob die Rezept-Seite etwas anzeigen kann.
|
||
|
||
Für alle angemeldeten Benutzer, damit die Oberfläche den Menüpunkt nur dann
|
||
zeigt. Anders als bei den Anmeldeanbietern **mit** Anmeldung: Es gibt keinen
|
||
Grund, Unbekannten zu verraten, dass hier eine Mealie-Instanz hängt.
|
||
"""
|
||
return MealieAvailable(available=mealie.eingerichtet(db))
|
||
|
||
|
||
@router.put("/config", response_model=MealieStatus)
|
||
def mealie_konfigurieren(
|
||
payload: MealieConfigIn,
|
||
db: Session = Depends(get_db),
|
||
_: User = Depends(require_admin),
|
||
) -> MealieStatus:
|
||
"""Mit Benutzername und Passwort anmelden und ein Dauertoken holen.
|
||
|
||
Das Passwort wird nur für diese eine Anmeldung benutzt und danach verworfen
|
||
– es steht in keiner Zeile der Datenbank und in keiner Antwort.
|
||
"""
|
||
try:
|
||
dauertoken = mealie.hole_dauertoken(
|
||
payload.base_url, payload.username, payload.password
|
||
)
|
||
except mealie.MealieFehler as fehler:
|
||
raise HTTPException(_http_code(fehler), str(fehler)) from fehler
|
||
_setze_adresse(db, payload.base_url)
|
||
mealie.speichere_token(db, dauertoken)
|
||
return _status(db, pruefen=True)
|
||
|
||
|
||
@router.put("/token", response_model=MealieStatus)
|
||
def mealie_token_setzen(
|
||
payload: MealieTokenIn,
|
||
db: Session = Depends(get_db),
|
||
_: User = Depends(require_admin),
|
||
) -> MealieStatus:
|
||
"""Ein in Mealie selbst erzeugtes Dauertoken einsetzen.
|
||
|
||
Für alle, die ihr Passwort nicht eintippen wollen: In Mealie unter Profil ▸
|
||
API-Tokens eines anlegen und hier einfügen.
|
||
"""
|
||
_setze_adresse(db, payload.base_url)
|
||
mealie.speichere_token(db, payload.token.strip())
|
||
return _status(db, pruefen=True)
|
||
|
||
|
||
@router.delete("/config", status_code=status.HTTP_204_NO_CONTENT)
|
||
def mealie_trennen(
|
||
db: Session = Depends(get_db),
|
||
_: User = Depends(require_admin),
|
||
) -> None:
|
||
"""Verbindung lösen: Adresse und Token hier entfernen.
|
||
|
||
Das Token selbst bleibt in Mealie bestehen – wer es dort endgültig los sein
|
||
will, löscht es unter Profil ▸ API-Tokens.
|
||
"""
|
||
mealie.loesche_zugang(db)
|
||
|
||
|
||
def _http_code(fehler: mealie.MealieFehler) -> int:
|
||
"""Fehlerkennung auf einen Statuscode abbilden.
|
||
|
||
Getrennt, weil „Mealie ist weg" (502) etwas anderes ist als „dein Passwort
|
||
stimmt nicht" (400) – und die Oberfläche beides verschieden erklären soll.
|
||
"""
|
||
if fehler.kennung in ("anmeldung_fehlgeschlagen",):
|
||
return status.HTTP_400_BAD_REQUEST
|
||
if fehler.kennung == "nicht_eingerichtet":
|
||
return status.HTTP_503_SERVICE_UNAVAILABLE
|
||
return status.HTTP_502_BAD_GATEWAY
|