forked from sagnik/Project_Velocity
fix: Applied fix for name, the oracle team sharing, sentinel client list visibility
This commit is contained in:
@@ -34,13 +34,19 @@ ROLE_HIERARCHY = {
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
|
||||
def _truncate_bcrypt_input(value: str) -> str:
|
||||
raw = value.encode("utf-8")
|
||||
if len(raw) <= 72:
|
||||
return value
|
||||
return raw[:72].decode("utf-8", errors="ignore")
|
||||
|
||||
|
||||
def hash_password(plain: str) -> str:
|
||||
return pwd_context.hash(plain)
|
||||
return pwd_context.hash(_truncate_bcrypt_input(plain))
|
||||
|
||||
|
||||
def verify_password(plain: str, hashed: str) -> bool:
|
||||
# Truncate to 72 bytes to prevent bcrypt 500 errors
|
||||
return pwd_context.verify(plain[:72], hashed)
|
||||
return pwd_context.verify(_truncate_bcrypt_input(plain), hashed)
|
||||
|
||||
|
||||
# ── JWT helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
122
backend/main.py
122
backend/main.py
@@ -11,15 +11,50 @@ import os
|
||||
import json
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Set
|
||||
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends, HTTPException, status
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends, HTTPException, status, UploadFile, File
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from dotenv import load_dotenv
|
||||
|
||||
def _load_velocity_env() -> None:
|
||||
repo_root = Path(__file__).resolve().parent.parent
|
||||
backend_root = repo_root / "backend"
|
||||
|
||||
explicit_env = os.getenv("VELOCITY_ENV_FILE", "").strip()
|
||||
candidate_paths = []
|
||||
|
||||
if explicit_env:
|
||||
candidate_paths.append(Path(explicit_env))
|
||||
|
||||
candidate_paths.extend(
|
||||
[
|
||||
backend_root / ".env",
|
||||
repo_root / ".env",
|
||||
]
|
||||
)
|
||||
|
||||
loaded_any = False
|
||||
seen: set[Path] = set()
|
||||
for candidate in candidate_paths:
|
||||
resolved = candidate.resolve()
|
||||
if resolved in seen or not candidate.exists():
|
||||
continue
|
||||
load_dotenv(candidate, override=not loaded_any)
|
||||
loaded_any = True
|
||||
seen.add(resolved)
|
||||
|
||||
if not loaded_any:
|
||||
load_dotenv()
|
||||
|
||||
|
||||
_load_velocity_env()
|
||||
|
||||
from backend.api.routes_catalyst import router as catalyst_router
|
||||
from backend.api.routes_crm import crm_router, analytics_router
|
||||
from backend.api.routes_oracle import router as oracle_helper_router
|
||||
@@ -39,8 +74,6 @@ from backend.routers.videos import router as videos_router
|
||||
from backend.routers.vault import router as vault_router
|
||||
from backend.routers.sentinel import router as sentinel_router, broadcast_sentinel_event
|
||||
|
||||
load_dotenv()
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger("velocity.main")
|
||||
|
||||
@@ -91,6 +124,11 @@ ASSET_DIR = os.getenv("VELOCITY_ASSET_DIR", "/opt/dlami/nvme/assets")
|
||||
if os.path.isdir(ASSET_DIR):
|
||||
app.mount("/assets", StaticFiles(directory=ASSET_DIR), name="assets")
|
||||
|
||||
|
||||
def _sanitize_filename(value: str) -> str:
|
||||
cleaned = re.sub(r"[^A-Za-z0-9._-]+", "_", value).strip("._")
|
||||
return cleaned or "upload"
|
||||
|
||||
# ── Routers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
app.include_router(catalyst_router, prefix="/api/catalyst", tags=["Catalyst"])
|
||||
@@ -160,7 +198,7 @@ async def me(user: UserPrincipal = Depends(get_current_user)):
|
||||
async with pool.acquire() as conn:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT full_name, email
|
||||
SELECT full_name, email, avatar_url
|
||||
FROM users_and_roles
|
||||
WHERE id = $1::uuid
|
||||
""",
|
||||
@@ -172,9 +210,85 @@ async def me(user: UserPrincipal = Depends(get_current_user)):
|
||||
"role": user.role,
|
||||
"full_name": row["full_name"] if row else None,
|
||||
"email": row["email"] if row else None,
|
||||
"avatar_url": row["avatar_url"] if row else None,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/auth/users", tags=["Auth"])
|
||||
async def list_auth_users(_: UserPrincipal = Depends(get_current_user)):
|
||||
pool = app.state.db_pool
|
||||
if pool is None:
|
||||
raise HTTPException(status_code=503, detail="Database unavailable.")
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
rows = await conn.fetch(
|
||||
"""
|
||||
SELECT
|
||||
id::text AS user_id,
|
||||
role,
|
||||
full_name,
|
||||
email,
|
||||
avatar_url
|
||||
FROM users_and_roles
|
||||
WHERE is_active = TRUE
|
||||
ORDER BY
|
||||
COALESCE(NULLIF(full_name, ''), email, id::text) ASC
|
||||
"""
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
"user_id": row["user_id"],
|
||||
"role": row["role"],
|
||||
"full_name": row["full_name"],
|
||||
"email": row["email"],
|
||||
"avatar_url": row["avatar_url"],
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
@app.post("/api/auth/profile/avatar", tags=["Auth"])
|
||||
async def upload_profile_avatar(
|
||||
file: UploadFile = File(...),
|
||||
user: UserPrincipal = Depends(get_current_user),
|
||||
):
|
||||
pool = app.state.db_pool
|
||||
if pool is None:
|
||||
raise HTTPException(status_code=503, detail="Database unavailable.")
|
||||
|
||||
allowed = {"image/png", "image/jpeg", "image/jpg", "image/webp"}
|
||||
if file.content_type not in allowed:
|
||||
raise HTTPException(status_code=400, detail="Unsupported avatar format.")
|
||||
|
||||
extension = Path(file.filename or "avatar.png").suffix.lower() or ".png"
|
||||
if extension not in {".png", ".jpg", ".jpeg", ".webp"}:
|
||||
extension = ".png"
|
||||
|
||||
avatar_dir = Path(ASSET_DIR) / "profile_avatars"
|
||||
avatar_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
filename = f"{user.user_id}_{_sanitize_filename(Path(file.filename or 'avatar').stem)}_{int(datetime.now(UTC).timestamp())}{extension}"
|
||||
destination = avatar_dir / filename
|
||||
contents = await file.read()
|
||||
destination.write_bytes(contents)
|
||||
|
||||
avatar_url = f"/assets/profile_avatars/{filename}"
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"""
|
||||
UPDATE users_and_roles
|
||||
SET avatar_url = $2
|
||||
WHERE id = $1::uuid
|
||||
""",
|
||||
user.user_id,
|
||||
avatar_url,
|
||||
)
|
||||
|
||||
return {"avatar_url": avatar_url}
|
||||
|
||||
|
||||
# ── Catalyst WebSocket (preserved from v1) ────────────────────────────────────
|
||||
|
||||
class _CatalystManager:
|
||||
|
||||
Reference in New Issue
Block a user