Owner accounts: auth, password reset, CRM dashboard visibility (#19)
Flask-session login scoped to one client_id (never a request param), self-service + operator-triggered password reset via single-use tokens, and an Owner accounts tab on the CRM dashboard. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
"""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/<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)
|
||||
Reference in New Issue
Block a user