Selbst gehostete PWA, die Visitenkarten von einem Foto freistellt, ausliest und als Kontakt bereitstellt. - Stapelscan: OpenCV findet die Kartenrechtecke über mehrere Binärmasken, entzerrt sie perspektivisch und schneidet sie einzeln aus. Ohne Fund gilt das ganze Foto als eine Karte. - Extraktion: ein Aufruf je Zuschnitt an das Vision-Modell mit JSON-Schema. Kein vorgeschaltetes OCR - das würde Layout und Schriftgrößen wegwerfen, aus denen die Feldzuordnung entsteht. - Metadaten: Aufnahmezeit und GPS aus den EXIF-Daten des Fotos, Ortsname über Nominatim, Browserstandort nur als Rückfallebene. - Übersicht mit Volltextsuche und Filtern, Detailansicht mit Korrekturmaske. - Notizfeld je Karte, Erinnerungen per Mail inklusive Nachholen verpasster Termine nach einem Neustart. - vCard 3.0 einzeln und als Sammeldatei, Karte gilt danach als exportiert. - Anmeldung über ein Passwort, Sitzung als signiertes Cookie. - Deployment per Tag-Push nach den Konventionen in DEPLOY.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
50 lines
1.2 KiB
Python
50 lines
1.2 KiB
Python
"""Anmeldung ueber ein einzelnes Passwort, Sitzung als signiertes Cookie."""
|
|
import hashlib
|
|
import hmac
|
|
import time
|
|
|
|
from fastapi import HTTPException, Request
|
|
|
|
from . import config
|
|
|
|
COOKIE = "bcs_session"
|
|
|
|
|
|
def enabled() -> bool:
|
|
return bool(config.APP_PASSWORD)
|
|
|
|
|
|
def _sign(payload: str) -> str:
|
|
return hmac.new(
|
|
config.SECRET_KEY.encode(), payload.encode(), hashlib.sha256
|
|
).hexdigest()
|
|
|
|
|
|
def issue_token() -> str:
|
|
expires = str(int(time.time()) + config.SESSION_DAYS * 86400)
|
|
return f"{expires}.{_sign(expires)}"
|
|
|
|
|
|
def valid_token(token: str | None) -> bool:
|
|
if not token or "." not in token:
|
|
return False
|
|
expires, signature = token.split(".", 1)
|
|
if not hmac.compare_digest(signature, _sign(expires)):
|
|
return False
|
|
try:
|
|
return int(expires) > time.time()
|
|
except ValueError:
|
|
return False
|
|
|
|
|
|
def check_password(candidate: str) -> bool:
|
|
return hmac.compare_digest(candidate, config.APP_PASSWORD)
|
|
|
|
|
|
def require_auth(request: Request) -> None:
|
|
"""FastAPI-Abhaengigkeit fuer alle geschuetzten Endpunkte."""
|
|
if not enabled():
|
|
return
|
|
if not valid_token(request.cookies.get(COOKIE)):
|
|
raise HTTPException(status_code=401, detail="Nicht angemeldet")
|