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:
2026-08-03 17:00:37 +02:00
parent 644c99ee30
commit 59c35ce39f
14 changed files with 635 additions and 0 deletions
+31
View File
@@ -273,6 +273,37 @@ def get_user_by_email(client_id, email):
return cur.fetchone()
def find_user_by_email(email):
"""Login lookup (#19): users.email is globally unique, and at login time
there's no client_id yet to scope by -- resolving client_id is exactly
what a successful login establishes for the session."""
with db.connect() as conn, conn.cursor() as cur:
cur.execute("SELECT * FROM users WHERE email = %s", (email,))
return cur.fetchone()
def get_user(user_id):
with db.connect() as conn, conn.cursor() as cur:
cur.execute("SELECT * FROM users WHERE user_id = %s", (user_id,))
return cur.fetchone()
def list_users(client_id=None):
"""Owner accounts for the CRM operator dashboard (#19). Unlike the rest
of this module, this one is deliberately allowed to span every tenant
when client_id is omitted -- an operator manages all clients, not one."""
with db.connect() as conn, conn.cursor() as cur:
if client_id is None:
cur.execute(
"SELECT user_id, client_id, email, created_at FROM users "
"ORDER BY client_id, email")
else:
cur.execute(
"SELECT user_id, client_id, email, created_at FROM users "
"WHERE client_id = %s ORDER BY email", (client_id,))
return cur.fetchall()
def verify_password(user, password):
return check_password_hash(user["password_hash"], password)