from __future__ import annotations import os import re from datetime import datetime, timezone from pathlib import Path from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile, status from pydantic import BaseModel from backend.auth.dependencies import UserPrincipal, get_current_user from backend.auth.service import ( list_tenant_users, login_with_directory, read_authenticated_user_profile, ) from backend.auth.user_directory import ensure_user_directory_schema router = APIRouter() ASSET_DIR = os.getenv("VELOCITY_ASSET_DIR", "/opt/dlami/nvme/assets") def _sanitize_filename(value: str) -> str: cleaned = re.sub(r"[^A-Za-z0-9._-]+", "_", value).strip("._") return cleaned or "upload" class LoginRequest(BaseModel): email: str password: str @router.post("/api/auth/login", tags=["Auth"]) async def login(body: LoginRequest, request: Request): """ Authenticate a user and return a JWT. Credentials are verified against the users_and_roles table. """ return await login_with_directory( app=request.app, email=body.email, password=body.password, ) @router.get("/api/auth/me", tags=["Auth"]) async def me(request: Request, user: UserPrincipal = Depends(get_current_user)): return await read_authenticated_user_profile(app=request.app, user=user) @router.get("/api/auth/users", tags=["Auth"]) async def list_auth_users(request: Request, user: UserPrincipal = Depends(get_current_user)): return await list_tenant_users(app=request.app, user=user) @router.post("/api/auth/profile/avatar", tags=["Auth"]) async def upload_profile_avatar( request: Request, file: UploadFile = File(...), user: UserPrincipal = Depends(get_current_user), ): await ensure_user_directory_schema(request.app) pool = getattr(request.app.state, "db_pool", None) 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)}_" f"{int(datetime.now(timezone.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: result = await conn.execute( """ UPDATE users_and_roles SET avatar_url = $2 WHERE id = $1::uuid AND tenant_id = $3 """, user.user_id, avatar_url, user.tenant_id, ) if result == "UPDATE 0": raise HTTPException(status_code=404, detail="Authenticated user profile was not found.") return {"avatar_url": avatar_url}