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
|