from datetime import datetime, timedelta, timezone from typing import Optional from jose import JWTError, jwt from passlib.context import CryptContext from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from sqlalchemy.orm import Session from config import get_settings from database import get_db import models settings = get_settings() pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login", auto_error=False) def verify_password(plain_password: str, hashed_password: str) -> bool: return pwd_context.verify(plain_password, hashed_password) def get_password_hash(password: str) -> str: return pwd_context.hash(password) def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: to_encode = data.copy() expire = datetime.now(timezone.utc) + ( expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) ) to_encode.update({"exp": expire, "type": "access"}) return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) def create_refresh_token(data: dict) -> str: to_encode = data.copy() expire = datetime.now(timezone.utc) + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS) to_encode.update({"exp": expire, "type": "refresh"}) return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) def verify_token(token: str, token_type: str) -> Optional[dict]: try: payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) if payload.get("type") != token_type: return None return payload except JWTError: return None def get_current_user( token: Optional[str] = Depends(oauth2_scheme), db: Session = Depends(get_db), ) -> Optional[models.User]: if not token: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated", headers={"WWW-Authenticate": "Bearer"}, ) payload = verify_token(token, "access") if not payload or "sub" not in payload: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") user = db.query(models.User).filter(models.User.username == payload["sub"]).first() if not user: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found") return user def require_admin(user: models.User = Depends(get_current_user)): if user.role != "admin": raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin required") return user def log_action(db: Session, user: Optional[models.User], action: str, details: Optional[str] = None): try: entry = models.AuditLog( user_id=user.id if user else None, username=user.username if user else None, action=action, details=details, ) db.add(entry) db.commit() except Exception: db.rollback() def create_default_admin(db: Session) -> Optional[models.User]: if not settings.CREATE_ADMIN_ON_STARTUP: return None existing = db.query(models.User).filter(models.User.username == settings.ADMIN_USERNAME).first() if existing: return existing admin = models.User( username=settings.ADMIN_USERNAME, password_hash=get_password_hash(settings.ADMIN_PASSWORD), role="admin", ) db.add(admin) db.commit() db.refresh(admin) return admin