mirror of
https://github.com/StanGirard/quivr.git
synced 2024-09-11 13:15:41 +03:00
Feat/error handling (#366)
* feat: improve error handling * docs: explain error handling system
This commit is contained in:
parent
59fe7b089b
commit
3922d8ca83
@ -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"]
|
||||
|
@ -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
|
||||
|
@ -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},
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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([{
|
||||
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()
|
||||
"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({
|
||||
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()
|
||||
"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()
|
||||
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
|
@ -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)
|
||||
|
||||
|
||||
|
56
docs/docs/backend/api/error_handling.md
Normal file
56
docs/docs/backend/api/error_handling.md
Normal 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.
|
Loading…
Reference in New Issue
Block a user