import logging import secrets import string from datetime import UTC, datetime, timedelta from typing import Annotated import jwt from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from jwt.exceptions import InvalidTokenError from pwdlib import PasswordHash from sqlmodel import Session, select from app.model.models import Token, TokenData, UserDB from app.services.database import add_and_refresh, get_session from app.services.settings import settings logger = logging.getLogger(__name__) SECRET_KEY = settings.secret_key ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 120 oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/token") token_router = APIRouter(tags=["Token"], prefix="/api/v1") password_hash = PasswordHash.recommended() def verify_password(plain_password, hashed_password): return password_hash.verify(plain_password, hashed_password) def get_password_hash(password): return password_hash.hash(password) def get_user(db, username: str): user = db.exec(select(UserDB).where(UserDB.name == username)).first() return user def authenticate_user(db, username: str, password: str): user = get_user(db, username) if not user: return False if not verify_password(password, user.passwordhash): return False return user def create_access_token(data: dict, expires_delta: timedelta | None = None): to_encode = data.copy() if expires_delta: expire = datetime.now(UTC) + expires_delta else: expire = datetime.now(UTC) + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt def get_current_user( token: Annotated[str, Depends(oauth2_scheme)], db: Session = Depends(get_session), ): credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username = payload.get("sub") if username is None: raise credentials_exception token_data = TokenData(username=username) except InvalidTokenError: raise credentials_exception user = get_user(db, username=token_data.username) if user is None: raise credentials_exception return user def auth_is_admin( token: str = Depends(oauth2_scheme), db: Session = Depends(get_session), ): user = get_current_user(token=token, db=db) if not user.is_admin: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized to perform this action", headers={"WWW-Authenticate": "Bearer"}, ) return True def create_first_user(db: Session): logger.info("Checking for admin user") admin_user = db.exec(select(UserDB)).first() if admin_user is None: password = "".join(secrets.choice(string.digits) for i in range(8)) logger.info(f"Creating first admin user with password: {password}") user = UserDB( name="admin", passwordhash=get_password_hash(password), is_admin=True ) return add_and_refresh(db, user) logger.info(f"Admin user already exists: {admin_user.name}") @token_router.post("/token") def login_for_access_token( form_data: Annotated[OAuth2PasswordRequestForm, Depends()], db: Session = Depends(get_session), ) -> Token: user = authenticate_user(db, form_data.username, form_data.password) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or pw", headers={"WWW-Authenticate": "Bearer"}, ) access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( data={"sub": user.name}, expires_delta=access_token_expires ) return Token(access_token=access_token, token_type="bearer") @token_router.get("/test/login") def test_login(current_user: Annotated[UserDB, Depends(get_current_user)]) -> UserDB: return current_user