Langlebige API-Tokens fuer externe Zugriffe (Home Assistant)

Die Einkaufsliste war ueber /shopping-list und /shopping-list/groups schon
abrufbar, aber nur mit einem Login-JWT, das nach 7 Tagen ablaeuft - fuer HA
unpraktisch.

- Neue Tabelle api_tokens (Name, SHA-256-Hash, Ersteller, zuletzt genutzt).
  Der Klartext wird nur einmal beim Anlegen zurueckgegeben.
- get_current_user akzeptiert jetzt JWT ODER API-Token im Authorization-Header;
  bei Nutzung wird last_used_at fortgeschrieben.
- Endpunkte /api-tokens (Admin): auflisten, erzeugen, widerrufen.
- Einstellungsseite: Tokens verwalten inkl. einmaliger Anzeige und einem
  aufklappbaren Konfigurationsbeispiel fuer Home Assistant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-22 16:36:17 +02:00
parent 93e487a1b0
commit faa9d6a25d
8 changed files with 240 additions and 7 deletions

View File

@@ -2,9 +2,11 @@ from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from datetime import datetime, timezone
from .database import get_db
from .models import Role, User
from .security import decode_access_token
from .models import ApiToken, Role, User
from .security import decode_access_token, hash_api_token
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
@@ -18,15 +20,28 @@ _credentials_exc = HTTPException(
def get_current_user(
token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)
) -> User:
"""Akzeptiert ein JWT aus dem Login oder ein langlebiges API-Token."""
payload = decode_access_token(token)
if payload is None:
if payload is not None:
username = payload.get("sub")
if not username:
raise _credentials_exc
user = db.query(User).filter(User.username == username).first()
if user is None:
raise _credentials_exc
return user
# Kein gültiges JWT als API-Token prüfen (z.B. Home Assistant).
entry = (
db.query(ApiToken).filter(ApiToken.token_hash == hash_api_token(token)).first()
)
if entry is None:
raise _credentials_exc
username = payload.get("sub")
if not username:
raise _credentials_exc
user = db.query(User).filter(User.username == username).first()
user = db.get(User, entry.user_id) if entry.user_id else None
if user is None:
raise _credentials_exc
entry.last_used_at = datetime.now(timezone.utc)
db.commit()
return user