fixes
This commit is contained in:
72
admin.py
72
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")
|
||||
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
4
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,
|
||||
|
||||
209
query_projects.py
Executable file
209
query_projects.py
Executable 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()
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user