fix: embedding migration, worker error handling, path traversal security, response mismatches (unread-count, plugins, manifests), frontend type safety
This commit is contained in:
@@ -346,6 +346,10 @@ def create_app() -> FastAPI:
|
||||
# Don't intercept API routes
|
||||
if full_path.startswith(("api/", "docs", "openapi", "redoc")):
|
||||
raise HTTPException(status_code=404, detail="Not Found")
|
||||
# Block path traversal and system file access
|
||||
blocked_prefixes = ("var/log/", "error/", "error_log", "var/", "etc/", "proc/", "sys/")
|
||||
if full_path.startswith(blocked_prefixes) or "/../" in full_path or full_path.endswith("/.."):
|
||||
raise HTTPException(status_code=404, detail="Not Found")
|
||||
index_path = os.path.join(frontend_dist, "index.html")
|
||||
if os.path.isfile(index_path):
|
||||
return FileResponse(index_path)
|
||||
|
||||
@@ -42,6 +42,10 @@ async def index_mails(ctx: dict[str, Any], mail_ids: list[str]) -> None:
|
||||
await index_entity("mail", eid, tenant_id, db)
|
||||
except Exception:
|
||||
logger.exception("Failed to index mail %s", mail_id)
|
||||
try:
|
||||
await db.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def index_file(ctx: dict[str, Any], file_id: str) -> None:
|
||||
@@ -98,6 +102,10 @@ async def index_file(ctx: dict[str, Any], file_id: str) -> None:
|
||||
logger.info("Indexed file %s", file_id)
|
||||
except Exception:
|
||||
logger.exception("Failed to index file %s", file_id)
|
||||
try:
|
||||
await db.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def index_contact(ctx: dict[str, Any], contact_id: str) -> None:
|
||||
@@ -119,6 +127,10 @@ async def index_contact(ctx: dict[str, Any], contact_id: str) -> None:
|
||||
await index_entity("contact", eid, row["tenant_id"], db)
|
||||
except Exception:
|
||||
logger.exception("Failed to index contact %s", contact_id)
|
||||
try:
|
||||
await db.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def index_event(ctx: dict[str, Any], event_id: str) -> None:
|
||||
@@ -140,6 +152,10 @@ async def index_event(ctx: dict[str, Any], event_id: str) -> None:
|
||||
await index_entity("event", eid, row["tenant_id"], db)
|
||||
except Exception:
|
||||
logger.exception("Failed to index event %s", event_id)
|
||||
try:
|
||||
await db.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def reindex(ctx: dict[str, Any], entity_type: str) -> None:
|
||||
@@ -182,6 +198,10 @@ async def reindex(ctx: dict[str, Any], entity_type: str) -> None:
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Reindex failed for %s/%s", entity_type, row["id"])
|
||||
try:
|
||||
await db.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
offset += BATCH_SIZE
|
||||
|
||||
logger.info("Reindex complete for %s", entity_type)
|
||||
@@ -217,7 +237,15 @@ async def embedding_batch(ctx: dict[str, Any]) -> None:
|
||||
await index_entity(etype, row["id"], row["tenant_id"], db)
|
||||
except Exception:
|
||||
logger.exception("Batch index failed for %s/%s", etype, row["id"])
|
||||
try:
|
||||
await db.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
logger.exception("Batch query failed for %s", etype)
|
||||
try:
|
||||
await db.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.info("Embedding batch job complete")
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
-- Unified Search: Embedding columns and HNSW indexes
|
||||
|
||||
-- Ensure pgvector extension is installed
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
|
||||
-- ─── Mails ───
|
||||
ALTER TABLE mails ADD COLUMN IF NOT EXISTS embedding vector(768);
|
||||
CREATE INDEX IF NOT EXISTS ix_mails_embedding ON mails USING hnsw(embedding vector_cosine_ops);
|
||||
|
||||
@@ -67,7 +67,7 @@ async def mark_notification_read_endpoint(
|
||||
}
|
||||
|
||||
|
||||
@router.get("/unread-count", response_model=UnreadCountResponse)
|
||||
@router.get("/unread-count")
|
||||
async def unread_count_endpoint(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(require_permission("notifications:read")),
|
||||
@@ -76,7 +76,7 @@ async def unread_count_endpoint(
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
user_id = uuid.UUID(current_user["user_id"])
|
||||
count = await get_unread_count(db, tenant_id, user_id)
|
||||
return {"count": count}
|
||||
return count
|
||||
|
||||
|
||||
@router.get("/types")
|
||||
|
||||
@@ -50,7 +50,7 @@ async def list_plugins(
|
||||
"""List all plugins with their current status (discovered, installed, active, inactive)."""
|
||||
service = get_plugin_service()
|
||||
plugins = await service.list_plugins(db)
|
||||
return {"plugins": plugins, "total": len(plugins)}
|
||||
return plugins
|
||||
|
||||
|
||||
@router.get("/manifest")
|
||||
@@ -76,7 +76,7 @@ async def get_active_manifests(
|
||||
"""
|
||||
service = get_plugin_service()
|
||||
manifests = await service.get_active_manifests(db)
|
||||
return {"plugins": manifests, "total": len(manifests)}
|
||||
return manifests
|
||||
|
||||
|
||||
@router.get("/{name}/config")
|
||||
|
||||
Reference in New Issue
Block a user