From dc6c24ca6dcc16363584d2aae60ebe5fbcdf8e7f Mon Sep 17 00:00:00 2001 From: Bryce Date: Tue, 27 Jan 2026 22:06:18 -0800 Subject: [PATCH] fixes --- .env | 5 +- app.py | 2 +- filevine_client.py | 4 +- models/project_model.py | 4 + sync.py | 32 +++++- templates/dashboard.html | 2 +- terraform/main.tf | 152 ++++++++++++++++++++++++++++ terraform/modules/cloud_run/main.tf | 144 ++++++++++++++++++++++++++ 8 files changed, 338 insertions(+), 7 deletions(-) create mode 100644 terraform/main.tf create mode 100644 terraform/modules/cloud_run/main.tf diff --git a/.env b/.env index 1dbbdd7..0d8a8d8 100644 --- a/.env +++ b/.env @@ -9,8 +9,9 @@ GOOGLE_APPLICATION_CREDENTIALS=./rothbard-staging2-12345-firebase-adminsdk-fbsvc # Filevine auth FILEVINE_CLIENT_ID=4F18738C-107A-4B82-BFAC-308F1B6A626A -FILEVINE_CLIENT_SECRET=*2{aXWvYN(9!BiYUXC_tXj^n8 -FILEVINE_PERSONAL_ACCESS_TOKEN=C8F5C606B834D4EE0CBF0793969496F6210037EB934523756BA80BF2D8EC1880 +FILEVINE_CLIENT_SECRET=q<}QzfD^3t_atF-7+U8(gJCgj +FILEVINE_PERSONAL_ACCESS_TOKEN=68BEBFB8437B9668642BD71EDEAF09593ACF075CE35AE4079FC7588410094210 + FILEVINE_ORG_ID=9227 FILEVINE_USER_ID=100510 diff --git a/app.py b/app.py index 43d1b4f..99279cc 100644 --- a/app.py +++ b/app.py @@ -383,7 +383,7 @@ def dashboard_export_xls(): elif header == 'Notice Expir. Date': field_name = 'notice_expiration_date' elif header == 'Date Case Filed': - field_name = 'case_field_date' + field_name = 'case_filed_date' elif header == 'Daily Rent Damages': field_name = 'daily_rent_damages' elif header == 'Default Date': diff --git a/filevine_client.py b/filevine_client.py index 54ef77e..a3f9c3f 100644 --- a/filevine_client.py +++ b/filevine_client.py @@ -11,14 +11,15 @@ FV_USER_ID = os.environ.get("FILEVINE_USER_ID") class FilevineClient: def __init__(self, bearer_token: str = None): - self.bearer_token = bearer_token self.base_url = "https://api.filevineapp.com/fv-app/v2" + self.bearer_token = bearer_token self.headers = { "Accept": "application/json", "Authorization": f"Bearer {self.bearer_token}", "x-fv-orgid": str(FV_ORG_ID), "x-fv-userid": str(FV_USER_ID), } + self.get_bearer_token() def get_bearer_token(self) -> str: """Get a new bearer token using Filevine credentials""" @@ -32,6 +33,7 @@ class FilevineClient: } headers = {"Accept": "application/json"} + print("data is", data) print(data) resp = requests.post(url, data=data, headers=headers, timeout=30) resp.raise_for_status() diff --git a/models/project_model.py b/models/project_model.py index 80a47aa..04e8d6e 100644 --- a/models/project_model.py +++ b/models/project_model.py @@ -31,6 +31,7 @@ class ProjectModel: pending_tasks: List[Dict[str, Any]] = None, notice_service_date: str = "", notice_expiration_date: str = "", + case_filed_date: str = "", case_field_date: str = "", daily_rent_damages: str = "", default_date: str = "", @@ -89,6 +90,7 @@ class ProjectModel: self.pending_tasks = pending_tasks or [] self.notice_service_date = notice_service_date self.notice_expiration_date = notice_expiration_date + self.case_filed_date = case_filed_date or case_field_date self.case_field_date = case_field_date self.daily_rent_damages = daily_rent_damages self.default_date = default_date @@ -149,6 +151,7 @@ class ProjectModel: "pending_tasks": self.pending_tasks, "notice_service_date": self.notice_service_date, "notice_expiration_date": self.notice_expiration_date, + "case_filed_date": self.case_filed_date, "case_field_date": self.case_field_date, "daily_rent_damages": self.daily_rent_damages, "default_date": self.default_date, @@ -211,6 +214,7 @@ class ProjectModel: pending_tasks=data.get("pending_tasks", []), notice_service_date=data.get("notice_service_date", ""), notice_expiration_date=data.get("notice_expiration_date", ""), + case_filed_date=data.get("case_filed_date", ""), case_field_date=data.get("case_field_date", ""), daily_rent_damages=data.get("daily_rent_damages", ""), default_date=data.get("default_date", ""), diff --git a/sync.py b/sync.py index 8a103e2..662302d 100644 --- a/sync.py +++ b/sync.py @@ -29,6 +29,22 @@ def convert_to_pacific_time(date_str): if not date_str: return '' + try: + # Parse the UTC datetime + utc_time = datetime.fromisoformat(date_str.replace('Z', '+00:00')) + + # Set timezone to UTC + utc_time = utc_time.replace(tzinfo=pytz.UTC) + + # Convert to Pacific Time + pacific_time = utc_time.astimezone(pytz.timezone('America/Los_Angeles')) + + # Format as YYYY-MM-DD + return pacific_time.strftime('%Y-%m-%d') + except (ValueError, AttributeError) as e: + print(f"[WARN] Date conversion failed for '{date_str}': {e}") + return '' + def extract_domains_from_emails(emails: List[str]) -> List[str]: """Extract unique domains from a list of email addresses. @@ -155,6 +171,10 @@ def process_project(index: int, total: int, project_data: dict, client: Filevine # Extract default date default_date = convert_to_pacific_time(dates_and_deadlines.get("defaultDate")) or '' case_filed_date = convert_to_pacific_time(dates_and_deadlines.get("dateCaseFiled")) or '' + cf = dates_and_deadlines.get("dateCaseFiled") + from pprint import pprint + print(f"CASE FILED {case_filed_date} {cf}") + pprint(dates_and_deadlines) # Extract motion hearing dates demurrer_hearing_date = convert_to_pacific_time(dates_and_deadlines.get("demurrerHearingDate")) or '' @@ -205,7 +225,6 @@ def process_project(index: int, total: int, project_data: dict, client: Filevine import itertools # valid_property_managers = list(itertools.chain(*)) valid_property_managers = [e.get('address').lower() for pm in property_managers if pm and pm.get('emails') for e in pm.get('emails') if e and e.get('address')] - pprint(valid_property_managers) row = ProjectModel( @@ -226,6 +245,7 @@ def process_project(index: int, total: int, project_data: dict, client: Filevine notice_service_date=notice_service_date, notice_expiration_date=notice_expiration_date, case_field_date=case_filed_date, + case_filed_date=case_filed_date, daily_rent_damages=daily_rent_damages, default_date=default_date, demurrer_hearing_date=demurrer_hearing_date, @@ -326,7 +346,7 @@ def main(): # List projects (all pages) with filter for projects updated in the last 7 days from datetime import datetime, timedelta - seven_days_ago = (datetime.now() - timedelta(days=14)).strftime('%Y-%m-%d') + seven_days_ago = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d') projects = client.list_all_projects(latest_activity_since=seven_days_ago) #projects = [p for p in projects if (p.get("projectId") or {}).get("native") == 15914808] @@ -343,6 +363,14 @@ def main(): import traceback traceback.print_exc() sys.exit(1) + +def sync_single(x): + client = FilevineClient() + z = process_project(0, 1, client.fetch_project_detail(x), client) + from pprint import pprint + + #pprint(z) + if __name__ == "__main__": main() diff --git a/templates/dashboard.html b/templates/dashboard.html index 07c1c1d..415d973 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -382,7 +382,7 @@ {% call expander() %} - {{ r.case_field_date }} + {{ r.case_filed_date || r.case_field_date }} {% endcall %} diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000..22ff1ca --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,152 @@ +terraform { + required_providers { + google-beta = { + source = "hashicorp/google-beta" + version = "~> 6.0" + } + } +} + +provider "google" { + project = var.gcp_project_id + region = var.gcp_region +} + + +# Firebase Project Setup +resource "google_firebase_project" "default" { + provider = google-beta + project = var.gcp_project_id +} + +# Firebase Web App +resource "google_firebase_web_app" "rothbard_portal" { + provider = google-beta + project = google_firebase_project.default.project + display_name = "Rothbard Client Portal" + + app_urls = ["https://${var.domain_name}"] +} + +# Firestore Database +resource "google_firestore_database" "default" { + provider = google-beta + project = var.gcp_project_id + name = "(default)" + location_id = var.firestore_location + type = "FIRESTORE_NATIVE" + + delete_protection_state = "DELETE_PROTECTION_DISABLED" +} + +# Firebase Authentication - Complete Configuration +resource "google_firebase_auth_config" "default" { + provider = google-beta + project = var.gcp_project_id + + sign_in_options { + email { + enabled = true + password_required = true + } + + # Disable other providers for security + phone { + enabled = false + } + + google { + enabled = var.enable_google_signin + } + + facebook { + enabled = false + } + + apple { + enabled = false + } + } + + # Email configuration + email { + reset_password_template { + from_email_address = var.auth_from_email + from_display_name = var.auth_from_name + reply_to = var.auth_reply_to + subject = "Reset your Rothbard Law Group password" + html = file("${path.module}/templates/reset_password.html") + text = file("${path.module}/templates/reset_password.txt") + } + + email_verification_template { + from_email_address = var.auth_from_email + from_display_name = var.auth_from_name + reply_to = var.auth_reply_to + subject = "Verify your Rothbard Law Group account" + html = file("${path.module}/templates/email_verification.html") + text = file("${path.module}/templates/email_verification.txt") + } + } + + # Security settings + sign_in { + allow_duplicate_emails = false + } + + # Multi-factor authentication (disabled for simplicity) + multi_factor_auth { + enabled = false + } + + # Anonymous user access (disabled) + anonymous { + enabled = false + } +} + +# Service Account for the Flask App +resource "google_service_account" "flask_app" { + account_id = "rothbard-flask-app" + display_name = "Rothbard Flask App Service Account" +} + +# IAM permissions for the Flask App +resource "google_project_iam_member" "firestore_access" { + project = var.gcp_project_id + role = "roles/datastore.user" + member = "serviceAccount:${google_service_account.flask_app.email}" +} + +resource "google_project_iam_member" "firebase_admin" { + project = var.gcp_project_id + role = "roles/firebase.admin" + member = "serviceAccount:${google_service_account.flask_app.email}" +} + +# Firestore Security Rules - Note: Firestore security policies are managed through Firestore rules +# This section is commented out as google_firestore_security_policy is not supported +# Security rules should be managed through firestore.rules file or Firebase console + +# Firebase Hosting (optional - for static assets) +resource "google_firebase_hosting_site" "default" { + provider = google-beta + project = var.gcp_project_id + site_id = "rothbard-portal" +} + +# Output important values +output "firebase_web_app_id" { + description = "Firebase Web App ID" + value = google_firebase_web_app.rothbard_portal.app_id +} + +output "firebase_project_id" { + description = "Firebase Project ID" + value = google_firebase_project.default.project +} + +output "service_account_email" { + description = "Service account email for Flask app" + value = google_service_account.flask_app.email +} diff --git a/terraform/modules/cloud_run/main.tf b/terraform/modules/cloud_run/main.tf new file mode 100644 index 0000000..97e73c5 --- /dev/null +++ b/terraform/modules/cloud_run/main.tf @@ -0,0 +1,144 @@ +# Cloud Run Service for Flask App +resource "google_cloud_run_service" "flask_app" { + name = "${var.app_name}-service" + location = var.gcp_region + + template { + spec { + containers { + image = var.container_image + + # Environment variables for the Flask app + env { + name = "FLASK_SECRET_KEY" + value = var.flask_secret_key + } + + env { + name = "FIREBASE_PROJECT_ID" + value = var.firebase_project_id + } + + env { + name = "GOOGLE_APPLICATION_CREDENTIALS" + value = "/etc/secrets/service-account.json" + } + + # Filevine API credentials + env { + name = "FILEVINE_CLIENT_ID" + value = var.filevine_client_id + } + + env { + name = "FILEVINE_CLIENT_SECRET" + value = var.filevine_client_secret + } + + env { + name = "FILEVINE_PERSONAL_ACCESS_TOKEN" + value = var.filevine_pat + } + + env { + name = "FILEVINE_ORG_ID" + value = var.filevine_org_id + } + + env { + name = "FILEVINE_USER_ID" + value = var.filevine_user_id + } + + # Memory and CPU limits + resources { + limits = { + cpu = "1000m" + memory = "512Mi" + } + } + + # Mount service account key + volume_mount { + name = "service-account-key" + mount_path = "/etc/secrets" + read_only = true + } + } + + # Service account for the container + service_account_name = var.service_account_email + + # Volumes + volumes { + name = "service-account-key" + secret { + secret_name = google_secret_manager_secret.service_account_key.secret_id + items { + key = "latest" + path = "service-account.json" + } + } + } + + # Allow unauthenticated access + container_concurrency = 100 + timeout_seconds = 300 + } + + # Traffic settings + metadata { + annotations = { + "autoscaling.knative.dev/maxScale" = "10" + "autoscaling.knative.dev/minScale" = "1" + "run.googleapis.com/ingress" = "all" + } + } + } + + traffic { + percent = 100 + latest_revision = true + } + + depends_on = [google_secret_manager_secret_version.service_account_key] +} + +# Make Cloud Run service publicly accessible +resource "google_cloud_run_service_iam_member" "public" { + location = google_cloud_run_service.flask_app.location + project = google_cloud_run_service.flask_app.project + service = google_cloud_run_service.flask_app.name + role = "roles/run.invoker" + member = "allUsers" +} + +# Store service account key in Secret Manager +resource "google_secret_manager_secret" "service_account_key" { + project = var.gcp_project_id + secret_id = "${var.app_name}-service-account-key" + + replication { + automatic {} + } +} + +resource "google_secret_manager_secret_version" "service_account_key" { + secret = google_secret_manager_secret.service_account_key.id + secret_data = var.service_account_key_data +} + +# Cloud Storage bucket for container storage (if needed) +resource "google_storage_bucket" "app_storage" { + name = "${var.app_name}-storage-${var.gcp_project_id}" + location = var.gcp_region + force_destroy = true + + uniform_bucket_level_access = true +} + +# Output the service URL +output "service_url" { + description = "Cloud Run service URL" + value = google_cloud_run_service.flask_app.status[0].url +} \ No newline at end of file