Skip to content

Commit

Permalink
Feat/error handling (QuivrHQ#366)
Browse files Browse the repository at this point in the history
* feat: improve error handling

* docs: explain error handling system
  • Loading branch information
mamadoudicko authored Jun 23, 2023
1 parent 59fe7b0 commit 3922d8c
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 40 deletions.
13 changes: 10 additions & 3 deletions backend/auth/api_key_handler.py
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -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"]
Expand Down
35 changes: 23 additions & 12 deletions backend/auth/auth_bearer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,50 @@
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


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": "[email protected]"} # replace with test user information
Expand Down
2 changes: 1 addition & 1 deletion backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ async def startup_event():


@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},
Expand Down
2 changes: 1 addition & 1 deletion backend/models/brains.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 4 additions & 1 deletion backend/repository/chat/update_chat_history.py
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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]
78 changes: 57 additions & 21 deletions backend/routes/api_key_routes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import time
from datetime import datetime
from secrets import token_hex
from typing import List
Expand All @@ -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.
Expand All @@ -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

Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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
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
4 changes: 3 additions & 1 deletion backend/routes/chat_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
56 changes: 56 additions & 0 deletions docs/docs/backend/api/error_handling.md
Original file line number Diff line number Diff line change
@@ -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.

0 comments on commit 3922d8c

Please sign in to comment.