app.py auf APIRouter-Module umgestellt (db.py, templating.py, anode_client.py, auth.py, routers/geraete.py + vier Admin-Router als Platzhalter). benutzer.ist_admin (Migration 0002) + require_admin- Dependency (403 ohne Admin-Flag). Admin-Nav-Link im Header nur für Admin-Konten sichtbar. create_user.py um --admin-Flag erweitert. Die vier Admin-Bereiche (Technik/Datenbank, Geräte, Flows, Kunden) sind als Platzhalterseiten unter /admin/... verdrahtet und bekommen in den nächsten Schritten echte Funktionalität. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
74 lines
2.1 KiB
Python
74 lines
2.1 KiB
Python
import bcrypt
|
|
from fastapi import APIRouter, Depends, Form, HTTPException, Request
|
|
from fastapi.responses import RedirectResponse
|
|
|
|
from db import get_database_connection
|
|
from templating import templates
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def get_current_user(request: Request) -> dict:
|
|
user = request.session.get("user")
|
|
|
|
if user is None:
|
|
raise HTTPException(status_code=303, headers={"Location": "/login"})
|
|
|
|
return user
|
|
|
|
|
|
def require_admin(user: dict = Depends(get_current_user)) -> dict:
|
|
if not user.get("ist_admin"):
|
|
raise HTTPException(status_code=403, detail="Kein Admin-Zugriff.")
|
|
|
|
return user
|
|
|
|
|
|
@router.get("/login")
|
|
def login_form(request: Request):
|
|
if request.session.get("user") is not None:
|
|
return RedirectResponse("/geraete", status_code=303)
|
|
|
|
return templates.TemplateResponse(request, "login.html", {"error": None})
|
|
|
|
|
|
@router.post("/login")
|
|
def login_submit(request: Request, email: str = Form(...), password: str = Form(...)):
|
|
with get_database_connection() as conn:
|
|
with conn.cursor() as cur:
|
|
cur.execute(
|
|
"SELECT id, organization_id, password_hash, ist_admin FROM benutzer WHERE email = %s",
|
|
(email,),
|
|
)
|
|
row = cur.fetchone()
|
|
|
|
if row is None:
|
|
return templates.TemplateResponse(
|
|
request, "login.html", {"error": "E-Mail oder Passwort ist falsch."}, status_code=401
|
|
)
|
|
|
|
# psycopg liefert TEXT-Spalten hier als bytes zurück, nicht als str.
|
|
stored_hash = row[2] if isinstance(row[2], bytes) else row[2].encode("utf-8")
|
|
|
|
if not bcrypt.checkpw(password.encode("utf-8"), stored_hash):
|
|
return templates.TemplateResponse(
|
|
request, "login.html", {"error": "E-Mail oder Passwort ist falsch."}, status_code=401
|
|
)
|
|
|
|
request.session["user"] = {
|
|
"id": str(row[0]),
|
|
"organization_id": str(row[1]),
|
|
"email": email,
|
|
"ist_admin": row[3],
|
|
}
|
|
|
|
return RedirectResponse("/geraete", status_code=303)
|
|
|
|
|
|
@router.get("/logout")
|
|
def logout(request: Request):
|
|
request.session.clear()
|
|
|
|
return RedirectResponse("/login", status_code=303)
|