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

Feature/authentication #262

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ PROXY_URL=your_proxy_url
POSTGRES_PORT=placeholder
POSTGRES_DB=placeholder
POSTGRES_USER=placeholder
POSTGRES_PASSWORD=placeholder
POSTGRES_PASSWORD=placeholder
SECRET_KEY='your_secret_key'
72 changes: 72 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,78 @@ For full API documentation, see [localhost:8100/docs](localhost:8100/docs) after

If you want to see the API docs before deployment, check out the [hosted docs here](https://opengpts-example-vz4y4ooboq-uc.a.run.app/docs).

## Register a New User

To register a new user, you can use the following API endpoint:

```python
import requests

response = requests.post('http://127.0.0.1:8100/users/register', json={
"username": "example_user",
"password_hash": "example_password_hash",
"email": "[email protected]",
"full_name": "Example User",
"address": "123 Example St, City",
}).content
```

## Login User

To login with a new user, you can use the following API endpoint

```python
import requests
response = requests.post('http://127.0.0.1:8100/users/login', json={
"username": "example_user",
"password_hash": "example_password_hash"
}).content
```
## Get All Active User
To Fetch all the active User, you can use the following API endpoint:-

```python
import requests

response = requests.get('http://127.0.0.1:8100/users')
```

## Get User by ID
To Fetch the User by ID, you can use the following API endpoint:-
Replace {user_id} with the actual ID of the user you want to delete.

```python
import requests

response = requests.get('http://127.0.0.1:8100/users/{user_id}')
```

## Update User by ID
To Update the User, you can use the following API endpoint:-
Replace {user_id} with the actual ID of the user you want to delete.

```python
import requests

response = requests.put('http://127.0.0.1:8100/users/{user_id}', json={
"username": "new_username",
"password_hash": "new_password_hash",
"email": "[email protected]",
"full_name": "New Name",
"address": "456 New St, City",
}).content
```

## Delete User by ID
To delete a user by user ID, you can use the following API endpoint:
Replace {user_id} with the actual ID of the user you want to delete.
```python
import requests

response = requests.delete('http://127.0.0.1:8100/users/{user_id}')

```

## Create an Assistant

First, let's use the API to create an assistant.
Expand Down
8 changes: 7 additions & 1 deletion backend/app/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from app.api.assistants import router as assistants_router
from app.api.runs import router as runs_router
from app.api.threads import router as threads_router

from app.api.users import router as users_router
router = APIRouter()


Expand All @@ -27,3 +27,9 @@ async def ok():
prefix="/threads",
tags=["threads"],
)

router.include_router(
users_router,
prefix="/users",
tags=["users"],
)
4 changes: 3 additions & 1 deletion backend/app/api/assistants.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from typing import Annotated, List, Optional
from uuid import uuid4

from fastapi import APIRouter, HTTPException, Path, Query
from app.api.security import verify_token
from fastapi import APIRouter, HTTPException, Path, Query, Depends
from pydantic import BaseModel, Field

import app.storage as storage
from app.schema import Assistant, OpengptsUserId

router = APIRouter()


FEATURED_PUBLIC_ASSISTANTS = []


Expand Down
3 changes: 2 additions & 1 deletion backend/app/api/runs.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import json
from typing import Optional, Sequence

from app.api.security import verify_token
import langsmith.client
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, Depends
from fastapi.exceptions import RequestValidationError
from langchain.pydantic_v1 import ValidationError
from langchain_core.messages import AnyMessage
Expand Down
30 changes: 30 additions & 0 deletions backend/app/api/security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import datetime
import os
import jwt

from fastapi import HTTPException

# Get the secret key from environment variable
SECRET_KEY = os.environ.get("SECRET_KEY")
if not SECRET_KEY:
raise ValueError("SECRET_KEY not set on your environment.")

# Token expiration time (1 hour)
TOKEN_EXPIRATION = datetime.timedelta(hours=1)

def create_token(username: str) -> str:
"""Create JWT token."""
payload = {
'username': username,
'exp': datetime.datetime.utcnow() + TOKEN_EXPIRATION
}
token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
return token

def verify_token(token: str) -> dict:
"""Verify JWT token and return payload."""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
return payload
except jwt.PyJWTError:
raise HTTPException(status_code=403, detail="Could not validate credentials")
3 changes: 2 additions & 1 deletion backend/app/api/threads.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import Annotated, List, Sequence
from uuid import uuid4

from fastapi import APIRouter, HTTPException, Path
from app.api.security import verify_token
from fastapi import APIRouter, HTTPException, Path, Depends
from langchain.schema.messages import AnyMessage
from pydantic import BaseModel, Field

Expand Down
92 changes: 92 additions & 0 deletions backend/app/api/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from typing import Optional, List

from app.api.security import create_token, verify_token
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel, Field

import app.storage as storage
from app.schema import User

router = APIRouter()

class UserID(str):
"""Type annotation for user ID."""

class UserRegisterRequest(BaseModel):
"""Payload for registering a new user."""
username: str = Field(..., description="The username of the user.")
password_hash: str = Field(..., description="The hashed password of the user.")
email: str = Field(..., description="The email of the user.")
full_name: str = Field(..., description="The full name of the user.")
address: str = Field(..., description="The address of the user.")

class UserLoginRequest(BaseModel):
"""Payload for logging in a user."""
username: str = Field(..., description="The username of the user.")
password_hash: str = Field(..., description="The hashed password of the user.")

class UserResponse(BaseModel):
"""Response model for registering a new user."""
token: str
message: str


@router.post("/register", response_model=UserResponse, status_code=201)
async def register_user(user_register_request: UserRegisterRequest) -> UserResponse:
"""Register a new user."""
user = await storage.register_user(**user_register_request.dict())
if user:
# Generate token
token = create_token(user.username)
return {'token': token, "message": "Register successful"}
else:
raise HTTPException(status_code=401, detail="Invalid username or password")

@router.post("/login", response_model=Optional[UserResponse])
async def login_user(user_login_request: UserLoginRequest) -> Optional[UserResponse]:
"""Login a user."""
user = await storage.login_user(**user_login_request.dict())
if user:
# Generate token
token = create_token(user.username)
return {"token": token, "message": 'Login successful'}
else:
raise HTTPException(status_code=401, detail="Invalid username or password")

@router.post("/logout")
async def logout_user():
# You may add additional logic here if needed, such as invalidating sessions, etc.
return {"message": "Logout successful"}

@router.get("/{user_id}", response_model=User)
async def get_user_by_id(user_id: UserID) -> User:
"""Get a user by ID."""
user = await storage.get_user(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user

@router.get("/", response_model=List[User], status_code=200)
async def list_active_users():
"""List all active users."""
users = await storage.list_active_users()
if users:
return users
else:
raise HTTPException(status_code=404, detail="No active users found")


@router.put("/{user_id}", response_model=User)
async def update_user_by_id(user_id: UserID, user_update_request: UserRegisterRequest) -> User:
"""Update a user by ID."""
user = await storage.update_user(user_id, **user_update_request.dict())
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user

@router.delete("/{user_id}", status_code=204)
async def delete_user_by_id(user_id: UserID):
"""Delete a user by ID."""
deleted = await storage.delete_user(user_id)
if not deleted:
raise HTTPException(status_code=404, detail="User not found")
28 changes: 27 additions & 1 deletion backend/app/schema.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from datetime import datetime
from typing import Annotated, Optional
from uuid import UUID

from fastapi import Cookie
from typing_extensions import TypedDict

from pydantic import BaseModel
from datetime import datetime


class Assistant(TypedDict):
"""Assistant model."""
Expand Down Expand Up @@ -35,6 +37,30 @@ class Thread(TypedDict):
updated_at: datetime
"""The last time the thread was updated."""

class User(BaseModel):
"""User model"""

user_id: UUID
"""The ID of the user."""
username: str
"""The username of the user."""
password_hash: str
"""The hashed password of the user."""
email: str
"""The email address of the user."""
full_name: str
"""The full name of the user."""
address: str
"""The address of the user."""
creation_date: datetime
"""The date and time when the user account was created."""
last_login_date: Optional[datetime] = None
"""The date and time when the user last logged in. Can be None initially."""
is_active: bool
"""Boolean flag indicating whether the user account is active."""
is_deleted: bool = False
"""indicate if the user is deleted"""


OpengptsUserId = Annotated[
str,
Expand Down
14 changes: 14 additions & 0 deletions backend/app/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import orjson
from fastapi import FastAPI, Form, UploadFile
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware

from app.api import router as api_router
from app.lifespan import lifespan
Expand All @@ -14,6 +15,19 @@

app = FastAPI(title="OpenGPTs API", lifespan=lifespan)

# CORS settings
origins = [
"http://localhost:5173", # Add additional URLs if needed
]

app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)


# Get root of app, used to point to directory containing static files
ROOT = Path(__file__).parent.parent
Expand Down
Loading