528a13ca7c
Adds an owner-authenticated weekly agenda (grouped by day, today highlighted) with manual walk-in/phone booking creation, cancel, and reschedule -- all routed through booking_api.py's create/cancel/reschedule logic (refactored into shared helpers) so the EXCLUDE overlap constraint and confirmation email stay on the single existing code path. Manual creation can skip the opening-hours/min-notice/max-advance/buffer checks via an explicit override, but never the overlap constraint itself. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
101 lines
3.4 KiB
Python
101 lines
3.4 KiB
Python
"""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 7/8 (settings, notify channel) still build on top of this
|
|
# session; the agenda/calendar (#20) now lives at owner_booking.agenda.
|
|
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/<token>")
|
|
def reset_password(token):
|
|
return render_template(
|
|
"owner/reset_password.html", token=token, error=None, done=False)
|
|
|
|
|
|
@bp.post("/reset-password/<token>")
|
|
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)
|