From 7532b558c74962e5916b951235e8578cc8e882a2 Mon Sep 17 00:00:00 2001
From: Mamadou DICKO <63923024+mamadoudicko@users.noreply.github.com>
Date: Tue, 1 Aug 2023 09:24:57 +0200
Subject: [PATCH] feat: add user level open ai key management (#805)
* feat: add user user identity table
* feat: add user openai api key input
* feat: add encryption missing message
* chore: log more details about 422 errors
* docs(API): update api creation path
* feat: use user openai key if defined
---
backend/core/main.py | 24 +++++-
backend/core/models/user_identity.py | 9 +++
backend/core/models/users.py | 5 +-
.../user_identity/create_user_identity.py | 13 ++++
.../user_identity/get_user_identity.py | 21 +++++
.../user_identity/update_user_identity.py | 36 +++++++++
backend/core/routes/upload_routes.py | 10 ++-
backend/core/routes/user_routes.py | 35 +++++++++
docs/docs/backend/api/getting_started.md | 7 +-
.../components/ApiKeyConfig/ApiKeyConfig.tsx | 7 +-
.../ApiKeyConfig/hooks/useApiKeyConfig.ts | 3 +
.../components/ApiKeyConfig/ApiKeyConfig.tsx | 62 ++++++++++++++-
.../__tests__/ApiKeyConfig.test.tsx | 5 +-
.../hooks/__tests__/useApiKeyConfig.test.ts | 31 ++++++++
.../ApiKeyConfig/hooks/useApiKeyConfig.ts | 77 ++++++++++++++++++-
.../lib/api/user/__tests__/useUserApi.test.ts | 48 ++++++++++++
frontend/lib/api/user/useUserApi.ts | 19 +++++
frontend/lib/api/user/user.ts | 25 ++++++
...20230731172400_add_user_identity_table.sql | 16 ++++
scripts/tables.sql | 10 ++-
20 files changed, 452 insertions(+), 11 deletions(-)
create mode 100644 backend/core/models/user_identity.py
create mode 100644 backend/core/repository/user_identity/create_user_identity.py
create mode 100644 backend/core/repository/user_identity/get_user_identity.py
create mode 100644 backend/core/repository/user_identity/update_user_identity.py
create mode 100644 frontend/lib/api/user/__tests__/useUserApi.test.ts
create mode 100644 frontend/lib/api/user/useUserApi.ts
create mode 100644 frontend/lib/api/user/user.ts
create mode 100644 scripts/20230731172400_add_user_identity_table.sql
diff --git a/backend/core/main.py b/backend/core/main.py
index a0f1d35cd..749acbf6c 100644
--- a/backend/core/main.py
+++ b/backend/core/main.py
@@ -2,7 +2,8 @@ import os
import pypandoc
import sentry_sdk
-from fastapi import FastAPI, HTTPException
+from fastapi import FastAPI, HTTPException, Request, status
+from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from logger import get_logger
from middlewares.cors import add_cors_middleware
@@ -53,3 +54,24 @@ async def http_exception_handler(_, exc):
status_code=exc.status_code,
content={"detail": exc.detail},
)
+
+
+# log more details about validation errors (422)
+def handle_request_validation_error(app: FastAPI):
+ @app.exception_handler(RequestValidationError)
+ async def validation_exception_handler(
+ request: Request, exc: RequestValidationError
+ ):
+ exc_str = f"{exc}".replace("\n", " ").replace(" ", " ")
+ logger.error(request, exc_str)
+ content = {
+ "status_code": status.HTTP_422_UNPROCESSABLE_ENTITY,
+ "message": exc_str,
+ "data": None,
+ }
+ return JSONResponse(
+ content=content, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY
+ )
+
+
+handle_request_validation_error(app)
diff --git a/backend/core/models/user_identity.py b/backend/core/models/user_identity.py
new file mode 100644
index 000000000..aac40b8cd
--- /dev/null
+++ b/backend/core/models/user_identity.py
@@ -0,0 +1,9 @@
+from typing import Optional
+from uuid import UUID
+
+from pydantic import BaseModel
+
+
+class UserIdentity(BaseModel):
+ user_id: UUID
+ openai_api_key: Optional[str] = None
diff --git a/backend/core/models/users.py b/backend/core/models/users.py
index 0ebc05e5b..5294f1ac7 100644
--- a/backend/core/models/users.py
+++ b/backend/core/models/users.py
@@ -2,19 +2,20 @@ from typing import Optional
from uuid import UUID
from logger import get_logger
-from models.settings import common_dependencies
from pydantic import BaseModel
+from models.settings import common_dependencies
+
logger = get_logger(__name__)
+# [TODO] Rename the user table and its references to 'user_usage'
class User(BaseModel):
id: UUID
email: Optional[str]
user_openai_api_key: Optional[str] = None
requests_count: int = 0
- # [TODO] Rename the user table and its references to 'user_usage'
def create_user(self, date):
"""
Create a new user entry in the database
diff --git a/backend/core/repository/user_identity/create_user_identity.py b/backend/core/repository/user_identity/create_user_identity.py
new file mode 100644
index 000000000..26d95e8f6
--- /dev/null
+++ b/backend/core/repository/user_identity/create_user_identity.py
@@ -0,0 +1,13 @@
+from models.settings import common_dependencies
+from models.user_identity import UserIdentity
+
+
+def create_user_identity(user_identity: UserIdentity) -> UserIdentity:
+ commons = common_dependencies()
+ user_identity_dict = user_identity.dict()
+ user_identity_dict["user_id"] = str(user_identity.user_id)
+ response = (
+ commons["supabase"].from_("user_identity").insert(user_identity_dict).execute()
+ )
+
+ return UserIdentity(**response.data[0])
diff --git a/backend/core/repository/user_identity/get_user_identity.py b/backend/core/repository/user_identity/get_user_identity.py
new file mode 100644
index 000000000..0e3c2a236
--- /dev/null
+++ b/backend/core/repository/user_identity/get_user_identity.py
@@ -0,0 +1,21 @@
+from uuid import UUID
+
+from models.settings import common_dependencies
+from models.user_identity import UserIdentity
+from repository.user_identity.create_user_identity import create_user_identity
+
+
+def get_user_identity(user_id: UUID) -> UserIdentity:
+ commons = common_dependencies()
+ response = (
+ commons["supabase"]
+ .from_("user_identity")
+ .select("*")
+ .filter("user_id", "eq", user_id)
+ .execute()
+ )
+
+ if len(response.data) == 0:
+ return create_user_identity(UserIdentity(user_id=user_id))
+
+ return UserIdentity(**response.data[0])
diff --git a/backend/core/repository/user_identity/update_user_identity.py b/backend/core/repository/user_identity/update_user_identity.py
new file mode 100644
index 000000000..a30136e2b
--- /dev/null
+++ b/backend/core/repository/user_identity/update_user_identity.py
@@ -0,0 +1,36 @@
+from typing import Optional
+from uuid import UUID
+
+from models.settings import common_dependencies
+from models.user_identity import UserIdentity
+from pydantic import BaseModel
+from repository.user_identity.create_user_identity import (
+ create_user_identity,
+)
+
+
+class UserIdentityUpdatableProperties(BaseModel):
+ openai_api_key: Optional[str]
+
+
+def update_user_identity(
+ user_id: UUID,
+ user_identity_updatable_properties: UserIdentityUpdatableProperties,
+) -> UserIdentity:
+ commons = common_dependencies()
+ response = (
+ commons["supabase"]
+ .from_("user_identity")
+ .update(user_identity_updatable_properties.__dict__)
+ .filter("user_id", "eq", user_id)
+ .execute()
+ )
+
+ if len(response.data) == 0:
+ user_identity = UserIdentity(
+ user_id=user_id,
+ openai_api_key=user_identity_updatable_properties.openai_api_key,
+ )
+ return create_user_identity(user_identity)
+
+ return UserIdentity(**response.data[0])
diff --git a/backend/core/routes/upload_routes.py b/backend/core/routes/upload_routes.py
index d423300fc..4a9a181c5 100644
--- a/backend/core/routes/upload_routes.py
+++ b/backend/core/routes/upload_routes.py
@@ -7,6 +7,7 @@ from models.brains import Brain
from models.files import File
from models.settings import common_dependencies
from models.users import User
+from repository.user_identity.get_user_identity import get_user_identity
from utils.file import convert_bytes, get_file_size
from utils.processors import filter_file
@@ -59,12 +60,19 @@ async def upload_file(
"type": "error",
}
else:
+ openai_api_key = request.headers.get("Openai-Api-Key", None)
+ if openai_api_key is None:
+ openai_api_key = brain.get_brain_details()["openai_api_key"]
+
+ if openai_api_key is None:
+ openai_api_key = get_user_identity(current_user.id).openai_api_key
+
message = await filter_file(
commons,
file,
enable_summarization,
brain_id=brain_id,
- openai_api_key=request.headers.get("Openai-Api-Key", None),
+ openai_api_key=openai_api_key,
)
return message
diff --git a/backend/core/routes/user_routes.py b/backend/core/routes/user_routes.py
index ae0b68c8f..c54fc455f 100644
--- a/backend/core/routes/user_routes.py
+++ b/backend/core/routes/user_routes.py
@@ -5,7 +5,13 @@ from auth import AuthBearer, get_current_user
from fastapi import APIRouter, Depends, Request
from models.brains import Brain, get_default_user_brain
from models.settings import BrainRateLimiting
+from models.user_identity import UserIdentity
from models.users import User
+from repository.user_identity.get_user_identity import get_user_identity
+from repository.user_identity.update_user_identity import (
+ UserIdentityUpdatableProperties,
+ update_user_identity,
+)
user_router = APIRouter()
@@ -56,3 +62,32 @@ async def get_user_endpoint(
"requests_stats": requests_stats,
"date": date,
}
+
+
+@user_router.put(
+ "/user/identity",
+ dependencies=[Depends(AuthBearer())],
+ tags=["User"],
+)
+def update_user_identity_route(
+ user_identity_updatable_properties: UserIdentityUpdatableProperties,
+ current_user: User = Depends(get_current_user),
+) -> UserIdentity:
+ """
+ Update user identity.
+ """
+ return update_user_identity(current_user.id, user_identity_updatable_properties)
+
+
+@user_router.get(
+ "/user/identity",
+ dependencies=[Depends(AuthBearer())],
+ tags=["User"],
+)
+def get_user_identity_route(
+ current_user: User = Depends(get_current_user),
+) -> UserIdentity:
+ """
+ Get user identity.
+ """
+ return get_user_identity(current_user.id)
diff --git a/docs/docs/backend/api/getting_started.md b/docs/docs/backend/api/getting_started.md
index 707d1506a..c8f0a2b5b 100644
--- a/docs/docs/backend/api/getting_started.md
+++ b/docs/docs/backend/api/getting_started.md
@@ -9,25 +9,30 @@ sidebar_position: 1
**Swagger**: https://api.quivr.app/docs
## Overview
+
This documentation outlines the key points and usage instructions for interacting with the API backend. Please follow the guidelines below to use the backend services effectively.
## Usage Instructions
1. Standalone Backend
+
- The backend can now be used independently without the frontend application.
- Users can interact with the API endpoints directly using API testing tools like Postman.
2. Generating API Key
+
- To access the backend services, you need to sign in to the frontend application.
- - Once signed in, navigate to the `/config` page to generate a new API key.
+ - Once signed in, navigate to the `/user` page to generate a new API key.
- The API key will be required to authenticate your requests to the backend.
3. Authenticating Requests
+
- When making requests to the backend API, include the following header:
- `Authorization: Bearer {api_key}`
- Replace `{api_key}` with the generated API key obtained from the frontend.
4. Future Plans
+
- The development team has plans to introduce additional features and improvements.
- These include the ability to delete API keys and view the list of active keys.
- The GitHub roadmap will provide more details on upcoming features, including addressing active issues.
diff --git a/frontend/app/config/components/ApiKeyConfig/ApiKeyConfig.tsx b/frontend/app/config/components/ApiKeyConfig/ApiKeyConfig.tsx
index 11e628c73..776c47074 100644
--- a/frontend/app/config/components/ApiKeyConfig/ApiKeyConfig.tsx
+++ b/frontend/app/config/components/ApiKeyConfig/ApiKeyConfig.tsx
@@ -5,7 +5,12 @@ import Button from "@/lib/components/ui/Button";
import { useApiKeyConfig } from "./hooks/useApiKeyConfig";
export const ApiKeyConfig = (): JSX.Element => {
- const { apiKey, handleCopyClick, handleCreateClick } = useApiKeyConfig();
+ const {
+ apiKey,
+ handleCopyClick,
+
+ handleCreateClick,
+ } = useApiKeyConfig();
return (
<>
diff --git a/frontend/app/config/components/ApiKeyConfig/hooks/useApiKeyConfig.ts b/frontend/app/config/components/ApiKeyConfig/hooks/useApiKeyConfig.ts
index 6d9b3d064..204c7f3e4 100644
--- a/frontend/app/config/components/ApiKeyConfig/hooks/useApiKeyConfig.ts
+++ b/frontend/app/config/components/ApiKeyConfig/hooks/useApiKeyConfig.ts
@@ -6,6 +6,7 @@ import { useEventTracking } from "@/services/analytics/useEventTracking";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useApiKeyConfig = () => {
const [apiKey, setApiKey] = useState("");
+ const [openAiApiKey, setOpenAiApiKey] = useState("");
const { track } = useEventTracking();
const { createApiKey } = useAuthApi();
@@ -38,5 +39,7 @@ export const useApiKeyConfig = () => {
handleCreateClick,
apiKey,
handleCopyClick,
+ openAiApiKey,
+ setOpenAiApiKey,
};
};
diff --git a/frontend/app/user/components/ApiKeyConfig/ApiKeyConfig.tsx b/frontend/app/user/components/ApiKeyConfig/ApiKeyConfig.tsx
index 28ab0a20c..1bdad8d38 100644
--- a/frontend/app/user/components/ApiKeyConfig/ApiKeyConfig.tsx
+++ b/frontend/app/user/components/ApiKeyConfig/ApiKeyConfig.tsx
@@ -1,12 +1,25 @@
+/* eslint-disable max-lines */
"use client";
import Button from "@/lib/components/ui/Button";
import { Divider } from "@/lib/components/ui/Divider";
+import Field from "@/lib/components/ui/Field";
import { useApiKeyConfig } from "./hooks/useApiKeyConfig";
export const ApiKeyConfig = (): JSX.Element => {
- const { apiKey, handleCopyClick, handleCreateClick } = useApiKeyConfig();
+ const {
+ apiKey,
+ handleCopyClick,
+ handleCreateClick,
+ openAiApiKey,
+ setOpenAiApiKey,
+ changeOpenAiApiKey,
+ changeOpenAiApiKeyRequestPending,
+ userIdentity,
+ removeOpenAiApiKey,
+ hasOpenAiApiKey,
+ } = useApiKeyConfig();
return (
<>
@@ -36,6 +49,53 @@ export const ApiKeyConfig = (): JSX.Element => {
)}
+
+