from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from sqlalchemy.orm import Session from jose import JWTError, jwt from passlib.context import CryptContext from datetime import datetime, timedelta from pydantic import BaseModel from typing import Optional import uuid import os from database import get_db, SessionLocal from models.user import User, Role SECRET_KEY = os.getenv("SECRET_KEY", "aidsmonitoring-secret-2026-change-in-prod") ALGORITHM = "HS256" TOKEN_HOURS = 8 pwd_ctx = CryptContext(schemes=["bcrypt"], deprecated="auto") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login") router = APIRouter(prefix="/auth", tags=["auth"]) # ── Helpers ──────────────────────────────────────────────────────────────── def hash_password(plain: str) -> str: return pwd_ctx.hash(plain) def verify_password(plain: str, hashed: str) -> bool: return pwd_ctx.verify(plain, hashed) def create_token(data: dict) -> str: payload = data.copy() payload["exp"] = datetime.utcnow() + timedelta(hours=TOKEN_HOURS) return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM) def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)) -> User: exc = HTTPException(status_code=401, detail="Invalid or expired token", headers={"WWW-Authenticate": "Bearer"}) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username = payload.get("sub") if not username: raise exc except JWTError: raise exc user = db.query(User).filter(User.username == username, User.activo == True).first() if not user: raise exc return user def require_admin(user: User = Depends(get_current_user)): if user.role not in (Role.ADMIN, Role.SUPERADMIN): raise HTTPException(status_code=403, detail="Admin access required") return user def require_superadmin(user: User = Depends(get_current_user)): if user.role != Role.SUPERADMIN: raise HTTPException(status_code=403, detail="Superadmin access required") return user # ── Seed usuario inicial ─────────────────────────────────────────────────── def seed_users(): db = SessionLocal() if not db.query(User).first(): db.add(User( id=str(uuid.uuid4()), username="admin", nombre="Administrador", email="admin@aidsmonitoring.com", hashed_pw=hash_password("admin123"), role=Role.SUPERADMIN, )) db.add(User( id=str(uuid.uuid4()), username="operador", nombre="Operador Barranquilla", email="operador@aidsmonitoring.com", hashed_pw=hash_password("operador123"), role=Role.ADMIN, )) db.add(User( id=str(uuid.uuid4()), username="visor", nombre="Usuario Solo Lectura", email="visor@aidsmonitoring.com", hashed_pw=hash_password("visor123"), role=Role.USER, )) db.commit() db.close() # ── Endpoints ────────────────────────────────────────────────────────────── class TokenResponse(BaseModel): access_token: str token_type: str username: str nombre: str role: str class UserCreate(BaseModel): username: str nombre: str email: Optional[str] = None password: str role: str = "USER" company_id: Optional[str] = None @router.post("/login", response_model=TokenResponse) def login(form: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): user = db.query(User).filter(User.username == form.username, User.activo == True).first() if not user or not verify_password(form.password, user.hashed_pw): raise HTTPException(status_code=401, detail="Invalid credentials") user.ultimo_login = datetime.utcnow() db.commit() token = create_token({"sub": user.username, "role": user.role}) return TokenResponse( access_token=token, token_type="bearer", username=user.username, nombre=user.nombre, role=user.role, ) @router.get("/me") def me(user: User = Depends(get_current_user)): return { "username": user.username, "nombre": user.nombre, "role": user.role, "company_id": getattr(user, "company_id", None), } @router.get("/users", dependencies=[Depends(require_superadmin)]) def list_users(db: Session = Depends(get_db)): users = db.query(User).all() return [ { "id": u.id, "username": u.username, "nombre": u.nombre, "email": u.email, "role": u.role, "activo": u.activo, "company_id": getattr(u, "company_id", None), "ultimo_login": u.ultimo_login.isoformat() if u.ultimo_login else None, } for u in users ] @router.post("/users", dependencies=[Depends(require_superadmin)]) def create_user(data: UserCreate, db: Session = Depends(get_db)): if db.query(User).filter(User.username == data.username).first(): raise HTTPException(status_code=400, detail="Username already exists") user = User( id=str(uuid.uuid4()), username=data.username, nombre=data.nombre, email=data.email, hashed_pw=hash_password(data.password), role=data.role, company_id=data.company_id, ) db.add(user) db.commit() return {"ok": True, "username": user.username, "role": user.role, "company_id": user.company_id} @router.put("/users/{username}", dependencies=[Depends(require_superadmin)]) def update_user(username: str, data: dict, db: Session = Depends(get_db)): user = db.query(User).filter(User.username == username).first() if not user: raise HTTPException(status_code=404, detail="User not found") if "nombre" in data: user.nombre = data["nombre"] if "email" in data: user.email = data["email"] if "role" in data: user.role = data["role"] if "activo" in data: user.activo = data["activo"] if "password" in data and data["password"]: user.hashed_pw = hash_password(data["password"]) if "company_id" in data: user.company_id = data["company_id"] or None db.commit() return {"ok": True} @router.delete("/users/{username}", dependencies=[Depends(require_superadmin)]) def delete_user(username: str, db: Session = Depends(get_db)): if username == "admin": raise HTTPException(status_code=400, detail="Cannot delete admin") user = db.query(User).filter(User.username == username).first() if not user: raise HTTPException(status_code=404, detail="User not found") db.delete(user) db.commit() return {"ok": True}