resets passwords

This commit is contained in:
2026-01-29 20:51:14 -08:00
parent 86a09225e7
commit 607e65560c
10 changed files with 412 additions and 32 deletions

View File

@@ -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

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ __pycache__/**
**/*.pyc **/*.pyc
node_modules/**

View File

@@ -1,7 +1,9 @@
import json import json
import os import os
import random
import string
from functools import wraps 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_init import db
from firebase_admin import auth as fb_auth from firebase_admin import auth as fb_auth
from utils import get_user_profile from utils import get_user_profile
@@ -47,7 +49,8 @@ def register_admin_routes(app):
"case_email": user_data.get("case_email", ""), "case_email": user_data.get("case_email", ""),
"case_domain_email": user_data.get("case_domain_email", ""), "case_domain_email": user_data.get("case_domain_email", ""),
"enabled": bool(user_data.get("enabled", False)), "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: except Exception as e:
@@ -94,17 +97,23 @@ def register_admin_routes(app):
# Get the user from Firebase Auth # Get the user from Firebase Auth
user = fb_auth.get_user(uid) 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 # Generate temporary password (random word + 3 digits)
# This will send an email to the user with a link to reset their password words = ["sun", "moon", "star", "cloud", "rain", "wind", "fire", "water", "snow", "stone",
# Firebase automatically handles the email template and delivery "tree", "leaf", "flower", "bird", "wolf", "tiger", "bear", "fish", "dragon",
print(f"[INFO] Password reset link generated for {user.email}: {password_reset_link}") "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}"
# Store the password reset link in the session for display in the banner # Create user profile in Firestore with password reset required flag
session['password_reset_link'] = password_reset_link user_ref = db.collection("users").document(user.uid)
session['reset_user_email'] = user.email user_ref.set({
"password_reset_required": True
},merge=True)
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 # Redirect back to the admin users table
return redirect(url_for('admin_users')) return redirect(url_for('admin_users'))
@@ -152,7 +161,7 @@ def register_admin_routes(app):
@app.route("/admin/users/create", methods=["POST"]) @app.route("/admin/users/create", methods=["POST"])
@admin_required @admin_required
def create_user(): def create_user():
"""Create a new user""" """Create a new user with temporary password"""
try: try:
# Get form data # Get form data
user_email = request.form.get("user_email") user_email = request.form.get("user_email")
@@ -163,23 +172,37 @@ def register_admin_routes(app):
if "@" not in user_email: if "@" not in user_email:
abort(400, "Invalid email format") 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( user_record = fb_auth.create_user(
email=user_email, email=user_email,
email_verified=False, 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 = db.collection("users").document(user_record.uid)
user_ref.set({ user_ref.set({
"user_email": user_email, "user_email": user_email,
"case_email": request.form.get("case_email", ""), "case_email": request.form.get("case_email", ""),
"case_domain_email": request.form.get("case_domain_email", ""), "case_domain_email": request.form.get("case_domain_email", ""),
"enabled": bool(request.form.get("enabled", False)), "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 # Redirect to admin users page
return redirect(url_for("admin_users")) return redirect(url_for("admin_users"))

51
app.py
View File

@@ -170,6 +170,11 @@ def session_login():
# Optional: short session # Optional: short session
session["expires_at"] = (datetime.utcnow() + timedelta(hours=8)).isoformat() 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}) return jsonify({"ok": True})
except Exception as e: except Exception as e:
print("[ERR] session_login:", e) print("[ERR] session_login:", e)
@@ -182,6 +187,52 @@ def logout():
return redirect(url_for("login")) 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") @app.route("/welcome")
@login_required @login_required
def welcome(): def welcome():

71
package-lock.json generated Normal file
View File

@@ -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"
}
}
}
}

5
package.json Normal file
View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"@playwright/test": "1.58.0"
}
}

View File

@@ -1,6 +1,16 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
<div class="h-full flex flex-col"> <div class="h-full flex flex-col">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="bg-{% if category == 'success' %}green{% else %}blue{% endif %}-50 border border-{% if category == 'success' %}green{% else %}blue{% endif %}-200 text-{% if category == 'success' %}green{% else %}blue{% endif %}-800 px-4 py-3 rounded-md mb-4">
<p>{{ message }}</p>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% if session.get('password_reset_link') %} {% if session.get('password_reset_link') %}
<div class="bg-blue-50 border border-blue-200 text-blue-800 px-4 py-3 rounded-md mb-4"> <div class="bg-blue-50 border border-blue-200 text-blue-800 px-4 py-3 rounded-md mb-4">
<p class="font-medium">Please send an email to {{ session.get('reset_user_email') }}</p> <p class="font-medium">Please send an email to {{ session.get('reset_user_email') }}</p>

View File

@@ -31,18 +31,14 @@
<script> <script>
// Initialize Firebase configuration from template // Initialize Firebase configuration from template
window.FIREBASE_CONFIG = {{ firebase_config|tojson }}; window.FIREBASE_CONFIG = {{ firebase_config|tojson }};
// Initialize Firebase app and auth
const app = firebase.initializeApp(window.FIREBASE_CONFIG || {}); const app = firebase.initializeApp(window.FIREBASE_CONFIG || {});
const auth = firebase.auth(); const auth = firebase.auth();
// Get form and input elements
const form = document.getElementById('login-form'); const form = document.getElementById('login-form');
const email = document.getElementById('email'); const email = document.getElementById('email');
const password = document.getElementById('password'); const password = document.getElementById('password');
const err = document.getElementById('error'); const err = document.getElementById('error');
// Handle form submission
form.addEventListener('submit', async (e) => { form.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
err.classList.add('hidden'); err.classList.add('hidden');
@@ -54,10 +50,17 @@
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ idToken }) body: JSON.stringify({ idToken })
}); });
if(!res.ok){
if (!res.ok) {
throw new Error('Session exchange failed'); throw new Error('Session exchange failed');
} }
const data = await res.json();
if (data.requires_password_reset) {
window.location.href = '/require-password-reset';
} else {
window.location.href = '/'; window.location.href = '/';
}
} catch (e) { } catch (e) {
err.textContent = e.message || 'Authentication failed'; err.textContent = e.message || 'Authentication failed';
err.classList.remove('hidden'); err.classList.remove('hidden');

View File

@@ -0,0 +1,47 @@
{% extends 'base.html' %}
{% block content %}
<div class="h-full flex flex-col max-w-md mx-auto">
<h1 class="text-xl font-semibold mb-6">Password Reset Required</h1>
<form method="POST" action="/reset-password-submit" class="space-y-6">
<div>
<label for="new_password" class="block text-sm font-medium text-slate-700">New Password</label>
<input type="password" id="new_password" name="new_password"
value=""
required
class="mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
minlength="6">
<p class="mt-1 text-sm text-slate-500">Password must be at least 6 characters</p>
</div>
<div>
<label for="confirm_password" class="block text-sm font-medium text-slate-700">Confirm Password</label>
<input type="password" id="confirm_password" name="confirm_password"
value=""
required
class="mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-md">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="flex justify-end space-x-3 pt-4">
<a href="/login"
class="px-4 py-2 text-sm font-medium text-slate-700 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors text-center inline-block">
Cancel
</a>
<button type="submit"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors">
Reset Password
</button>
</div>
</form>
</div>
{% endblock %}

View File

@@ -3,7 +3,7 @@ from firebase_init import db
from firebase_admin import auth as fb_auth from firebase_admin import auth as fb_auth
def get_user_profile(uid: str): def get_user_profile(uid: str):
"""Fetch user's Firestore profile: users/{uid} => { enabled, case_email, is_admin, user_email }""" """Fetch user's Firestore profile: users/{uid} => { enabled, case_email, is_admin, user_email, password_reset_required }"""
doc_ref = db.collection("users").document(uid) doc_ref = db.collection("users").document(uid)
snap = doc_ref.get() snap = doc_ref.get()
if not snap.exists: if not snap.exists:
@@ -20,14 +20,16 @@ def get_user_profile(uid: str):
"is_admin": False, "is_admin": False,
"user_email": user_email, "user_email": user_email,
"case_email": user_email, "case_email": user_email,
"case_domain_email": "" "case_domain_email": "",
"password_reset_required": True
}, merge=True) }, merge=True)
return { return {
"enabled": False, "enabled": False,
"is_admin": False, "is_admin": False,
"user_email": user_email, "user_email": user_email,
"case_email": user_email, "case_email": user_email,
"case_domain_email": "" "case_domain_email": "",
"password_reset_required": True
} }
data = snap.to_dict() or {} data = snap.to_dict() or {}
return { return {
@@ -35,5 +37,6 @@ def get_user_profile(uid: str):
"is_admin": bool(data.get("is_admin", False)), "is_admin": bool(data.get("is_admin", False)),
"user_email": data.get("user_email"), "user_email": data.get("user_email"),
"case_email": data.get("case_email"), "case_email": data.get("case_email"),
"case_domain_email": data.get("case_domain_email", "") "case_domain_email": data.get("case_domain_email", ""),
"password_reset_required": bool(data.get("password_reset_required", False))
} }