import json import os from functools import wraps from datetime import datetime, timedelta from flask import Flask, render_template, request, redirect, url_for, session, abort, jsonify from dotenv import load_dotenv import firebase_admin from firebase_admin import credentials, auth as fb_auth, firestore import requests load_dotenv() app = Flask(__name__) app.secret_key = os.environ.get("FLASK_SECRET_KEY", os.urandom(32)) # --- Firebase Admin init --- _creds = None json_inline = os.environ.get("FIREBASE_SERVICE_ACCOUNT_JSON") file_path = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS") if json_inline: _creds = credentials.Certificate(json.loads(json_inline)) elif file_path and os.path.exists(file_path): _creds = credentials.Certificate(file_path) else: raise RuntimeError("Firebase credentials not configured. Set GOOGLE_APPLICATION_CREDENTIALS or FIREBASE_SERVICE_ACCOUNT_JSON.") firebase_admin.initialize_app(_creds) db = firestore.client() # --- Filevine env --- FV_CLIENT_ID = os.environ.get("FILEVINE_CLIENT_ID") FV_CLIENT_SECRET = os.environ.get("FILEVINE_CLIENT_SECRET") FV_PAT = os.environ.get("FILEVINE_PERSONAL_ACCESS_TOKEN") FV_ORG_ID = os.environ.get("FILEVINE_ORG_ID") FV_USER_ID = os.environ.get("FILEVINE_USER_ID") if not all([FV_CLIENT_ID, FV_CLIENT_SECRET, FV_PAT, FV_ORG_ID, FV_USER_ID]): print("[WARN] Missing one or more Filevine env vars — dashboard will fail until set.") # --- Cache --- from cache import project_cache # --- Helpers --- def login_required(view): @wraps(view) def wrapped(*args, **kwargs): uid = session.get("uid") if not uid: return redirect(url_for("login")) return view(*args, **kwargs) return wrapped def get_user_profile(uid: str): """Fetch user's Firestore profile: users/{uid} => { enabled, caseEmail }""" doc_ref = db.collection("users").document(uid) snap = doc_ref.get() if not snap.exists: # bootstrap a placeholder doc so admins can fill it in doc_ref.set({"enabled": False}, merge=True) return {"enabled": False, "caseEmail": None} data = snap.to_dict() or {} return {"enabled": bool(data.get("enabled", False)), "caseEmail": data.get("caseEmail")} def fetch_all_projects_for_user(uid: str): """Fetch all projects for a user and cache them""" # Get bearer token bearer = get_filevine_bearer() # List projects (all pages) projects = list_all_projects(bearer) # Filter to ProjectEmailAddress == caseEmail profile = get_user_profile(uid) case_email = profile.get("caseEmail") # if case_email: # filtered = [p for p in projects if str(p.get("ProjectEmailAddress", "")).lower() == str(case_email).lower()] # else: # filtered = [] filtered = projects # Fetch details for each detailed_rows = [] for p in filtered: pid = (p.get("projectId") or {}).get("native") c = fetch_client(bearer, (p.get("clientId") or {}).get("native")) cs = fetch_contacts(bearer, pid) print("CS IS", cs) if pid is None: continue try: detail = fetch_project_detail(bearer, pid) except Exception as e: print(f"[WARN] detail fetch failed for {pid}: {e}") detail = {} row = { "client": c.get("firstName"), "matter_description": p.get("projectName"), "contacts": cs, "ProjectEmailAddress": p.get("projectEmailAddress"), "Number": p.get("number"), "IncidentDate": (p.get("incidentDate") or detail.get("incidentDate")), "ProjectId": pid, "ProjectName": p.get("projectName") or detail.get("projectName"), "ProjectUrl": p.get("projectUrl") or detail.get("projectUrl"), } detailed_rows.append(row) # Cache the results project_cache.set_projects(detailed_rows) return detailed_rows # --- Routes --- @app.route("/") def index(): uid = session.get("uid") if not uid: return redirect(url_for("login")) profile = get_user_profile(uid) if profile.get("enabled") and profile.get("caseEmail"): return redirect(url_for("dashboard")) return redirect(url_for("welcome")) @app.route("/login") def login(): # Pass public Firebase config to template fb_public = { "apiKey": os.environ.get("FIREBASE_API_KEY", ""), "authDomain": os.environ.get("FIREBASE_AUTH_DOMAIN", ""), "projectId": os.environ.get("FIREBASE_PROJECT_ID", ""), "appId": os.environ.get("FIREBASE_APP_ID", ""), } return render_template("login.html", firebase_config=fb_public) @app.route("/session_login", methods=["POST"]) def session_login(): """Exchanges Firebase ID token for a server session.""" id_token = request.json.get("idToken") if request.is_json else request.form.get("idToken") if not id_token: abort(400, "Missing idToken") try: decoded = fb_auth.verify_id_token(id_token) uid = decoded["uid"] session.clear() session["uid"] = uid # Optional: short session session["expires_at"] = (datetime.utcnow() + timedelta(hours=8)).isoformat() # Async update cache after login from threading import Thread thread = Thread(target=fetch_all_projects_for_user, args=(uid,)) thread.daemon = True thread.start() return jsonify({"ok": True}) except Exception as e: print("[ERR] session_login:", e) abort(401, "Invalid ID token") @app.route("/logout") def logout(): session.clear() return redirect(url_for("login")) @app.route("/welcome") @login_required def welcome(): uid = session.get("uid") profile = get_user_profile(uid) return render_template("welcome.html", profile=profile) # --- Filevine API --- def get_filevine_bearer(): url = "https://identity.filevine.com/connect/token" data = { "client_id": FV_CLIENT_ID, "client_secret": FV_CLIENT_SECRET, "grant_type": "personal_access_token", "scope": "fv.api.gateway.access tenant filevine.v2.api.* email openid fv.auth.tenant.read", "token": FV_PAT, } headers = {"Accept": "application/json"} resp = requests.post(url, data=data, headers=headers, timeout=30) resp.raise_for_status() js = resp.json() print(js) return js.get("access_token") def list_all_projects(bearer: str): base = "https://api.filevineapp.com/fv-app/v2/Projects" headers = { "Accept": "application/json", "Authorization": f"Bearer {bearer}", "x-fv-orgid": str(FV_ORG_ID), "x-fv-userid": str(FV_USER_ID), } print(headers) results = [] last_id = None tries = 0 while True: tries += 1 url = base params = {} if last_id is not None: # Some deployments use LastID/Offset pagination; adapt if needed params["lastID"] = last_id r = requests.get(url, headers=headers, params=params, timeout=30) print(r.content) r.raise_for_status() page = r.json() items = page.get("items", []) results.extend(items) has_more = page.get("hasMore") last_id = page.get("lastID") if not has_more: break # Safety valve if tries > 200: break print("RESULTS", results) return results def fetch_project_detail(bearer: str, project_id_native: int): url = f"https://api.filevineapp.com/fv-app/v2/Projects/{project_id_native}" headers = { "Accept": "application/json", "Authorization": f"Bearer {bearer}", "x-fv-orgid": str(FV_ORG_ID), "x-fv-userid": str(FV_USER_ID), } r = requests.get(url, headers=headers, timeout=30) r.raise_for_status() return r.json() def fetch_client(bearer: str, client_id_native: int): url = f"https://api.filevineapp.com/fv-app/v2/contacts/{client_id_native}" headers = { "Accept": "application/json", "Authorization": f"Bearer {bearer}", "x-fv-orgid": str(FV_ORG_ID), "x-fv-userid": str(FV_USER_ID), } r = requests.get(url, headers=headers, timeout=30) r.raise_for_status() return r.json() def fetch_contacts(bearer: str, project_id_native: int): url = f"https://api.filevineapp.com/fv-app/v2/projects/{project_id_native}/contacts" headers = { "Accept": "application/json", "Authorization": f"Bearer {bearer}", "x-fv-orgid": str(FV_ORG_ID), "x-fv-userid": str(FV_USER_ID), } r = requests.get(url, headers=headers, timeout=30) r.raise_for_status() return r.json().get("items") @app.route("/dashboard") @login_required def dashboard(): uid = session.get("uid") profile = get_user_profile(uid) if not profile.get("enabled"): return redirect(url_for("welcome")) case_email = profile.get("caseEmail") if not case_email: return redirect(url_for("welcome")) # Check cache first cached_projects = project_cache.get_projects() if cached_projects is not None: detailed_rows = cached_projects print("USING CACHE") else: # Fetch and cache projects detailed_rows = fetch_all_projects_for_user(uid) print("FETCHING") print("HI", len(detailed_rows)) # 5) Render table return render_template("dashboard.html", rows=detailed_rows, case_email=case_email) # GAE compatibility if __name__ == "__main__": app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", "5004")))