Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add django_auth to ninja API #94

Merged
merged 5 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion django_ai_assistant/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from langchain_core.messages import message_to_dict
from ninja import NinjaAPI
from ninja.operation import Operation
from ninja.security import django_auth

from django_ai_assistant import package_name, version
from django_ai_assistant.api.schemas import (
Expand All @@ -26,7 +27,14 @@ def get_openapi_operation_id(self, operation: Operation) -> str:
return (package_name + "_" + name).replace(".", "_")


api = API(title=package_name, version=version, urls_namespace="django_ai_assistant")
api = API(
title=package_name,
version=version,
urls_namespace="django_ai_assistant",
# Add auth to all endpoints
auth=django_auth,
csrf=True,
)


@api.exception_handler(AIUserNotAllowedError)
Expand Down
7 changes: 1 addition & 6 deletions example/assets/js/components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,7 @@ function ChatMessageList({
deleteMessage,
}: {
messages: ThreadMessagesSchemaOut[];
deleteMessage: ({
threadId,
messageId,
}: {
messageId: string;
}) => Promise<void>;
deleteMessage: ({ messageId }: { messageId: string }) => Promise<void>;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive-by fix

}) {
if (messages.length === 0) {
return <Text c="dimmed">No messages.</Text>;
Expand Down
77 changes: 67 additions & 10 deletions frontend/openapi_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@
}
}
}
}
},
"security": [
{
"SessionAuth": []
}
]
}
},
"/assistants/{assistant_id}/": {
Expand Down Expand Up @@ -55,7 +60,12 @@
}
}
}
}
},
"security": [
{
"SessionAuth": []
}
]
}
},
"/threads/": {
Expand All @@ -78,7 +88,12 @@
}
}
}
}
},
"security": [
{
"SessionAuth": []
}
]
},
"post": {
"operationId": "django_ai_assistant_create_thread",
Expand All @@ -105,7 +120,12 @@
}
},
"required": true
}
},
"security": [
{
"SessionAuth": []
}
]
}
},
"/threads/{thread_id}/": {
Expand Down Expand Up @@ -134,7 +154,12 @@
}
}
}
}
},
"security": [
{
"SessionAuth": []
}
]
},
"patch": {
"operationId": "django_ai_assistant_update_thread",
Expand Down Expand Up @@ -171,7 +196,12 @@
}
},
"required": true
}
},
"security": [
{
"SessionAuth": []
}
]
},
"delete": {
"operationId": "django_ai_assistant_delete_thread",
Expand All @@ -191,7 +221,12 @@
"204": {
"description": "No Content"
}
}
},
"security": [
{
"SessionAuth": []
}
]
}
},
"/threads/{thread_id}/messages/": {
Expand Down Expand Up @@ -224,7 +259,12 @@
}
}
}
}
},
"security": [
{
"SessionAuth": []
}
]
},
"post": {
"operationId": "django_ai_assistant_create_thread_message",
Expand Down Expand Up @@ -254,7 +294,12 @@
}
},
"required": true
}
},
"security": [
{
"SessionAuth": []
}
]
}
},
"/threads/{thread_id}/messages/{message_id}/": {
Expand Down Expand Up @@ -285,7 +330,12 @@
"204": {
"description": "No Content"
}
}
},
"security": [
{
"SessionAuth": []
}
]
}
}
},
Expand Down Expand Up @@ -414,6 +464,13 @@
"title": "ThreadMessagesSchemaIn",
"type": "object"
}
},
"securitySchemes": {
"SessionAuth": {
"type": "apiKey",
"in": "cookie",
"name": "sessionid"
}
}
},
"servers": []
Expand Down
18 changes: 17 additions & 1 deletion frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
"generate-client": "openapi-ts"
},
"dependencies": {
"axios": "^1.7.2"
"axios": "^1.7.2",
"cookie": "^0.6.0"
},
"peerDependencies": {
"react": "^18.3.1",
Expand All @@ -52,6 +53,7 @@
"@hey-api/openapi-ts": "^0.46.3",
"@testing-library/dom": "^10.1.0",
"@testing-library/react": "^16.0.0",
"@types/cookie": "^0.6.0",
"@types/jest": "^29.5.12",
"@types/node": "^20.14.1",
"@types/react": "^18.3.3",
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
import cookie from "cookie";

import { OpenAPI } from "./client";
import { AxiosRequestConfig } from "axios";

/**
* Configures the base URL for the AI Assistant API which is path associated with
* the Django include.
*
* Configures the Axios request to include the CSRF token if it exists.
*
* @param baseURL Base URL of the AI Assistant API.
*
* @example
* configAIAssistant({ baseURL: "ai-assistant" });
*/
export function configAIAssistant({ baseURL }: { baseURL: string }) {
OpenAPI.BASE = baseURL;

OpenAPI.interceptors.request.use((request: AxiosRequestConfig) => {
const { csrftoken } = cookie.parse(document.cookie);
if (request.headers && csrftoken) {
request.headers["X-CSRFTOKEN"] = csrftoken;
}
return request;
});
Comment on lines +20 to +26
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Necessary due to auth=django_auth

}
15 changes: 9 additions & 6 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ def authenticated_client(client):
# Assistant Views


def test_list_assistants_with_results(client):
response = client.get(reverse("django_ai_assistant:assistants_list"))
@pytest.mark.django_db()
def test_list_assistants_with_results(authenticated_client):
response = authenticated_client.get(reverse("django_ai_assistant:assistants_list"))

assert response.status_code == HTTPStatus.OK
assert response.json() == [{"id": "temperature_assistant", "name": "Temperature Assistant"}]
Expand All @@ -62,8 +63,9 @@ def test_does_not_list_assistants_if_unauthorized():
pass


def test_get_assistant_that_exists(client):
response = client.get(
@pytest.mark.django_db()
def test_get_assistant_that_exists(authenticated_client):
response = authenticated_client.get(
reverse(
"django_ai_assistant:assistant_detail", kwargs={"assistant_id": "temperature_assistant"}
)
Expand All @@ -73,9 +75,10 @@ def test_get_assistant_that_exists(client):
assert response.json() == {"id": "temperature_assistant", "name": "Temperature Assistant"}


def test_get_assistant_that_does_not_exist(client):
@pytest.mark.django_db()
def test_get_assistant_that_does_not_exist(authenticated_client):
with pytest.raises(AIAssistantNotDefinedError):
client.get(
authenticated_client.get(
reverse(
"django_ai_assistant:assistant_detail", kwargs={"assistant_id": "fake_assistant"}
)
Expand Down