diff --git a/.claude/skills/filevine-api.md b/.claude/skills/filevine-api.md new file mode 100644 index 0000000..4c7ac1c --- /dev/null +++ b/.claude/skills/filevine-api.md @@ -0,0 +1,166 @@ +# Filevine API Curl Skill + +This skill enables intelligent exploration of the Filevine API by making curl requests and analyzing data patterns. + +## Capabilities + +1. **Discover relevant data within a project** - Search across all endpoints used in sync.py +2. **Make educated guesses** - Use patterns from sample data to infer where data might exist +3. **Query specific fields** - Search for fields like "matter number", "case number", dates, etc. + +## API Endpoints Discovered + +Based on analysis of `sync.py`, the following endpoints are used: + +| Endpoint | Purpose | Related Code | +|----------|---------|--------------| +| `Projects/{id}` | Project details (name, URLs, dates, phase) | `fetch_project_detail()` | +| `Projects/{id}/team` | Team members and roles | `fetch_project_team()` | +| `Projects/{id}/tasks` | Project tasks | `fetch_project_tasks()` | +| `Projects/{id}/Forms/{form}` | Specific forms (datesAndDeadlines, newFileReview, etc.) | `fetch_form()` | +| `Projects/{id}/Contacts` | Project contacts | `fetch_contacts()` | +| `Contacts/{id}` | Client information | `fetch_client()` | +| `Projects/{id}/Collections/{collection}` | Collections (serviceInfo) | `fetch_collection()` | + +## Sample Data Patterns + +From examining `examples/*.json`, here are common patterns: + +**Project-level fields (Projects/{id}):** +- `projectName` - Full project name +- `number` - Project number +- `incidentDate` - Incident date +- `projectEmailAddress` - Email address +- `projectUrl` - Link to Filevine +- `phaseName` - Current phase +- `createdDate` - Creation date + +**DatesAndDeadlines form:** +- `caseNumber` - Case number +- `dateCaseFiled` - Case filed date +- `defaultDate` - Default date +- `trialDate` - Trial date +- `mSCDate` - MSC date +- `noticeExpirationDate` - Notice expiration +- `judgmentDate` - Judgment date + +**NewFileReview form:** +- `noticeServiceDate` - When notice was served +- `noticeExpirationDate` - Notice expiration +- `noticeType` - Type of notice (NP, PPQ, etc.) +- `amountDemandedInNotice` - Amount demanded + +**ComplaintInfo form:** +- `totalDamagesSought` - Total damages +- `complaintVerificationBy` - Verification method + +## Usage + +When the user asks about finding data, follow this workflow: + +### 1. Parse the Query + +Identify what the user wants to find: +- Field name (e.g., "matter number", "case number", "trial date") +- Context (e.g., "in a project", "in the datesAndDeadlines form") + +### 2. Generate Endpoint Candidates + +Based on the query, suggest multiple endpoints to check: + +**For case/project identifiers:** +- `Projects/{id}` (check `number`, `caseNumber` fields) +- `Projects/{id}/Forms/datesAndDeadlines` (check `caseNumber` field) +- `Projects/{id}/Forms/matterOverview` (check `matterNumber` field) + +**For dates:** +- `Projects/{id}` (check `incidentDate`, `createdDate`) +- `Projects/{id}/Forms/datesAndDeadlines` (check `trialDate`, `mSCDate`, `defaultDate`, etc.) +- `Projects/{id}/Forms/newFileReview` (check `noticeServiceDate`) + +**For contact/defendant info:** +- `Projects/{id}/Contacts` (check contact list for defendant) +- `Contacts/{client_id}` (check full contact info) + +**For attorney/team:** +- `Projects/{id}/team` (check `teamOrgRoles` for "Assigned Attorney", "Primary", etc.) + +**For notices:** +- `Projects/{id}/Forms/newFileReview` (check `noticeType`, `noticeExpirationDate`, etc.) +- `Projects/{id}/Collections/serviceInfo` (check service dates) + +### 3. Execute curl Commands + +For each candidate endpoint, construct and execute curl commands using the auth headers from the codebase: + +```bash +curl -X GET "https://api.filevineapp.com/fv-app/v2/Projects/{PROJECT_ID}/{ENDPOINT}" \ + -H "Accept: application/json" \ + -H "Authorization: Bearer {BEARER_TOKEN}" \ + -H "x-fv-orgid: {ORG_ID}" \ + -H "x-fv-userid: {USER_ID}" +``` + +Use the project ID from sample data (e.g., `15974631` or `15914808`) as a placeholder. + +### 4. Analyze Results + +Look for the queried field in the JSON response: +- Search field names (not values) +- Check nested objects and arrays +- Note which endpoints actually contain the data + +### 5. Report Findings + +Provide: +1. The field value(s) found +2. The endpoint(s) that contain it +3. Any related fields discovered in the same endpoint +4. Context about how this data is used in the application + +## Common Patterns to Guess From + +When the user asks "What endpoint should I hit to find [field]?", consider: + +**Field Mapping:** +| Common Field Names | Likely Endpoints | +|-------------------|------------------| +| `matterNumber`, `matter number` | `Projects/{id}/Forms/matterOverview` | +| `caseNumber`, `case number` | `Projects/{id}/Forms/datesAndDeadlines` | +| `incident date`, `incidentDate` | `Projects/{id}`, `Projects/{id}/Forms/datesAndDeadlines` | +| `trial date`, `trialDate` | `Projects/{id}/Forms/datesAndDeadlines` | +| `notice service date`, `noticeServiceDate` | `Projects/{id}/Forms/newFileReview` | +| `notice expiration date`, `noticeExpirationDate` | `Projects/{id}/Forms/newFileReview` | +| `notice type` | `Projects/{id}/Forms/newFileReview` | +| `assigned attorney` | `Projects/{id}/team` (role: "Assigned Attorney") | +| `primary contact`, `staff person` | `Projects/{id}/team` (role: "Primary") | +| `defendant` | `Projects/{id}/Contacts` (filter for "Defendant" role) | +| `client` | `Contacts/{client_id}` or `Projects/{id}` (clientId field) | + +## Environment Setup + +When executing curl commands, use these placeholder values (user should provide actual values): + +- `PROJECT_ID` - Use sample IDs like 15974631 or 15914808 +- `ORG_ID` - From FILEVINE_ORG_ID env var +- `USER_ID` - From FILEVINE_USER_ID env var +- `BEARER_TOKEN` - From FilevineClient.get_bearer_token() + +## Tips + +1. Start with the most likely endpoints first +2. The sample data shows that many fields are duplicated across endpoints - check multiple places +3. Date fields are often in ISO format in API responses +4. Some forms (like `newFileReview`) contain multiple related date fields +5. Team roles use specific names - "Assigned Attorney", "Primary", "Secondary Paralegal" +6. Contact objects in arrays may have `personTypes` to identify their role + +## Example Workflow + +User: "What endpoint should I hit to find the matter number?" + +1. Identify: User wants "matter number" field +2. Guess: Matter number might be in `Projects/{id}/Forms/matterOverview` based on sample data patterns +3. Check: `Projects/{id}` also has `number` field +4. Execute: curl to both endpoints with project 15974631 +5. Report: Found `number` in project root and `matterNumber` in matterOverview form diff --git a/.gitignore b/.gitignore index a5b3e5b..124da0a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ __pycache__/** **/*.pyc +node_modules/** diff --git a/admin.py b/admin.py index 9c8341c..83d6369 100644 --- a/admin.py +++ b/admin.py @@ -1,7 +1,9 @@ import json import os +import random +import string from functools import wraps -from flask import render_template, request, redirect, url_for, session, abort, jsonify +from flask import render_template, request, redirect, url_for, session, abort, jsonify, flash from firebase_init import db from firebase_admin import auth as fb_auth from utils import get_user_profile @@ -47,7 +49,8 @@ def register_admin_routes(app): "case_email": user_data.get("case_email", ""), "case_domain_email": user_data.get("case_domain_email", ""), "enabled": bool(user_data.get("enabled", False)), - "is_admin": bool(user_data.get("is_admin", False)) + "is_admin": bool(user_data.get("is_admin", False)), + "password_reset_required": bool(user_data.get("password_reset_required", False)) }) except Exception as e: @@ -94,17 +97,23 @@ def register_admin_routes(app): # Get the user from Firebase Auth user = fb_auth.get_user(uid) - # Generate password reset link using Firebase Auth - password_reset_link = fb_auth.generate_password_reset_link(user.email) - # Send password reset email using Firebase's built-in template - # This will send an email to the user with a link to reset their password - # Firebase automatically handles the email template and delivery - print(f"[INFO] Password reset link generated for {user.email}: {password_reset_link}") + # Generate temporary password (random word + 3 digits) + words = ["sun", "moon", "star", "cloud", "rain", "wind", "fire", "water", "snow", "stone", + "tree", "leaf", "flower", "bird", "wolf", "tiger", "bear", "fish", "dragon", + "magic", "quest", "light", "dark", "gold", "silver", "ruby", "pearl", "diamond"] + random_word = random.choice(words) + random_digits = ''.join(random.choices(string.digits, k=3)) + temp_password = f"{random_word}{random_digits}" + + # Create user profile in Firestore with password reset required flag + user_ref = db.collection("users").document(user.uid) + user_ref.set({ + "password_reset_required": True + },merge=True) - # Store the password reset link in the session for display in the banner - session['password_reset_link'] = password_reset_link - session['reset_user_email'] = user.email + flash(f"User now has a temporary password {temp_password}.", "success") + fb_auth.update_user(uid, password=temp_password) # Redirect back to the admin users table return redirect(url_for('admin_users')) @@ -152,37 +161,51 @@ def register_admin_routes(app): @app.route("/admin/users/create", methods=["POST"]) @admin_required def create_user(): - """Create a new user""" + """Create a new user with temporary password""" try: # Get form data user_email = request.form.get("user_email") if not user_email: abort(400, "User email is required") - + # Validate email format if "@" not in user_email: abort(400, "Invalid email format") - - # Create user in Firebase Authentication + + # Generate temporary password (random word + 3 digits) + words = ["sun", "moon", "star", "cloud", "rain", "wind", "fire", "water", "snow", "stone", + "tree", "leaf", "flower", "bird", "wolf", "tiger", "bear", "fish", "dragon", + "magic", "quest", "light", "dark", "gold", "silver", "ruby", "pearl", "diamond"] + random_word = random.choice(words) + random_digits = ''.join(random.choices(string.digits, k=3)) + temp_password = f"{random_word}{random_digits}" + + # Create user in Firebase Authentication with temporary password user_record = fb_auth.create_user( email=user_email, email_verified=False, - disabled=not request.form.get("enabled", False) + disabled=not request.form.get("enabled", False), + password=temp_password ) - - # Create user profile in Firestore + + # Create user profile in Firestore with password reset required flag user_ref = db.collection("users").document(user_record.uid) user_ref.set({ "user_email": user_email, "case_email": request.form.get("case_email", ""), "case_domain_email": request.form.get("case_domain_email", ""), "enabled": bool(request.form.get("enabled", False)), - "is_admin": bool(request.form.get("is_admin", False)) + "is_admin": bool(request.form.get("is_admin", False)), + "password_reset_required": True }) - + + # Display success message with temporary password + flash(f"User created successfully. Temporary password: {temp_password}", "success") + print(f"[INFO] Created user {user_email} with temp password: {temp_password}") + # Redirect to admin users page return redirect(url_for("admin_users")) - + except fb_auth.EmailAlreadyExistsError: print(f"[ERR] User with email {user_email} already exists") abort(400, "A user with this email already exists") diff --git a/app.py b/app.py index 99279cc..49195cd 100644 --- a/app.py +++ b/app.py @@ -170,6 +170,11 @@ def session_login(): # Optional: short session session["expires_at"] = (datetime.utcnow() + timedelta(hours=8)).isoformat() + # Check if user needs password reset + user_profile = get_user_profile(uid) + if user_profile.get("password_reset_required"): + return jsonify({"requires_password_reset": True}) + return jsonify({"ok": True}) except Exception as e: print("[ERR] session_login:", e) @@ -182,6 +187,52 @@ def logout(): return redirect(url_for("login")) +@app.route("/require-password-reset") +@login_required +def require_password_reset(): + """Show password reset page for users who need to reset their password""" + return render_template("require_password_reset.html") + + +@app.route("/reset-password-submit", methods=["POST"]) +@login_required +def reset_password_submit(): + """Handle password reset form submission""" + uid = session.get("uid") + profile = get_user_profile(uid) + + new_password = request.form.get("new_password") + confirm_password = request.form.get("confirm_password") + + # Validate passwords match + if new_password != confirm_password: + flash("Passwords do not match", "error") + return redirect(url_for("require_password_reset")) + + # Validate password length + if len(new_password) < 6: + flash("Password must be at least 6 characters", "error") + return redirect(url_for("require_password_reset")) + + # Update password in Firebase Auth + try: + fb_auth.update_user(uid, password=new_password) + print(db.collection("users").document(uid)) + + # Clear the password reset required flag in Firestore + db.collection("users").document(uid).set({"password_reset_required": False},merge=True) + print(db.collection("users").document(uid)) + + print(f"[INFO] Password reset successful for user {uid}") + except Exception as e: + print(f"[ERR] Failed to reset password for {uid}: {e}") + flash("Failed to reset password. Please try again.", "error") + return redirect(url_for("require_password_reset")) + + # Allow user to login now + return redirect(url_for("login")) + + @app.route("/welcome") @login_required def welcome(): diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..bd86be4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,71 @@ +{ + "name": "rothbard", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@playwright/test": "1.58.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.0.tgz", + "integrity": "sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg==", + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.0.tgz", + "integrity": "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.0.tgz", + "integrity": "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..bdc646c --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@playwright/test": "1.58.0" + } +} diff --git a/templates/admin_users.html b/templates/admin_users.html index b0d7665..d2a99db 100644 --- a/templates/admin_users.html +++ b/templates/admin_users.html @@ -1,6 +1,16 @@ {% extends 'base.html' %} {% block content %}
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+

{{ message }}

+
+ {% endfor %} + {% endif %} + {% endwith %} + {% if session.get('password_reset_link') %}

Please send an email to {{ session.get('reset_user_email') }}

diff --git a/templates/login.html b/templates/login.html index 3cfa0cd..756e97a 100644 --- a/templates/login.html +++ b/templates/login.html @@ -31,18 +31,14 @@