This commit is contained in:
2026-04-01 13:51:25 -07:00
parent c3263a0eaf
commit dc81c8e2a7
6 changed files with 313 additions and 11 deletions

View File

@@ -212,3 +212,73 @@ def register_admin_routes(app):
except Exception as e:
print(f"[ERR] Failed to create user: {e}")
abort(500, "Failed to create user")
@app.route("/admin/users/<uid>/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")

4
app.py
View File

@@ -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,

209
query_projects.py Executable file
View File

@@ -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()

View File

@@ -77,13 +77,22 @@
</td>
<td class="px-4 py-3 text-sm text-slate-800">{{ user.case_email }}</td>
<td class="px-4 py-3 text-sm text-slate-800">
<form method="POST" action="/admin/users/{{ user.uid }}/reset-password" style="display: inline;">
<button type="submit"
class="text-blue-600 hover:text-blue-800 text-sm font-medium underline"
onclick="return confirm('Are you sure you want to reset the password for {{ user.user_email }}? This will send a password reset email to their account.')">
Reset Password
</button>
</form>
<div class="flex flex-col space-y-1">
<form method="POST" action="/admin/users/{{ user.uid }}/become-user" style="display: inline;">
<button type="submit"
class="text-green-600 hover:text-green-800 text-sm font-medium underline"
onclick="return confirm('Are you sure you want to view as {{ user.user_email }}? This will replace your current session.')">
Become User
</button>
</form>
<form method="POST" action="/admin/users/{{ user.uid }}/reset-password" style="display: inline;">
<button type="submit"
class="text-blue-600 hover:text-blue-800 text-sm font-medium underline"
onclick="return confirm('Are you sure you want to reset the password for {{ user.user_email }}? This will send a password reset email to their account.')">
Reset Password
</button>
</form>
</div>
</td>
</tr>
{% else %}

View File

@@ -18,11 +18,15 @@
<a href="/" class="font-semibold">Rothbard Law Group - Cases</a>
<nav class="space-x-4">
{% if session.uid %}
{% if session.impersonating %}
<a href="/admin/users/revert" class="text-sm text-orange-600 hover:text-orange-900 font-medium">Revert to Admin</a>
{% else %}
<a href="/dashboard" class="text-sm text-slate-600 hover:text-slate-900">Dashboard</a>
{% set profile = get_user_profile(session.uid) %}
{% if profile.is_admin %}
<a href="/admin/users" class="text-sm text-slate-600 hover:text-slate-900">Admin Users</a>
{% endif %}
{% endif %}
<a href="/logout" class="text-sm text-slate-600 hover:text-slate-900">Logout</a>
{% else %}
<a href="/login" class="text-sm text-slate-600 hover:text-slate-900">Login</a>

View File

@@ -1,6 +1,18 @@
{% extends 'base.html' %}
{% block content %}
<div class="h-full flex flex-col" x-data="columnConfig()">
{% if session.impersonating %}
{% set impersonated_profile = get_user_profile(session.uid) %}
<div class="bg-orange-50 border border-orange-200 text-orange-800 px-4 py-3 rounded-md mb-4 flex justify-between items-center">
<div>
<p class="font-medium">Viewing as: {{ impersonated_profile.user_email }}</p>
<p class="text-sm mt-1">You are impersonating this user. Click "Revert to Admin" in the navigation to return to your admin account.</p>
</div>
<a href="/admin/users/revert" class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-orange-700 bg-orange-100 hover:bg-orange-200 rounded-md transition-colors">
Revert to Admin
</a>
</div>
{% endif %}
{% if case_email %}
<h1 class="text-xl font-semibold mb-4">Projects for {{ case_email }}</h1>
{% else %}