Files
Project_Velocity/backend/auth/routes.py
2026-04-28 11:32:56 +05:30

106 lines
3.2 KiB
Python

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}