feat(auth): required login barrier with 1-click demo access and session management
This commit is contained in:
+199
-30
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user