Initial
This commit is contained in:
270
app.py
Normal file
270
app.py
Normal file
@@ -0,0 +1,270 @@
|
||||
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.")
|
||||
|
||||
# --- 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")}
|
||||
|
||||
|
||||
# --- 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()
|
||||
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"))
|
||||
|
||||
# 1) Bearer token
|
||||
bearer = get_filevine_bearer()
|
||||
|
||||
# 2) List projects (all pages)
|
||||
projects = list_all_projects(bearer)
|
||||
|
||||
# 3) Filter to ProjectEmailAddress == caseEmail
|
||||
#filtered = [p for p in projects if str(p.get("ProjectEmailAddress", "")).lower() == str(case_email).lower()]
|
||||
|
||||
filtered = projects
|
||||
# 4) 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)
|
||||
|
||||
# 5) Render table
|
||||
return render_template("dashboard.html", rows=detailed_rows, case_email=case_email)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True, host="0.0.0.0", port=5000)
|
||||
Reference in New Issue
Block a user