From dc81c8e2a70564c3561be04d0709692def70e564 Mon Sep 17 00:00:00 2001 From: Bryce Date: Wed, 1 Apr 2026 13:51:25 -0700 Subject: [PATCH] fixes --- admin.py | 72 ++++++++++++- app.py | 4 +- query_projects.py | 209 +++++++++++++++++++++++++++++++++++++ templates/admin_users.html | 23 ++-- templates/base.html | 4 + templates/dashboard.html | 12 +++ 6 files changed, 313 insertions(+), 11 deletions(-) create mode 100755 query_projects.py diff --git a/admin.py b/admin.py index 83d6369..a5de6d3 100644 --- a/admin.py +++ b/admin.py @@ -211,4 +211,74 @@ def register_admin_routes(app): abort(400, "A user with this email already exists") except Exception as e: print(f"[ERR] Failed to create user: {e}") - abort(500, "Failed to create user") \ No newline at end of file + abort(500, "Failed to create user") + + @app.route("/admin/users//become-user", methods=["POST"]) + @admin_required + def become_user(uid): + """Allow admin to impersonate another user by replacing session UID""" + try: + # Verify the target user exists + target_user_doc = db.collection("users").document(uid).get() + if not target_user_doc.exists: + abort(404, "User not found") + + # Store original admin UID and set impersonation flags + session['original_uid'] = session.get('uid') + session['impersonating'] = True + + # Replace session UID with target user's UID + session['uid'] = uid + + print(f"[INFO] Admin {session['original_uid']} is now impersonating user {uid}") + + # Redirect to dashboard + return redirect(url_for('dashboard')) + + except Exception as e: + print(f"[ERR] Failed to become user {uid}: {e}") + abort(500, "Failed to impersonate user") + + @app.route("/admin/users/revert") + def revert_user(): + """Revert back to the original admin session. + + Only accessible if the original session owner (before impersonation) was an admin. + This check must happen before @admin_required decorator to verify the original UID, + not the currently impersonated user's UID. + """ + try: + # Check if user is currently impersonating + if 'original_uid' not in session: + # Not impersonating - check if current user is admin + uid = session.get('uid') + if not uid: + return redirect(url_for('login')) + profile = get_user_profile(uid) + if profile.get('is_admin'): + return redirect(url_for('admin_users')) + # Not an admin, deny access + abort(403, "Access denied. Admin privileges required.") + + # Verify the original session owner was an admin (not the impersonated user) + original_uid = session.get('original_uid') + if not original_uid: + abort(403, "Access denied. Invalid session state.") + + original_profile = get_user_profile(original_uid) + if not original_profile.get('is_admin'): + abort(403, "Access denied. Only admins can revert from impersonation.") + + # Restore original admin UID + session['uid'] = original_uid + session.pop('impersonating', None) + session.pop('original_uid', None) + + print(f"[INFO] Reverted from impersonation back to admin") + + # Redirect to admin users page + return redirect(url_for('admin_users')) + + except Exception as e: + print(f"[ERR] Failed to revert user: {e}") + abort(500, "Failed to revert session") \ No newline at end of file diff --git a/app.py b/app.py index 49195cd..9971418 100644 --- a/app.py +++ b/app.py @@ -175,6 +175,7 @@ def session_login(): if user_profile.get("password_reset_required"): return jsonify({"requires_password_reset": True}) + print(f"logged in as {uid} - user_profile.email") return jsonify({"ok": True}) except Exception as e: print("[ERR] session_login:", e) @@ -270,9 +271,6 @@ def dashboard(page=1): # Read only the current page from Firestore using limit() and offset() import time print(f"Retrieved {len(paginated_rows)} projects from Firestore") - from pprint import pprint - pprint([p['property_contacts'] for p in paginated_rows if p['property_contacts'].get('propertyManager1', None)]) - pprint([p['ProjectId'] for p in paginated_rows ]) # Render table with pagination data return render_template("dashboard.html", rows=paginated_rows, diff --git a/query_projects.py b/query_projects.py new file mode 100755 index 0000000..3723030 --- /dev/null +++ b/query_projects.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +""" +CLI script to query Firebase for projects associated with a user email. + +Usage: + python scripts/query_projects.py --email user@example.com + python scripts/query_projects.py --email user@example.com --limit 10 + python scripts/query_projects.py --email @gmail.com # Domain search +""" + +import argparse +import json +import sys +from typing import List, Dict, Any + +# Add parent directory to path for imports +sys.path.insert(0, "..") + +from dotenv import load_dotenv +load_dotenv() + +from firebase_init import db +from firebase_admin import auth as fb_auth +from utils import get_user_profile + + +def get_uid_from_email(email: str) -> str | None: + """Get Firebase Auth UID from user email.""" + try: + user = fb_auth.get_user_by_email(email) + return user.uid + except fb_auth.UserNotFound: + return None + except Exception as e: + print(f"Error looking up user: {e}") + return None + + +def query_projects_for_user( + uid: str, + case_email: str | None = None, + case_domain_email: str | None = None, + limit: int = 100 +) -> tuple[List[Dict[str, Any]], int]: + """ + Query Firestore for projects associated with a user. + + Args: + uid: Firebase user UID + case_email: Specific case email to filter by (optional) + case_domain_email: Domain to filter by (optional) + limit: Maximum number of projects to return + + Returns: + Tuple of (projects list, total count) + """ + profile = get_user_profile(uid) + + if not profile.get("enabled"): + print(f"Warning: User is not enabled") + + # Determine which filter to use + filter_email = case_email or profile.get("case_email") + filter_domain = case_domain_email or profile.get("case_domain_email") + + if not filter_email and not filter_domain: + print("Error: No case_email or case_domain_email configured for this user") + return ([], 0) + + try: + if filter_domain: + # Domain-based search + domain_lower = filter_domain.lower() + projects_ref = db.collection("projects").where( + "viewing_domains", "array_contains", domain_lower + ) + else: + # Email-based search + email_lower = filter_email.lower() + projects_ref = db.collection("projects").where( + "viewing_emails", "array_contains", email_lower + ) + + # Get total count + total_count = int(projects_ref.count().get()[0][0].value) + + # Get paginated results + projects = [] + for doc in projects_ref.order_by("matter_description").limit(limit).stream(): + projects.append(doc.to_dict()) + + return (projects, total_count) + + except Exception as e: + print(f"Error querying projects: {e}") + return ([], 0) + + +def format_project(project: Dict[str, Any]) -> str: + """Format a project dictionary for display.""" + lines = [ + f"Matter #: {project.get('number', 'N/A')}", + f"Description: {project.get('matter_description', 'N/A')}", + f"Client: {project.get('client', 'N/A')}", + f"Defendant 1: {project.get('defendant_1', 'N/A')}", + f"Case #: {project.get('case_number', 'N/A')}", + f"Premises: {project.get('premises_address', 'N/A')}, {project.get('premises_city', 'N/A')}", + f"Assigned Attorney: {project.get('responsible_attorney', 'N/A')}", + f"Primary Contact: {project.get('staff_person', 'N/A')}", + f"Matter Stage: {project.get('phase_name', 'N/A')}", + f"Documents: {project.get('documents_url', 'N/A')}", + ] + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser( + description="Query Firebase for projects associated with a user email" + ) + parser.add_argument( + "--email", "-e", + required=True, + help="User email address (e.g., user@example.com or @domain.com for domain search)" + ) + parser.add_argument( + "--limit", "-l", + type=int, + default=100, + help="Maximum number of projects to return (default: 100)" + ) + parser.add_argument( + "--json", "-j", + action="store_true", + help="Output results as JSON" + ) + parser.add_argument( + "--verbose", "-v", + action="store_true", + help="Show detailed project information" + ) + parser.add_argument( + "--case-email", + help="Override case email filter (use user's case_email by default)" + ) + parser.add_argument( + "--case-domain", + help="Override case domain filter (use user's case_domain_email by default)" + ) + + args = parser.parse_args() + + # Get UID from email + uid = get_uid_from_email(args.email) + if not uid: + print(f"Error: User not found with email '{args.email}'") + sys.exit(1) + + # Get user profile + profile = get_user_profile(uid) + + print(f"\nUser: {profile.get('user_email', 'N/A')}") + print(f"UID: {uid}") + print(f"Enabled: {profile.get('enabled', False)}") + print(f"Is Admin: {profile.get('is_admin', False)}") + print(f"Case Email: {profile.get('case_email', 'N/A')}") + print(f"Case Domain Email: {profile.get('case_domain_email', 'N/A')}") + + # Query projects + projects, total_count = query_projects_for_user( + uid=uid, + case_email=args.case_email, + case_domain_email=args.case_domain, + limit=args.limit + ) + + print(f"\nFound {total_count} projects (showing {min(len(projects), args.limit)})") + + if args.json: + # Output as JSON + result = { + "user": { + "email": profile.get("user_email"), + "uid": uid, + "enabled": profile.get("enabled"), + "is_admin": profile.get("is_admin"), + "case_email": profile.get("case_email"), + "case_domain_email": profile.get("case_domain_email") + }, + "total_count": total_count, + "projects": projects + } + print(json.dumps(result, indent=2, default=str)) + else: + # Human-readable output + for i, project in enumerate(projects, 1): + print(f"\n{'='*60}") + print(f"Project {i}:") + print('='*60) + if args.verbose: + print(format_project(project)) + else: + print(f" Matter #: {project.get('number', 'N/A')}") + print(f" Description: {project.get('matter_description', 'N/A')}") + print(f" Client: {project.get('client', 'N/A')}") + print(f" Case #: {project.get('case_number', 'N/A')}") + + +if __name__ == "__main__": + main() diff --git a/templates/admin_users.html b/templates/admin_users.html index d2a99db..5bd708e 100644 --- a/templates/admin_users.html +++ b/templates/admin_users.html @@ -77,13 +77,22 @@ {{ user.case_email }} -
- -
+
+
+ +
+
+ +
+
{% else %} diff --git a/templates/base.html b/templates/base.html index 9ddab36..43e7e15 100644 --- a/templates/base.html +++ b/templates/base.html @@ -18,11 +18,15 @@ Rothbard Law Group - Cases