"""Owner login, password reset, and the session helper every owner-scoped route (tickets 6/7/8) will resolve client_id through (#19). Flask's built-in signed-cookie session is "scoped to exactly one client_id": session["client_id"] is set once at login, and login_required is the only sanctioned way a later owner route may read it back -- never from a request param, or a client could simply pass another tenant's client_id. """ import functools from flask import Blueprint, redirect, render_template, request, session, url_for import booking_db as bdb import owner_mail bp = Blueprint("owner_auth", __name__, url_prefix="/owner") def login_required(view): @functools.wraps(view) def wrapped(*args, **kwargs): if not session.get("client_id") or not session.get("user_id"): return redirect(url_for("owner_auth.login")) return view(*args, **kwargs) return wrapped @bp.get("/login") def login(): return render_template("owner/login.html", error=None) @bp.post("/login") def login_submit(): email = (request.form.get("email") or "").strip().lower() password = request.form.get("password") or "" user = bdb.find_user_by_email(email) if user is None or not bdb.verify_password(user, password): return render_template( "owner/login.html", error="E-Mail oder Passwort ist falsch."), 401 session.clear() session["user_id"] = user["user_id"] session["client_id"] = user["client_id"] return redirect(url_for("owner_auth.dashboard")) @bp.get("/logout") def logout(): session.clear() return redirect(url_for("owner_auth.login")) @bp.get("/") @login_required def dashboard(): # Tickets 6/7/8 (calendar, manual booking, settings) build the real # dashboard content on top of this session; this is just the landing # page proving a login resolved to (and is scoped to) one client_id. client = bdb.get_client(session["client_id"]) return render_template("owner/dashboard.html", client=client) @bp.get("/forgot-password") def forgot_password(): return render_template("owner/forgot_password.html", sent=False) @bp.post("/forgot-password") def forgot_password_submit(): email = (request.form.get("email") or "").strip().lower() user = bdb.find_user_by_email(email) if user is not None: client = bdb.get_client(user["client_id"]) tok = bdb.create_password_reset_token(user["user_id"]) owner_mail.send_password_reset(client, user, tok["token"]) # Same response whether or not the email matched a user -- confirming or # denying an email's existence to an anonymous requester is an # account-enumeration leak. return render_template("owner/forgot_password.html", sent=True) @bp.get("/reset-password/") def reset_password(token): return render_template( "owner/reset_password.html", token=token, error=None, done=False) @bp.post("/reset-password/") def reset_password_submit(token): password = request.form.get("password") or "" if len(password) < 8: return render_template( "owner/reset_password.html", token=token, done=False, error="Passwort muss mindestens 8 Zeichen haben."), 400 user_id = bdb.consume_password_reset_token(token, password) if user_id is None: return render_template( "owner/reset_password.html", token=token, done=False, error="Dieser Link ist ungültig oder abgelaufen."), 400 return render_template( "owner/reset_password.html", token=token, error=None, done=True)