feat(auth): required login barrier with 1-click demo access and session management

This commit is contained in:
2026-09-21 10:28:08 +02:00
parent 8200857754
commit 4f5ef164c3
6 changed files with 547 additions and 46 deletions
+199 -30
View File
@@ -1,29 +1,36 @@
import os
from datetime import datetime
from typing import Optional
from typing import Optional, Dict
from decimal import Decimal
from fastapi import FastAPI, Request, Depends, HTTPException, Form, status
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from starlette.middleware.sessions import SessionMiddleware
from sqlalchemy.orm import Session
from .database import get_db
from .models import Tenant, User, Product, Event, Order, OrderItem
from .auth import hash_password, verify_password
app = FastAPI(title="Schmankerl – Das moderne Brotzeitportal")
# Session Middleware für sichere Login-Cookies
app.add_middleware(
SessionMiddleware,
secret_key=os.getenv("SECRET_KEY", "schmankerl-secret-key-waiswuaschd-2026!"),
session_cookie="schmankerl_session",
max_age=86400 * 30 # 30 Tage
)
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
app.mount("/static", StaticFiles(directory=os.path.join(BASE_DIR, "static")), name="static")
templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "templates"))
def resolve_tenant(request: Request, db: Session) -> Optional[Tenant]:
"""
Ermittelt den Mandanten anhand des Host-Headers (Subdomain)
z.B. demo.dev.waiswuaschd.de -> slug='demo'
"""
"""Ermittelt den Mandanten anhand der Subdomain (Host-Header)."""
host = request.headers.get("host", "").split(":")[0].lower()
parts = host.split(".")
@@ -41,12 +48,55 @@ def resolve_tenant(request: Request, db: Session) -> Optional[Tenant]:
return tenant
def get_current_user(request: Request, db: Session) -> Optional[User]:
"""Liest den eingeloggten Benutzer aus der Session."""
user_id = request.session.get("user_id")
if not user_id:
return None
return db.query(User).filter(User.id == user_id, User.is_active == True).first()
def seed_demo_users_if_needed(tenant: Tenant, db: Session):
"""Stellt sicher, dass die Standard-Accounts für die Demo existieren."""
demo_user = db.query(User).filter(User.tenant_id == tenant.id, User.email == "maxi@waiswuaschd.de").first()
if not demo_user:
demo_user = User(
tenant_id=tenant.id,
email="maxi@waiswuaschd.de",
username="maxi",
first_name="Maxi",
last_name="Mustermann",
password_hash=hash_password("demo123"),
role="user"
)
db.add(demo_user)
admin_user = db.query(User).filter(User.tenant_id == tenant.id, User.email == "admin@waiswuaschd.de").first()
if not admin_user:
admin_user = User(
tenant_id=tenant.id,
email="admin@waiswuaschd.de",
username="admin",
first_name="Franz",
last_name="Xaver",
password_hash=hash_password("admin123"),
role="admin"
)
db.add(admin_user)
db.commit()
@app.get("/", response_class=HTMLResponse)
def index(request: Request, db: Session = Depends(get_db)):
tenant = resolve_tenant(request, db)
if not tenant:
return templates.TemplateResponse(request, "not_found_tenant.html", context={})
current_user = get_current_user(request, db)
if not current_user:
# Require Login: Weiterleitung zur Anmeldeseite
return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER)
# Aktuelle offene Runde suchen
event = db.query(Event).filter(
Event.tenant_id == tenant.id,
@@ -59,33 +109,154 @@ def index(request: Request, db: Session = Depends(get_db)):
Product.is_active == True
).order_by(Product.sort_order.asc(), Product.id.asc()).all()
# Bestehende Bestellungen für die Event-Runde zählen
# Bestehende Bestellung des Users in diesem Event
existing_order = None
user_items: Dict[int, int] = {}
if event:
existing_order = db.query(Order).filter(
Order.tenant_id == tenant.id,
Order.event_id == event.id,
Order.user_id == current_user.id
).first()
if existing_order:
for it in existing_order.items:
user_items[it.product_id] = it.quantity
# Bestellungen insgesamt zählen
order_count = 0
if event:
order_count = db.query(Order).filter(Order.event_id == event.id).count()
return templates.TemplateResponse(request, "index.html", context={
"tenant": tenant,
"current_user": current_user,
"event": event,
"products": products,
"existing_order": existing_order,
"user_items": user_items,
"order_count": order_count,
"now": datetime.now()
})
@app.post("/order")
async def place_order(
@app.get("/login", response_class=HTMLResponse)
def login_page(request: Request, db: Session = Depends(get_db), error: Optional[str] = None, success: Optional[str] = None):
tenant = resolve_tenant(request, db)
if tenant and tenant.slug == "demo":
seed_demo_users_if_needed(tenant, db)
current_user = get_current_user(request, db)
if current_user:
return RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER)
return templates.TemplateResponse(request, "login.html", context={
"tenant": tenant,
"error": error,
"success": success,
"current_user": None
})
@app.post("/login")
def login(
request: Request,
first_name: str = Form(...),
last_name: str = Form(...),
email: str = Form(...),
event_id: int = Form(...),
password: str = Form(...),
db: Session = Depends(get_db)
):
tenant = resolve_tenant(request, db)
clean_email = email.strip().lower()
user = db.query(User).filter(User.tenant_id == tenant.id, User.email == clean_email).first()
if not user or not verify_password(password, user.password_hash):
return templates.TemplateResponse(request, "login.html", context={
"tenant": tenant,
"error": "Ungültige E-Mail-Adresse oder falsches Passwort.",
"current_user": None
}, status_code=400)
# Session setzen
request.session["user_id"] = user.id
return RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER)
@app.post("/login/quick")
def quick_demo_login(
request: Request,
role: str = Form(...),
db: Session = Depends(get_db)
):
tenant = resolve_tenant(request, db)
if not tenant:
raise HTTPException(status_code=404, detail="Mandant nicht gefunden")
seed_demo_users_if_needed(tenant, db)
target_email = "admin@waiswuaschd.de" if role == "admin" else "maxi@waiswuaschd.de"
user = db.query(User).filter(User.tenant_id == tenant.id, User.email == target_email).first()
if not user:
raise HTTPException(status_code=500, detail="Demo-Benutzer konnte nicht geladen werden")
request.session["user_id"] = user.id
if role == "admin":
return RedirectResponse(url="/admin", status_code=status.HTTP_303_SEE_OTHER)
return RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER)
@app.post("/register")
def register(
request: Request,
first_name: str = Form(...),
last_name: str = Form(...),
email: str = Form(...),
password: str = Form(...),
db: Session = Depends(get_db)
):
tenant = resolve_tenant(request, db)
clean_email = email.strip().lower()
existing = db.query(User).filter(User.tenant_id == tenant.id, User.email == clean_email).first()
if existing:
return templates.TemplateResponse(request, "login.html", context={
"tenant": tenant,
"error": "Diese E-Mail-Adresse ist bereits registriert. Bitte melde dich an.",
"current_user": None
}, status_code=400)
username = clean_email.split("@")[0]
new_user = User(
tenant_id=tenant.id,
email=clean_email,
username=username,
first_name=first_name.strip(),
last_name=last_name.strip(),
password_hash=hash_password(password),
role="user"
)
db.add(new_user)
db.commit()
request.session["user_id"] = new_user.id
return RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER)
@app.get("/logout")
def logout(request: Request):
request.session.clear()
return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER)
@app.post("/order")
async def place_order(
request: Request,
event_id: int = Form(...),
db: Session = Depends(get_db)
):
tenant = resolve_tenant(request, db)
current_user = get_current_user(request, db)
if not current_user:
return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER)
event = db.query(Event).filter(Event.id == event_id, Event.tenant_id == tenant.id).first()
if not event or event.status != "open":
raise HTTPException(status_code=400, detail="Diese Bestellrunde ist bereits geschlossen")
@@ -93,23 +264,7 @@ async def place_order(
if event.deadline < datetime.now():
raise HTTPException(status_code=400, detail="Die Bestellfrist ist leider abgelaufen!")
# User ermitteln oder anlegen
clean_email = email.strip().lower()
user = db.query(User).filter(User.tenant_id == tenant.id, User.email == clean_email).first()
if not user:
username = clean_email.split("@")[0]
user = User(
tenant_id=tenant.id,
email=clean_email,
username=username,
first_name=first_name.strip(),
last_name=last_name.strip(),
role="user"
)
db.add(user)
db.flush()
# Formulardaten auslesen (Mengen je Produkt)
# Formulardaten auslesen
form_data = await request.form()
order_items = []
total_price = Decimal("0.00")
@@ -138,7 +293,7 @@ async def place_order(
existing_order = db.query(Order).filter(
Order.tenant_id == tenant.id,
Order.event_id == event.id,
Order.user_id == user.id
Order.user_id == current_user.id
).first()
if existing_order:
@@ -153,7 +308,7 @@ async def place_order(
new_order = Order(
tenant_id=tenant.id,
event_id=event.id,
user_id=user.id,
user_id=current_user.id,
total_price=total_price,
is_paid=False
)
@@ -171,12 +326,17 @@ async def place_order(
@app.get("/order/{order_id}/success", response_class=HTMLResponse)
def order_success(order_id: int, request: Request, db: Session = Depends(get_db)):
tenant = resolve_tenant(request, db)
current_user = get_current_user(request, db)
if not current_user:
return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER)
order = db.query(Order).filter(Order.id == order_id).first()
if not order:
raise HTTPException(status_code=404, detail="Bestellung nicht gefunden")
return templates.TemplateResponse(request, "order_success.html", context={
"tenant": tenant,
"current_user": current_user,
"order": order
})
@@ -187,6 +347,10 @@ def admin_dashboard(request: Request, db: Session = Depends(get_db)):
if not tenant:
return templates.TemplateResponse(request, "not_found_tenant.html", context={})
current_user = get_current_user(request, db)
if not current_user:
return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER)
# Neuestes Event finden
event = db.query(Event).filter(Event.tenant_id == tenant.id).order_by(Event.id.desc()).first()
@@ -211,6 +375,7 @@ def admin_dashboard(request: Request, db: Session = Depends(get_db)):
return templates.TemplateResponse(request, "admin.html", context={
"tenant": tenant,
"current_user": current_user,
"event": event,
"orders": orders,
"item_sums": item_sums,
@@ -221,6 +386,10 @@ def admin_dashboard(request: Request, db: Session = Depends(get_db)):
@app.post("/admin/toggle-paid/{order_id}")
def toggle_paid(order_id: int, request: Request, db: Session = Depends(get_db)):
current_user = get_current_user(request, db)
if not current_user:
return RedirectResponse(url="/login", status_code=status.HTTP_303_SEE_OTHER)
order = db.query(Order).filter(Order.id == order_id).first()
if order:
order.is_paid = not order.is_paid