from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, field_validator from typing import Optional import uuid from bd import get_session from bd.tables.users import User from route.auth_utils import get_current_user, require_admin_key, hash_password router = APIRouter(tags=["users"], prefix="/users") class UserCreate(BaseModel): username: str password: str first_name: str last_name: str group_id: Optional[str] = None organization_id: Optional[str] = None @field_validator("username") @classmethod def username_clean(cls, v: str) -> str: v = v.strip() if len(v) < 3: raise ValueError("Username must be at least 3 characters") return v @field_validator("password") @classmethod def password_strength(cls, v: str) -> str: if len(v) < 8: raise ValueError("Password must be at least 8 characters") return v class UserUpdate(BaseModel): first_name: Optional[str] = None last_name: Optional[str] = None group_id: Optional[str] = None organization_id: Optional[str] = None def _user_dict(user: User) -> dict: return { "id": str(user.id), "username": user.username, "first_name": user.first_name, "last_name": user.last_name, "group_id": str(user.group_id) if user.group_id else None, "organization_id": str(user.organization_id) if user.organization_id else None, } @router.post("/", status_code=201, dependencies=[Depends(require_admin_key)]) def create_user(payload: UserCreate): Session = get_session() with Session() as session: existing = session.query(User).filter(User.username == payload.username).first() if existing: raise HTTPException(status_code=400, detail="Username already taken") user = User( first_name=payload.first_name.strip(), last_name=payload.last_name.strip(), username=payload.username, hashed_password=hash_password(payload.password), group_id=uuid.UUID(payload.group_id) if payload.group_id else None, organization_id=uuid.UUID(payload.organization_id) if payload.organization_id else None, ) session.add(user) session.commit() session.refresh(user) return _user_dict(user) @router.put("/{user_id}") def update_user(user_id: str, payload: UserUpdate, current_user: User = Depends(get_current_user)): Session = get_session() try: uid = uuid.UUID(user_id) except Exception: raise HTTPException(status_code=400, detail="Invalid UUID") if uid != current_user.id: raise HTTPException(status_code=403, detail="You can only update your own profile") with Session() as session: user = session.get(User, uid) if not user: raise HTTPException(status_code=404, detail="User not found") if payload.first_name is not None: user.first_name = payload.first_name if payload.last_name is not None: user.last_name = payload.last_name if payload.group_id is not None: user.group_id = uuid.UUID(payload.group_id) if payload.organization_id is not None: user.organization_id = uuid.UUID(payload.organization_id) session.add(user) session.commit() session.refresh(user) return _user_dict(user) @router.delete("/{user_id}", status_code=204) def delete_user(user_id: str, current_user: User = Depends(get_current_user)): Session = get_session() try: uid = uuid.UUID(user_id) except Exception: raise HTTPException(status_code=400, detail="Invalid UUID") if uid != current_user.id: raise HTTPException(status_code=403, detail="You can only delete your own account") with Session() as session: user = session.get(User, uid) if not user: raise HTTPException(status_code=404, detail="User not found") session.delete(user) session.commit() return {} @router.get("/me") def get_me(current_user: User = Depends(get_current_user)): return _user_dict(current_user) @router.get("/", dependencies=[Depends(require_admin_key)]) def list_users(): Session = get_session() with Session() as session: rows = session.query(User).all() return [_user_dict(u) for u in rows] @router.get("/{user_id}") def get_user(user_id: str, current_user: User = Depends(get_current_user)): Session = get_session() try: uid = uuid.UUID(user_id) except Exception: raise HTTPException(status_code=400, detail="Invalid UUID") with Session() as session: user = session.get(User, uid) if not user: raise HTTPException(status_code=404, detail="User not found") return _user_dict(user)