diff --git a/backend/auth/api_key_handler.py b/backend/auth/api_key_handler.py index e0ecfa800..0ee26a90e 100644 --- a/backend/auth/api_key_handler.py +++ b/backend/auth/api_key_handler.py @@ -1,14 +1,17 @@ from datetime import datetime from fastapi import HTTPException -from models.settings import CommonsDep +from models.settings import common_dependencies from pydantic import DateError -async def verify_api_key(api_key: str, commons: CommonsDep): +async def verify_api_key( + api_key: str, +): try: # Use UTC time to avoid timezone issues current_date = datetime.utcnow().date() + commons = common_dependencies() result = ( commons["supabase"] .table("api_keys") @@ -30,7 +33,11 @@ async def verify_api_key(api_key: str, commons: CommonsDep): return False -async def get_user_from_api_key(api_key: str, commons: CommonsDep): +async def get_user_from_api_key( + api_key: str, +): + commons = common_dependencies() + # Lookup the user_id from the api_keys table user_id_data = ( commons["supabase"] diff --git a/backend/auth/auth_bearer.py b/backend/auth/auth_bearer.py index 1a1760050..e350721ce 100644 --- a/backend/auth/auth_bearer.py +++ b/backend/auth/auth_bearer.py @@ -5,7 +5,6 @@ from auth.api_key_handler import get_user_from_api_key, verify_api_key from auth.jwt_token_handler import decode_access_token, verify_token from fastapi import Depends, HTTPException, Request from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer -from models.settings import CommonsDep from models.users import User @@ -13,31 +12,43 @@ class AuthBearer(HTTPBearer): def __init__(self, auto_error: bool = True): super().__init__(auto_error=auto_error) - async def __call__(self, request: Request, commons: CommonsDep): + async def __call__( + self, + request: Request, + ): credentials: Optional[HTTPAuthorizationCredentials] = await super().__call__( request ) self.check_scheme(credentials) token = credentials.credentials - return await self.authenticate(token, commons) + return await self.authenticate( + token, + ) def check_scheme(self, credentials): - if credentials and not credentials.scheme == "Bearer": - raise HTTPException(status_code=402, detail="Invalid authorization scheme.") + if credentials and credentials.scheme != "Bearer": + raise HTTPException(status_code=401, detail="Token must be Bearer") elif not credentials: - raise HTTPException(status_code=403, detail="Invalid authorization code.") + raise HTTPException( + status_code=403, detail="Authentication credentials missing" + ) - async def authenticate(self, token: str, commons: CommonsDep): + async def authenticate( + self, + token: str, + ): if os.environ.get("AUTHENTICATE") == "false": return self.get_test_user() elif verify_token(token): return decode_access_token(token) - elif await verify_api_key(token, commons): - return await get_user_from_api_key(token, commons) - else: - raise HTTPException( - status_code=402, detail="Invalid token or expired token." + elif await verify_api_key( + token, + ): + return await get_user_from_api_key( + token, ) + else: + raise HTTPException(status_code=401, detail="Invalid token or api key.") def get_test_user(self): return {"email": "test@example.com"} # replace with test user information diff --git a/backend/main.py b/backend/main.py index 2034ba58e..9f4bed96c 100644 --- a/backend/main.py +++ b/backend/main.py @@ -40,7 +40,7 @@ app.include_router(stream_router) @app.exception_handler(HTTPException) -async def http_exception_handler(request, exc): +async def http_exception_handler(_, exc): return JSONResponse( status_code=exc.status_code, content={"detail": exc.detail}, diff --git a/backend/models/brains.py b/backend/models/brains.py index b8b065b07..12024e2dd 100644 --- a/backend/models/brains.py +++ b/backend/models/brains.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Tuple +from typing import Optional from uuid import UUID from models.settings import CommonsDep, common_dependencies diff --git a/backend/repository/chat/update_chat_history.py b/backend/repository/chat/update_chat_history.py index b42cf1e6f..0cfd1d86c 100644 --- a/backend/repository/chat/update_chat_history.py +++ b/backend/repository/chat/update_chat_history.py @@ -1,6 +1,7 @@ from models.chat import ChatHistory from models.settings import common_dependencies from typing import List # For type hinting +from fastapi import HTTPException def update_chat_history( @@ -20,5 +21,7 @@ def update_chat_history( .execute() ).data if len(response) == 0: - raise Exception("Error while updating chat history") + raise HTTPException( + status_code=500, detail="An exception occurred while updating chat history." + ) return response[0] diff --git a/backend/routes/api_key_routes.py b/backend/routes/api_key_routes.py index 0d94ae1e7..64fc04948 100644 --- a/backend/routes/api_key_routes.py +++ b/backend/routes/api_key_routes.py @@ -1,4 +1,3 @@ -import time from datetime import datetime from secrets import token_hex from typing import List @@ -20,14 +19,23 @@ class ApiKeyInfo(BaseModel): key_id: str creation_time: str + class ApiKey(BaseModel): api_key: str - + api_key_router = APIRouter() -@api_key_router.post("/api-key", response_model=ApiKey, dependencies=[Depends(AuthBearer())], tags=["API Key"]) -async def create_api_key(commons: CommonsDep, current_user: User = Depends(get_current_user)): + +@api_key_router.post( + "/api-key", + response_model=ApiKey, + dependencies=[Depends(AuthBearer())], + tags=["API Key"], +) +async def create_api_key( + commons: CommonsDep, current_user: User = Depends(get_current_user) +): """ Create new API key for the current user. @@ -47,13 +55,19 @@ async def create_api_key(commons: CommonsDep, current_user: User = Depends(get_c while not api_key_inserted: try: # Attempt to insert new API key into database - commons['supabase'].table('api_keys').insert([{ - "key_id": new_key_id, - "user_id": user_id, - "api_key": new_api_key, - "creation_time": datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"), - "is_active": True - }]).execute() + commons["supabase"].table("api_keys").insert( + [ + { + "key_id": new_key_id, + "user_id": user_id, + "api_key": new_api_key, + "creation_time": datetime.utcnow().strftime( + "%Y-%m-%d %H:%M:%S" + ), + "is_active": True, + } + ] + ).execute() api_key_inserted = True @@ -65,8 +79,13 @@ async def create_api_key(commons: CommonsDep, current_user: User = Depends(get_c return {"api_key": new_api_key} -@api_key_router.delete("/api-key/{key_id}", dependencies=[Depends(AuthBearer())], tags=["API Key"]) -async def delete_api_key(key_id: str, commons: CommonsDep, current_user: User = Depends(get_current_user)): + +@api_key_router.delete( + "/api-key/{key_id}", dependencies=[Depends(AuthBearer())], tags=["API Key"] +) +async def delete_api_key( + key_id: str, commons: CommonsDep, current_user: User = Depends(get_current_user) +): """ Delete (deactivate) an API key for the current user. @@ -77,15 +96,25 @@ async def delete_api_key(key_id: str, commons: CommonsDep, current_user: User = """ - commons['supabase'].table('api_keys').update({ - "is_active": False, - "deleted_time": datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S") - }).match({"key_id": key_id, "user_id": current_user.user_id}).execute() + commons["supabase"].table("api_keys").update( + { + "is_active": False, + "deleted_time": datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"), + } + ).match({"key_id": key_id, "user_id": current_user.user_id}).execute() return {"message": "API key deleted."} -@api_key_router.get("/api-keys", response_model=List[ApiKeyInfo], dependencies=[Depends(AuthBearer())], tags=["API Key"]) -async def get_api_keys(commons: CommonsDep, current_user: User = Depends(get_current_user)): + +@api_key_router.get( + "/api-keys", + response_model=List[ApiKeyInfo], + dependencies=[Depends(AuthBearer())], + tags=["API Key"], +) +async def get_api_keys( + commons: CommonsDep, current_user: User = Depends(get_current_user) +): """ Get all active API keys for the current user. @@ -98,5 +127,12 @@ async def get_api_keys(commons: CommonsDep, current_user: User = Depends(get_cur user_id = fetch_user_id_from_credentials(commons, {"email": current_user.email}) - response = commons['supabase'].table('api_keys').select("key_id, creation_time").filter('user_id', 'eq', user_id).filter('is_active', 'eq', True).execute() - return response.data \ No newline at end of file + response = ( + commons["supabase"] + .table("api_keys") + .select("key_id, creation_time") + .filter("user_id", "eq", user_id) + .filter("is_active", "eq", True) + .execute() + ) + return response.data diff --git a/backend/routes/chat_routes.py b/backend/routes/chat_routes.py index f5c3e67ae..501e72217 100644 --- a/backend/routes/chat_routes.py +++ b/backend/routes/chat_routes.py @@ -108,7 +108,9 @@ async def update_chat_metadata_handler( user_id = fetch_user_id_from_credentials(commons, {"email": current_user.email}) chat = get_chat_by_id(chat_id) if user_id != chat.user_id: - raise HTTPException(status_code=403, detail="Chat not owned by user") + raise HTTPException( + status_code=403, detail="You should be the owner of the chat to update it." + ) return update_chat(chat_id=chat_id, chat_data=chat_data) diff --git a/docs/docs/backend/api/error_handling.md b/docs/docs/backend/api/error_handling.md new file mode 100644 index 000000000..a0b25514d --- /dev/null +++ b/docs/docs/backend/api/error_handling.md @@ -0,0 +1,56 @@ +--- +sidebar_position: 3 +--- + +# Error Handling + +**URL**: https://api.quivr.app/chat + +**Swagger**: https://api.quivr.app/docs + +## Overview + +This page provides information about common error codes, their descriptions, and examples of scenarios where these errors may occur. + +| Error Code | Description | +| ---------- | --------------------------------------------------------------------------- | +| 401 | Unauthorized: The request lacks valid authentication credentials. | +| 403 | Forbidden: The requested operation is not allowed. | +| 422 | Unprocessable Entity: The request is well-formed but contains invalid data. | +| 500 | Internal Server Error: An unexpected error occurred on the server. | + +## Error Code: 401 + +**Description**: The request lacks valid authentication credentials or the provided token/api key is invalid. + +Example Scenarios: + +- Missing or invalid authentication token/api key. +- Expired authentication token. + +## Error Code: 403 + +**Description**: The requested operation is forbidden due to insufficient privileges or credentials missing. + +Example Scenarios: + +- Attempting to access a resource without proper authorization. +- Insufficient permissions to perform a specific action. + +## Error Code: 422 + +**Description**: The request is well-formed, but contains invalid data or parameters. + +Example Scenarios: + +- Invalid input data format. +- Required fields are missing or have incorrect values. + +## Error Code: 500 + +**Description**: An unexpected error occurred on the server. + +Example Scenarios: + +- Internal server error due to a server-side issue. +- Unhandled exceptions or errors during request processing.