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:
@@ -17,16 +17,26 @@ from decimal import Decimal
|
||||
from flask import Flask, jsonify, request, Response
|
||||
from waitress import serve
|
||||
|
||||
import booking_db as bdb
|
||||
import db
|
||||
import owner_mail
|
||||
from sheets import Sheets
|
||||
from booking_api import bp as booking_bp
|
||||
from public_booking import bp as public_booking_bp
|
||||
from manage_booking import bp as manage_booking_bp
|
||||
from owner_auth import bp as owner_auth_bp
|
||||
|
||||
app = Flask(__name__, static_folder="static", static_url_path="")
|
||||
app.register_blueprint(booking_bp)
|
||||
app.register_blueprint(public_booking_bp)
|
||||
app.register_blueprint(manage_booking_bp)
|
||||
app.register_blueprint(owner_auth_bp)
|
||||
|
||||
# Dedicated secret for the owner-login session cookie -- deliberately not
|
||||
# shared with CRM_API_TOKEN or BOOKING_TOKEN_SECRET (#19), same reasoning as
|
||||
# booking_api.py's own token secret: rotating one must never silently affect
|
||||
# the others.
|
||||
app.secret_key = os.environ.get("SESSION_SECRET_KEY", "")
|
||||
|
||||
CRM_TOKEN = os.environ.get("CRM_API_TOKEN", "")
|
||||
# Separate read-only token for the public iCal feed (calendar apps can't send
|
||||
@@ -272,6 +282,37 @@ def delete_entity(entity, ident):
|
||||
return jsonify({"deleted": ident})
|
||||
|
||||
|
||||
@app.get("/api/owner_users")
|
||||
def list_owner_users():
|
||||
"""Owner-accounts list for the CRM dashboard's new tab (#19). Requires
|
||||
the CRM token to read, same as credentials -- these rows identify who can
|
||||
log into a client's owner portal."""
|
||||
if not authed():
|
||||
return jsonify({"error": "forbidden"}), 403
|
||||
rows = bdb.list_users()
|
||||
return jsonify({"entity": "owner_users", "count": len(rows), "rows": rows_json(rows)})
|
||||
|
||||
|
||||
@app.post("/api/owner_users/<user_id>/reset-password")
|
||||
def trigger_owner_password_reset(user_id):
|
||||
"""Operator-triggered reset (#19): sends the same reset email an owner
|
||||
would send themselves via /owner/forgot-password."""
|
||||
if not authed():
|
||||
return jsonify({"error": "forbidden"}), 403
|
||||
user = bdb.get_user(user_id)
|
||||
if user is None:
|
||||
return jsonify({"error": "not found"}), 404
|
||||
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"])
|
||||
with db.connect() as conn, conn.cursor() as cur:
|
||||
log_activity(cur, user["client_id"], "owner password reset",
|
||||
f"user_id={user_id} (operator-triggered)")
|
||||
conn.commit()
|
||||
mirror_async("activity_log")
|
||||
return jsonify({"sent": user_id})
|
||||
|
||||
|
||||
@app.post("/api/sync")
|
||||
def sync_all():
|
||||
"""Full DB -> Sheets resync of every tab (manual / alignment)."""
|
||||
|
||||
Reference in New Issue
Block a user