Files
smb-online/backoffice/app/owner_auth.py
T
mivanchenko 528a13ca7c Owner calendar view + manual booking + owner cancel/reschedule (#20)
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>
2026-08-03 17:16:19 +02:00

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)