resets passwords
This commit is contained in:
166
.claude/skills/filevine-api.md
Normal file
166
.claude/skills/filevine-api.md
Normal 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
1
.gitignore
vendored
@@ -11,3 +11,4 @@ __pycache__/**
|
||||
|
||||
|
||||
**/*.pyc
|
||||
node_modules/**
|
||||
|
||||
55
admin.py
55
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}"
|
||||
|
||||
# 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
|
||||
# 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)
|
||||
|
||||
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,7 +161,7 @@ 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")
|
||||
@@ -163,23 +172,37 @@ def register_admin_routes(app):
|
||||
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"))
|
||||
|
||||
|
||||
51
app.py
51
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():
|
||||
|
||||
71
package-lock.json
generated
Normal file
71
package-lock.json
generated
Normal 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
5
package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.58.0"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,16 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<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') %}
|
||||
<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>
|
||||
|
||||
@@ -31,18 +31,14 @@
|
||||
<script>
|
||||
// Initialize Firebase configuration from template
|
||||
window.FIREBASE_CONFIG = {{ firebase_config|tojson }};
|
||||
|
||||
// Initialize Firebase app and auth
|
||||
const app = firebase.initializeApp(window.FIREBASE_CONFIG || {});
|
||||
const auth = firebase.auth();
|
||||
|
||||
// Get form and input elements
|
||||
const form = document.getElementById('login-form');
|
||||
const email = document.getElementById('email');
|
||||
const password = document.getElementById('password');
|
||||
const err = document.getElementById('error');
|
||||
|
||||
// Handle form submission
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
err.classList.add('hidden');
|
||||
@@ -54,10 +50,17 @@
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ idToken })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
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 = '/';
|
||||
}
|
||||
} catch (e) {
|
||||
err.textContent = e.message || 'Authentication failed';
|
||||
err.classList.remove('hidden');
|
||||
|
||||
47
templates/require_password_reset.html
Normal file
47
templates/require_password_reset.html
Normal 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 %}
|
||||
11
utils.py
11
utils.py
@@ -3,7 +3,7 @@ from firebase_init import db
|
||||
from firebase_admin import auth as fb_auth
|
||||
|
||||
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)
|
||||
snap = doc_ref.get()
|
||||
if not snap.exists:
|
||||
@@ -20,14 +20,16 @@ def get_user_profile(uid: str):
|
||||
"is_admin": False,
|
||||
"user_email": user_email,
|
||||
"case_email": user_email,
|
||||
"case_domain_email": ""
|
||||
"case_domain_email": "",
|
||||
"password_reset_required": True
|
||||
}, merge=True)
|
||||
return {
|
||||
"enabled": False,
|
||||
"is_admin": False,
|
||||
"user_email": user_email,
|
||||
"case_email": user_email,
|
||||
"case_domain_email": ""
|
||||
"case_domain_email": "",
|
||||
"password_reset_required": True
|
||||
}
|
||||
data = snap.to_dict() or {}
|
||||
return {
|
||||
@@ -35,5 +37,6 @@ def get_user_profile(uid: str):
|
||||
"is_admin": bool(data.get("is_admin", False)),
|
||||
"user_email": data.get("user_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))
|
||||
}
|
||||
Reference in New Issue
Block a user