- Fix missing await on FocusGroup.get_messages() (N-L1) - Replace time.sleep with asyncio.sleep in key_theme_service and focus_group_service (N-P10) - Replace flask import with quart in focus_groups.py (N-S3) - Add logger.error before all 500 returns in focus_groups.py (N-P6) - Add logging to silent except blocks across routes (N-M10, N-M11) - Add @rate_limit to 6 remaining AI endpoints (N-H4) - Add --confirm flag to populate scripts before delete_many (S-H2) - Remove hardcoded Azure ID fallbacks from msal_service.py and msalConfig.ts (A-M2, F-H4) - Centralize make_serializable() in utils.py, remove duplicates from 3 route files (N-P7) - Replace all datetime.utcnow() with datetime.now(timezone.utc) across entire backend (M-L2) - AuthContext.tsx: only mark token validated on 200 success, not on non-401 errors (F-H2) - Rename authType → auth_type in auth.py (N-S4) - Add security_report.md and security_report.pdf with full 92-finding status Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
138 lines
4.7 KiB
Python
Executable file
138 lines
4.7 KiB
Python
Executable file
import logging
|
|
from bson import ObjectId
|
|
from app.db import get_db
|
|
from datetime import datetime, timezone
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Allowed fields for create/update (mass assignment protection)
|
|
PERSONA_ALLOWED_FIELDS = {
|
|
"name", "age", "gender", "occupation", "education", "location",
|
|
"techSavviness", "personality", "interests", "brandLoyalty",
|
|
"priceConsciousness", "environmentalConcern", "hasPurchasingPower",
|
|
"hasChildren", "thinkFeelDo", "description", "imageUrl",
|
|
"folder_ids", "llm_model", "traits", "background", "goals",
|
|
"communication_style", "values", "demographics",
|
|
}
|
|
|
|
|
|
class Persona:
|
|
@staticmethod
|
|
async def create(persona_data, user_id=None):
|
|
db = await get_db()
|
|
|
|
# Apply field allowlist (mass assignment protection)
|
|
safe_data = {k: v for k, v in persona_data.items() if k in PERSONA_ALLOWED_FIELDS}
|
|
|
|
# Add metadata
|
|
safe_data["created_at"] = datetime.now(timezone.utc)
|
|
safe_data["created_by"] = user_id
|
|
|
|
# Initialize folder_ids array if not present
|
|
if "folder_ids" not in safe_data:
|
|
safe_data["folder_ids"] = []
|
|
|
|
result = await db.personas.insert_one(safe_data)
|
|
logger.info(f"Persona created: {safe_data.get('name', 'Unknown')}")
|
|
return str(result.inserted_id)
|
|
|
|
@staticmethod
|
|
async def find_by_id(persona_id):
|
|
db = await get_db()
|
|
try:
|
|
if isinstance(persona_id, ObjectId):
|
|
object_id = persona_id
|
|
else:
|
|
try:
|
|
object_id = ObjectId(persona_id)
|
|
except Exception as e:
|
|
logger.warning(f"Invalid ObjectId format: {persona_id}: {e}")
|
|
persona = await db.personas.find_one({"id": persona_id})
|
|
if persona:
|
|
persona["_id"] = str(persona["_id"])
|
|
return persona
|
|
return None
|
|
|
|
persona = await db.personas.find_one({"_id": object_id})
|
|
if persona:
|
|
persona["_id"] = str(persona["_id"])
|
|
return persona
|
|
except Exception as e:
|
|
logger.error(f"Error in find_by_id: {e}, persona_id: {persona_id}")
|
|
return None
|
|
|
|
@staticmethod
|
|
async def find_by_user(user_id, limit=100):
|
|
db = await get_db()
|
|
personas = db.personas.find({"created_by": user_id}).sort("created_at", -1).limit(limit)
|
|
result = []
|
|
|
|
async for persona in personas:
|
|
persona["_id"] = str(persona["_id"])
|
|
result.append(persona)
|
|
|
|
return result
|
|
|
|
@staticmethod
|
|
async def get_all(user_id=None, limit=100):
|
|
try:
|
|
db = await get_db()
|
|
query = {"created_by": user_id} if user_id else {}
|
|
personas = db.personas.find(query).sort("created_at", -1).limit(limit)
|
|
result = []
|
|
|
|
async for persona in personas:
|
|
persona["_id"] = str(persona["_id"])
|
|
result.append(persona)
|
|
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Error in Persona.get_all: {e}")
|
|
return []
|
|
|
|
@staticmethod
|
|
async def update(persona_id, data, user_id=None):
|
|
db = await get_db()
|
|
|
|
# Apply field allowlist
|
|
filtered_data = {k: v for k, v in data.items() if k in PERSONA_ALLOWED_FIELDS}
|
|
filtered_data["updated_at"] = datetime.now(timezone.utc)
|
|
|
|
# Build ownership-aware query
|
|
query = {"_id": ObjectId(persona_id)}
|
|
if user_id:
|
|
query["created_by"] = user_id
|
|
|
|
result = await db.personas.update_one(
|
|
query,
|
|
{"$set": filtered_data}
|
|
)
|
|
|
|
return result.modified_count > 0
|
|
|
|
@staticmethod
|
|
async def delete(persona_id, user_id=None):
|
|
db = await get_db()
|
|
try:
|
|
if isinstance(persona_id, ObjectId):
|
|
object_id = persona_id
|
|
else:
|
|
try:
|
|
object_id = ObjectId(persona_id)
|
|
except Exception as e:
|
|
logger.warning(f"Invalid ObjectId format for delete: {persona_id}: {e}")
|
|
query = {"id": persona_id}
|
|
if user_id:
|
|
query["created_by"] = user_id
|
|
result = await db.personas.delete_one(query)
|
|
return result.deleted_count > 0
|
|
|
|
query = {"_id": object_id}
|
|
if user_id:
|
|
query["created_by"] = user_id
|
|
|
|
result = await db.personas.delete_one(query)
|
|
return result.deleted_count > 0
|
|
except Exception as e:
|
|
logger.error(f"Error in delete: {e}, persona_id: {persona_id}")
|
|
return False
|