import bcrypt from datetime import timedelta from fastapi.exceptions import HTTPException from fastapi.security import OAuth2PasswordRequestForm from fastapi import APIRouter as FastAPI, Depends, Response, Request from config import ( LOGGER, SESSION_EXPIRE_LIMIT, COOKIE_SAMESITE_POLICY, COOKIE_DOMAIN, MANAGER, SESSION_COOKIE_NAME, LIMITER, SECURE_COOKIE, COOKIE_PATH, COOKIE_HTTPONLY, UNVERIFIED_MANAGER, ) from responses import ( Response as APIResponse, UnprocessableEntity, BadRequest, ) from orm.users import ( UserModel, PrivateUserModel, get_user_by_username, ) router = FastAPI() @router.post( "/user", tags=["Users"], status_code=200, responses={ 200: {"model": APIResponse}, 400: {"model": BadRequest}, 422: {"model": UnprocessableEntity}, }, ) @LIMITER.limit("5/minute") async def login( request: Request, response: Response, data: OAuth2PasswordRequestForm = Depends() ): """ Performs user authentication. Endpoint is limited to 5 hits per minute """ if request.cookies.get(SESSION_COOKIE_NAME): raise HTTPException(status_code=400, detail="Please logout first") username = data.username if len(username) > 32: raise HTTPException( status_code=413, detail="Authentication failed: username is too long" ) try: password = data.password.encode() if len(password) > 72: raise HTTPException( status_code=413, detail="Authentication failed: password is too long" ) except UnicodeEncodeError as e: LOGGER.warning( f"An error occurred while attempting to decode password for user {username} -> {type(e).__name__}: {e}" ) raise HTTPException( status_code=413, detail="Authentication failed: invalid characters in password", ) if not ( user := await get_user_by_username( username, include_secrets=True, restricted_ok=True ) ): raise HTTPException( status_code=413, detail="Authentication failed: the user does not exist", ) user: PrivateUserModel if not bcrypt.checkpw(password, user.password_hash): raise HTTPException( status_code=413, detail="Authentication failed: password mismatch", ) token = MANAGER.create_access_token( expires=timedelta(seconds=SESSION_EXPIRE_LIMIT), data={"sub": str(user.public_id)}, ) response.set_cookie( secure=SECURE_COOKIE, key=SESSION_COOKIE_NAME, max_age=SESSION_EXPIRE_LIMIT, value=token, httponly=COOKIE_HTTPONLY, samesite=COOKIE_SAMESITE_POLICY, domain=COOKIE_DOMAIN or None, path=COOKIE_PATH or "/", ) return APIResponse(status_code=200, msg="Authentication successful") @router.get( "/user/logout", tags=["Users"], status_code=200, responses={200: {"model": APIResponse}, 422: {"model": UnprocessableEntity}}, ) @LIMITER.limit("5/minute") async def logout( request: Request, response: Response, _user: UserModel = Depends(UNVERIFIED_MANAGER) ): """ Deletes a user's session cookie, logging them out. Endpoint is limited to 5 hits per minute """ response.delete_cookie( secure=SECURE_COOKIE, key=SESSION_COOKIE_NAME, httponly=COOKIE_HTTPONLY, samesite=COOKIE_SAMESITE_POLICY, domain=COOKIE_DOMAIN or None, path=COOKIE_PATH or "/", ) return APIResponse(status_code=200, msg="Logged out")