Feat/error handling (#366)

* feat: improve error handling

* docs: explain error handling system
This commit is contained in:
Mamadou DICKO 2023-06-23 10:36:55 +02:00 committed by GitHub
parent 59fe7b089b
commit 3922d8ca83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 155 additions and 40 deletions

View File

@ -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"]

View File

@ -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

View File

@ -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},

View File

@ -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

View File

@ -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]

View File

@ -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
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

View File

@ -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)

View File

@ -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.