From 0b4788805a5f9624a43c46a6c5abaae53bef9594 Mon Sep 17 00:00:00 2001 From: Rohit Bhati Date: Thu, 28 Mar 2024 16:56:51 +0530 Subject: [PATCH 01/17] Update env and md file for endpoints. --- .env.example | 3 ++- API.md | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index eb33d4ef..64b0027c 100644 --- a/.env.example +++ b/.env.example @@ -12,4 +12,5 @@ PROXY_URL=your_proxy_url POSTGRES_PORT=placeholder POSTGRES_DB=placeholder POSTGRES_USER=placeholder -POSTGRES_PASSWORD=placeholder \ No newline at end of file +POSTGRES_PASSWORD=placeholder +SECRET_KEY='your_secret_key' diff --git a/API.md b/API.md index 5d7155d3..5b582ed8 100644 --- a/API.md +++ b/API.md @@ -7,6 +7,80 @@ 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": "user@example.com", + "full_name": "Example User", + "address": "123 Example St, City", + "role": "user" +}).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": "new_email@example.com", + "full_name": "New Name", + "address": "456 New St, City", + "role": "admin" +}).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. From e61255ebfb2b5496e74777bff26922da3265a8ed Mon Sep 17 00:00:00 2001 From: Rohit Bhati Date: Thu, 28 Mar 2024 16:57:51 +0530 Subject: [PATCH 02/17] Add authentication on endpoints. --- backend/app/api/assistants.py | 6 ++++-- backend/app/api/runs.py | 5 +++-- backend/app/api/threads.py | 5 +++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/backend/app/api/assistants.py b/backend/app/api/assistants.py index 1667c5f4..edd89d37 100644 --- a/backend/app/api/assistants.py +++ b/backend/app/api/assistants.py @@ -1,13 +1,15 @@ 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() +router = APIRouter(dependencies=[Depends(verify_token)]) + FEATURED_PUBLIC_ASSISTANTS = [] diff --git a/backend/app/api/runs.py b/backend/app/api/runs.py index 70a72968..340d54cf 100644 --- a/backend/app/api/runs.py +++ b/backend/app/api/runs.py @@ -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 @@ -18,7 +19,7 @@ from app.storage import get_assistant from app.stream import astream_messages, to_sse -router = APIRouter() +router = APIRouter(dependencies=[Depends(verify_token)]) class CreateRunPayload(BaseModel): diff --git a/backend/app/api/threads.py b/backend/app/api/threads.py index 31fe6584..13d0489a 100644 --- a/backend/app/api/threads.py +++ b/backend/app/api/threads.py @@ -1,14 +1,15 @@ 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 import app.storage as storage from app.schema import OpengptsUserId, Thread -router = APIRouter() +router = APIRouter(dependencies=[Depends(verify_token)]) ThreadID = Annotated[str, Path(description="The ID of the thread.")] From 03b9b378d0c4baf12105a4a2747938d83d497ae1 Mon Sep 17 00:00:00 2001 From: Rohit Bhati Date: Thu, 28 Mar 2024 16:58:05 +0530 Subject: [PATCH 03/17] Create user schema. --- backend/app/schema.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/backend/app/schema.py b/backend/app/schema.py index 0c0e5923..fd1dca89 100644 --- a/backend/app/schema.py +++ b/backend/app/schema.py @@ -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.""" @@ -35,6 +37,29 @@ class Thread(TypedDict): updated_at: datetime """The last time the thread was updated.""" +class User(BaseModel): + """User model""" + 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.""" + role: str + """The role of the user.""" + creation_date: datetime + """The date and time when the user account was created.""" + last_login_date: datetime + """The date and time when the user last logged in.""" + 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, From d36a1b0529cd114b92c3fd66df4ec49a676fc2fa Mon Sep 17 00:00:00 2001 From: Rohit Bhati Date: Thu, 28 Mar 2024 16:59:14 +0530 Subject: [PATCH 04/17] Create storage for user. --- backend/app/storage.py | 141 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 139 insertions(+), 2 deletions(-) diff --git a/backend/app/storage.py b/backend/app/storage.py index bd87bfaa..e8764661 100644 --- a/backend/app/storage.py +++ b/backend/app/storage.py @@ -1,11 +1,11 @@ from datetime import datetime, timezone from typing import List, Optional, Sequence - +from fastapi import HTTPException, status from langchain_core.messages import AnyMessage from app.agent import AgentType, get_agent_executor from app.lifespan import get_pg_pool -from app.schema import Assistant, Thread +from app.schema import Assistant, Thread, User from app.stream import map_chunk_to_msg @@ -161,3 +161,140 @@ async def put_thread( "name": name, "updated_at": updated_at, } + + +async def get_user(user_id: str) -> Optional[User]: + """Get a user by ID.""" + async with get_pg_pool().acquire() as conn: + return await conn.fetchrow( + "SELECT * FROM users WHERE user_id = $1 AND is_deleted = FALSE", + user_id, + ) + + +async def list_active_users() -> List[User]: + """List all active users.""" + async with get_pg_pool().acquire() as conn: + return await conn.fetch( + "SELECT * FROM users WHERE is_active = TRUE AND is_deleted = FALSE" + ) + + +async def register_user( + username: str, + password_hash: str, + email: str, + full_name: str, + address: str, + role: str +) -> User: + """Register a new user.""" + creation_date = datetime.now(timezone.utc) + last_login_date = None + is_active = True + + async with get_pg_pool().acquire() as conn: + try: + async with conn.transaction(): + await conn.execute( + "INSERT INTO users (username, password_hash, email, full_name, address, role, creation_date, last_login_date, is_active, is_deleted) " + "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + username, password_hash, email, full_name, address, role, + creation_date, last_login_date, is_active, False + ) + return User( + username=username, + password_hash=password_hash, + email=email, + full_name=full_name, + address=address, + role=role, + creation_date=creation_date, + last_login_date=last_login_date, + is_active=is_active + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to register user", + ) + +async def login_user( + username: str, + password_hash: str +) -> Optional[User]: + """Login a user.""" + async with get_pg_pool().acquire() as conn: + try: + user_record = await conn.fetchrow( + "SELECT * FROM users WHERE username = $1 AND password_hash = $2 AND is_deleted = FALSE", + username, password_hash + ) + + if user_record is not None: + last_login_date = datetime.now(timezone.utc) + await conn.execute( + "UPDATE users SET last_login_date = $1 WHERE username = $2", + last_login_date, username + ) + return User( + username=user_record['username'], + password_hash=user_record['password_hash'], + email=user_record['email'], + full_name=user_record['full_name'], + address=user_record['address'], + role=user_record['role'], + creation_date=user_record['creation_date'], + last_login_date=last_login_date, + is_active=user_record['is_active'] + ) + else: + return None + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to login user", + ) + +async def update_user( + user_id: str, + username: str, + password_hash: str, + email: str, + full_name: str, + address: str, + role: str +) -> Optional[User]: + """Update a user.""" + async with get_pg_pool().acquire() as conn: + try: + async with conn.transaction(): + await conn.execute( + "UPDATE users SET username = $1, password_hash = $2, email = $3, full_name = $4, address = $5, role = $6 WHERE user_id = $7 AND is_deleted = FALSE", + username, password_hash, email, full_name, address, role, user_id + ) + # Retrieve the updated user to return + updated_user = await get_user(user_id) + return updated_user + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to update user", + ) + +async def delete_user(user_id: str) -> bool: + """Soft delete a user.""" + async with get_pg_pool().acquire() as conn: + try: + async with conn.transaction(): + result = await conn.execute( + "UPDATE users SET is_deleted = TRUE WHERE user_id = $1 AND is_deleted = FALSE", + user_id + ) + # Check if a row was affected + return result == 'UPDATE 1' + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to delete user", + ) From 89fb81accbda8e2fcb9c1ed4452aa26f8016d2a3 Mon Sep 17 00:00:00 2001 From: Rohit Bhati Date: Thu, 28 Mar 2024 18:04:12 +0530 Subject: [PATCH 05/17] Create api routes. --- backend/app/api/users.py | 72 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 backend/app/api/users.py diff --git a/backend/app/api/users.py b/backend/app/api/users.py new file mode 100644 index 00000000..abbc338c --- /dev/null +++ b/backend/app/api/users.py @@ -0,0 +1,72 @@ +from typing import Optional + +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(dependencies=[Depends(verify_token)]) + +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.") + role: str = Field(..., description="The role 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.") + +@router.post("/register", response_model=User, status_code=201) +async def register_user(user_register_request: UserRegisterRequest) -> User: + """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": 'registration successful'} + else: + raise HTTPException(status_code=401, detail="Invalid username or password") + +@router.post("/login", response_model=Optional[User]) +async def login_user(user_login_request: UserLoginRequest) -> Optional[User]: + """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.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.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") From 40680e9dc63ea4f8b04359883fd62ce113a0e7a8 Mon Sep 17 00:00:00 2001 From: Rohit Bhati Date: Thu, 28 Mar 2024 18:05:01 +0530 Subject: [PATCH 06/17] Add methods to create and fetch token for validation. --- backend/app/api/security.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 backend/app/api/security.py diff --git a/backend/app/api/security.py b/backend/app/api/security.py new file mode 100644 index 00000000..05961d80 --- /dev/null +++ b/backend/app/api/security.py @@ -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") From 5e6f2af15a83c5c141bdc0dda3edd676d0b0f84f Mon Sep 17 00:00:00 2001 From: Rohit Bhati Date: Fri, 29 Mar 2024 16:01:55 +0530 Subject: [PATCH 07/17] update routers for api doc and response. --- backend/app/api/__init__.py | 8 +++++++- backend/app/api/users.py | 32 ++++++++++++++++++++++++-------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py index 47d463e5..45a91b7c 100644 --- a/backend/app/api/__init__.py +++ b/backend/app/api/__init__.py @@ -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() @@ -27,3 +27,9 @@ async def ok(): prefix="/threads", tags=["threads"], ) + +router.include_router( + users_router, + prefix="/users", + tags=["users"], +) \ No newline at end of file diff --git a/backend/app/api/users.py b/backend/app/api/users.py index abbc338c..90f09c2b 100644 --- a/backend/app/api/users.py +++ b/backend/app/api/users.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, List from app.api.security import create_token, verify_token from fastapi import APIRouter, HTTPException, Depends @@ -7,7 +7,7 @@ import app.storage as storage from app.schema import User -router = APIRouter(dependencies=[Depends(verify_token)]) +router = APIRouter() class UserID(str): """Type annotation for user ID.""" @@ -26,25 +26,31 @@ class UserLoginRequest(BaseModel): username: str = Field(..., description="The username of the user.") password_hash: str = Field(..., description="The hashed password of the user.") -@router.post("/register", response_model=User, status_code=201) -async def register_user(user_register_request: UserRegisterRequest) -> 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": 'registration successful'} + return {'token': token, "message": "Register successful"} else: raise HTTPException(status_code=401, detail="Invalid username or password") -@router.post("/login", response_model=Optional[User]) -async def login_user(user_login_request: UserLoginRequest) -> Optional[User]: +@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'} + return {"token": token, "message": 'Login successful'} else: raise HTTPException(status_code=401, detail="Invalid username or password") @@ -56,6 +62,16 @@ async def get_user_by_id(user_id: UserID) -> 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.""" From d0472fb057c5a3c1a4e3a29e6cbf09821ce5fbde Mon Sep 17 00:00:00 2001 From: Rohit Bhati Date: Fri, 29 Mar 2024 16:02:25 +0530 Subject: [PATCH 08/17] Remove for testing. --- backend/app/api/assistants.py | 2 +- backend/app/api/runs.py | 2 +- backend/app/api/threads.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/app/api/assistants.py b/backend/app/api/assistants.py index edd89d37..20344ef1 100644 --- a/backend/app/api/assistants.py +++ b/backend/app/api/assistants.py @@ -8,7 +8,7 @@ import app.storage as storage from app.schema import Assistant, OpengptsUserId -router = APIRouter(dependencies=[Depends(verify_token)]) +router = APIRouter() FEATURED_PUBLIC_ASSISTANTS = [] diff --git a/backend/app/api/runs.py b/backend/app/api/runs.py index 340d54cf..dc0cb578 100644 --- a/backend/app/api/runs.py +++ b/backend/app/api/runs.py @@ -19,7 +19,7 @@ from app.storage import get_assistant from app.stream import astream_messages, to_sse -router = APIRouter(dependencies=[Depends(verify_token)]) +router = APIRouter() class CreateRunPayload(BaseModel): diff --git a/backend/app/api/threads.py b/backend/app/api/threads.py index 13d0489a..415a1675 100644 --- a/backend/app/api/threads.py +++ b/backend/app/api/threads.py @@ -9,7 +9,7 @@ import app.storage as storage from app.schema import OpengptsUserId, Thread -router = APIRouter(dependencies=[Depends(verify_token)]) +router = APIRouter() ThreadID = Annotated[str, Path(description="The ID of the thread.")] From 5771f291274a3950d81e0be0b6bcd899297fdc3f Mon Sep 17 00:00:00 2001 From: Rohit Bhati Date: Fri, 29 Mar 2024 16:04:08 +0530 Subject: [PATCH 09/17] Update middleware, schema and sql query. --- backend/app/schema.py | 7 ++++-- backend/app/server.py | 14 +++++++++++ backend/app/storage.py | 23 ++++++++++--------- .../000003_create_user_table.up.sql | 13 +++++++++++ .../000004_update_user_table.up.sql | 13 +++++++++++ 5 files changed, 57 insertions(+), 13 deletions(-) create mode 100644 backend/migrations/000003_create_user_table.up.sql create mode 100644 backend/migrations/000004_update_user_table.up.sql diff --git a/backend/app/schema.py b/backend/app/schema.py index fd1dca89..6095ea27 100644 --- a/backend/app/schema.py +++ b/backend/app/schema.py @@ -39,6 +39,9 @@ class Thread(TypedDict): class User(BaseModel): """User model""" + + user_id: UUID + """The ID of the user.""" username: str """The username of the user.""" password_hash: str @@ -53,8 +56,8 @@ class User(BaseModel): """The role of the user.""" creation_date: datetime """The date and time when the user account was created.""" - last_login_date: datetime - """The date and time when the user last logged in.""" + 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 diff --git a/backend/app/server.py b/backend/app/server.py index 3213cf2f..3576fb47 100644 --- a/backend/app/server.py +++ b/backend/app/server.py @@ -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 @@ -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 diff --git a/backend/app/storage.py b/backend/app/storage.py index e8764661..d5252dc4 100644 --- a/backend/app/storage.py +++ b/backend/app/storage.py @@ -167,7 +167,7 @@ async def get_user(user_id: str) -> Optional[User]: """Get a user by ID.""" async with get_pg_pool().acquire() as conn: return await conn.fetchrow( - "SELECT * FROM users WHERE user_id = $1 AND is_deleted = FALSE", + "SELECT * FROM \"user\" WHERE user_id = $1 AND is_deleted = FALSE", user_id, ) @@ -176,7 +176,7 @@ async def list_active_users() -> List[User]: """List all active users.""" async with get_pg_pool().acquire() as conn: return await conn.fetch( - "SELECT * FROM users WHERE is_active = TRUE AND is_deleted = FALSE" + "SELECT * FROM \"user\" WHERE is_active = TRUE AND is_deleted = FALSE" ) @@ -190,14 +190,14 @@ async def register_user( ) -> User: """Register a new user.""" creation_date = datetime.now(timezone.utc) - last_login_date = None + last_login_date = None # Assuming no login has occurred yet is_active = True async with get_pg_pool().acquire() as conn: try: async with conn.transaction(): await conn.execute( - "INSERT INTO users (username, password_hash, email, full_name, address, role, creation_date, last_login_date, is_active, is_deleted) " + "INSERT INTO \"user\" (username, password_hash, email, full_name, address, role, creation_date, last_login_date, is_active, is_deleted) " "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", username, password_hash, email, full_name, address, role, creation_date, last_login_date, is_active, False @@ -216,28 +216,28 @@ async def register_user( except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to register user", + detail=f"Failed to register user {e}", ) async def login_user( username: str, password_hash: str ) -> Optional[User]: - """Login a user.""" async with get_pg_pool().acquire() as conn: try: user_record = await conn.fetchrow( - "SELECT * FROM users WHERE username = $1 AND password_hash = $2 AND is_deleted = FALSE", + "SELECT * FROM \"user\" WHERE username = $1 AND password_hash = $2 AND is_deleted = FALSE", username, password_hash ) if user_record is not None: last_login_date = datetime.now(timezone.utc) await conn.execute( - "UPDATE users SET last_login_date = $1 WHERE username = $2", + "UPDATE \"user\" SET last_login_date = $1 WHERE username = $2", last_login_date, username ) return User( + user_id=user_record['user_id'], username=user_record['username'], password_hash=user_record['password_hash'], email=user_record['email'], @@ -253,9 +253,10 @@ async def login_user( except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to login user", + detail=f"Failed to login user {e}", ) + async def update_user( user_id: str, username: str, @@ -270,7 +271,7 @@ async def update_user( try: async with conn.transaction(): await conn.execute( - "UPDATE users SET username = $1, password_hash = $2, email = $3, full_name = $4, address = $5, role = $6 WHERE user_id = $7 AND is_deleted = FALSE", + "UPDATE \"user\" SET username = $1, password_hash = $2, email = $3, full_name = $4, address = $5, role = $6 WHERE user_id = $7 AND is_deleted = FALSE", username, password_hash, email, full_name, address, role, user_id ) # Retrieve the updated user to return @@ -288,7 +289,7 @@ async def delete_user(user_id: str) -> bool: try: async with conn.transaction(): result = await conn.execute( - "UPDATE users SET is_deleted = TRUE WHERE user_id = $1 AND is_deleted = FALSE", + "UPDATE \"user\" SET is_deleted = TRUE WHERE user_id = $1 AND is_deleted = FALSE", user_id ) # Check if a row was affected diff --git a/backend/migrations/000003_create_user_table.up.sql b/backend/migrations/000003_create_user_table.up.sql new file mode 100644 index 00000000..7b406a88 --- /dev/null +++ b/backend/migrations/000003_create_user_table.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS "user" ( + user_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + username VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + full_name VARCHAR(255) NOT NULL, + address VARCHAR(255) NOT NULL, + role VARCHAR(50) NOT NULL, + creation_date TIMESTAMP WITH TIME ZONE DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'), + last_login_date TIMESTAMP WITH TIME ZONE, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + is_deleted BOOLEAN NOT NULL DEFAULT FALSE +); diff --git a/backend/migrations/000004_update_user_table.up.sql b/backend/migrations/000004_update_user_table.up.sql new file mode 100644 index 00000000..ea1cb3bc --- /dev/null +++ b/backend/migrations/000004_update_user_table.up.sql @@ -0,0 +1,13 @@ + +-- Update the column definition to allow NULL values for last_login_date +ALTER TABLE "user" +ALTER COLUMN last_login_date DROP NOT NULL; + +-- Set default values for existing rows where last_login_date is NULL +UPDATE "user" +SET last_login_date = CURRENT_TIMESTAMP +WHERE last_login_date IS NULL; + +-- Modify the last_login_date column to use CURRENT_TIMESTAMP as the default value +ALTER TABLE "user" +ALTER COLUMN last_login_date SET DEFAULT CURRENT_TIMESTAMP; \ No newline at end of file From d550602d841ab978a37c306f001db0d303b3ca10 Mon Sep 17 00:00:00 2001 From: Rohit Bhati Date: Fri, 29 Mar 2024 16:04:32 +0530 Subject: [PATCH 10/17] Add packages. --- frontend/package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index cc40c377..0b4bcd34 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,7 +25,10 @@ "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "tailwind-merge": "^2.0.0", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "react-router-dom": "^6.22.3", + "@material-ui/core": "^4.12.3", + "react-spring": "^9.7.3" }, "devDependencies": { "@types/dompurify": "^3.0.4", From 78cf0629bc0c8d7f2804f0865abe86287078c349 Mon Sep 17 00:00:00 2001 From: Rohit Bhati Date: Tue, 2 Apr 2024 13:26:13 +0530 Subject: [PATCH 11/17] Update schema for user model. --- backend/app/api/users.py | 6 +++++- backend/app/schema.py | 2 -- backend/app/storage.py | 18 +++++++++--------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/backend/app/api/users.py b/backend/app/api/users.py index 90f09c2b..195932de 100644 --- a/backend/app/api/users.py +++ b/backend/app/api/users.py @@ -19,7 +19,6 @@ class UserRegisterRequest(BaseModel): 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.") - role: str = Field(..., description="The role of the user.") class UserLoginRequest(BaseModel): """Payload for logging in a user.""" @@ -54,6 +53,11 @@ async def login_user(user_login_request: UserLoginRequest) -> Optional[UserRespo 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.""" diff --git a/backend/app/schema.py b/backend/app/schema.py index 6095ea27..af2081c7 100644 --- a/backend/app/schema.py +++ b/backend/app/schema.py @@ -52,8 +52,6 @@ class User(BaseModel): """The full name of the user.""" address: str """The address of the user.""" - role: str - """The role of the user.""" creation_date: datetime """The date and time when the user account was created.""" last_login_date: Optional[datetime] = None diff --git a/backend/app/storage.py b/backend/app/storage.py index d5252dc4..20df81ba 100644 --- a/backend/app/storage.py +++ b/backend/app/storage.py @@ -1,3 +1,4 @@ +import uuid from datetime import datetime, timezone from typing import List, Optional, Sequence from fastapi import HTTPException, status @@ -186,9 +187,9 @@ async def register_user( email: str, full_name: str, address: str, - role: str ) -> User: """Register a new user.""" + user_id = uuid.uuid4() # Generate a unique user ID creation_date = datetime.now(timezone.utc) last_login_date = None # Assuming no login has occurred yet is_active = True @@ -197,18 +198,18 @@ async def register_user( try: async with conn.transaction(): await conn.execute( - "INSERT INTO \"user\" (username, password_hash, email, full_name, address, role, creation_date, last_login_date, is_active, is_deleted) " - "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", - username, password_hash, email, full_name, address, role, + "INSERT INTO \"user\" (username, password_hash, email, full_name, address, creation_date, last_login_date, is_active, is_deleted) " + "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", + username, password_hash, email, full_name, address, creation_date, last_login_date, is_active, False ) return User( + user_id=user_id, username=username, password_hash=password_hash, email=email, full_name=full_name, address=address, - role=role, creation_date=creation_date, last_login_date=last_login_date, is_active=is_active @@ -243,7 +244,6 @@ async def login_user( email=user_record['email'], full_name=user_record['full_name'], address=user_record['address'], - role=user_record['role'], creation_date=user_record['creation_date'], last_login_date=last_login_date, is_active=user_record['is_active'] @@ -264,15 +264,14 @@ async def update_user( email: str, full_name: str, address: str, - role: str ) -> Optional[User]: """Update a user.""" async with get_pg_pool().acquire() as conn: try: async with conn.transaction(): await conn.execute( - "UPDATE \"user\" SET username = $1, password_hash = $2, email = $3, full_name = $4, address = $5, role = $6 WHERE user_id = $7 AND is_deleted = FALSE", - username, password_hash, email, full_name, address, role, user_id + "UPDATE \"user\" SET username = $1, password_hash = $2, email = $3, full_name = $4, address = $5, WHERE user_id = $6 AND is_deleted = FALSE", + username, password_hash, email, full_name, address, user_id ) # Retrieve the updated user to return updated_user = await get_user(user_id) @@ -299,3 +298,4 @@ async def delete_user(user_id: str) -> bool: status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to delete user", ) + From e095c0a44249d6acb448625139504096bea8a8ec Mon Sep 17 00:00:00 2001 From: Rohit Bhati Date: Tue, 2 Apr 2024 13:26:50 +0530 Subject: [PATCH 12/17] Update app component. --- frontend/src/App.tsx | 211 +++-------------------------- frontend/src/components/Layout.tsx | 47 ++++++- 2 files changed, 66 insertions(+), 192 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index df449b19..ea7c27de 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,196 +1,25 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; -import { InformationCircleIcon } from "@heroicons/react/24/outline"; -import { Chat } from "./components/Chat"; -import { ChatList } from "./components/ChatList"; -import { Layout } from "./components/Layout"; -import { NewChat } from "./components/NewChat"; -import { Chat as ChatType, useChatList } from "./hooks/useChatList"; -import { useSchemas } from "./hooks/useSchemas"; -import { useStreamState } from "./hooks/useStreamState"; -import { useConfigList } from "./hooks/useConfigList"; -import { Config } from "./components/Config"; -import { MessageWithFiles } from "./utils/formTypes.ts"; -import { TYPE_NAME } from "./constants.ts"; - -function App() { - const [sidebarOpen, setSidebarOpen] = useState(false); - const { configSchema, configDefaults } = useSchemas(); - const { chats, currentChat, createChat, enterChat } = useChatList(); - const { configs, currentConfig, saveConfig, enterConfig } = useConfigList(); - const { startStream, stopStream, stream } = useStreamState(); - const [isDocumentRetrievalActive, setIsDocumentRetrievalActive] = - useState(false); - - useEffect(() => { - let configurable = null; - if (currentConfig) { - configurable = currentConfig?.config?.configurable; - } - if (currentChat && configs) { - const conf = configs.find( - (c) => c.assistant_id === currentChat.assistant_id, - ); - configurable = conf?.config?.configurable; - } - const agent_type = configurable?.["type"] as TYPE_NAME | null; - if (agent_type === null || agent_type === "chatbot") { - setIsDocumentRetrievalActive(false); - return; - } - if (agent_type === "chat_retrieval") { - setIsDocumentRetrievalActive(true); - return; - } - const tools = - (configurable?.["type==agent/tools"] as { name: string }[]) ?? []; - setIsDocumentRetrievalActive(tools.some((t) => t.name === "Retrieval")); - }, [currentConfig, currentChat, configs]); - - const startTurn = useCallback( - async (message?: MessageWithFiles, chat: ChatType | null = currentChat) => { - if (!chat) return; - const config = configs?.find( - (c) => c.assistant_id === chat.assistant_id, - )?.config; - if (!config) return; - const files = message?.files || []; - if (files.length > 0) { - const formData = files.reduce((formData, file) => { - formData.append("files", file); - return formData; - }, new FormData()); - formData.append( - "config", - JSON.stringify({ configurable: { thread_id: chat.thread_id } }), - ); - await fetch(`/ingest`, { - method: "POST", - body: formData, - }); - } - await startStream( - message - ? [ - { - content: message.message, - additional_kwargs: {}, - type: "human", - example: false, - }, - ] - : null, - chat.assistant_id, - chat.thread_id, - ); - }, - [currentChat, startStream, configs], - ); - - const startChat = useCallback( - async (message: MessageWithFiles) => { - if (!currentConfig) return; - const chat = await createChat( - message.message, - currentConfig.assistant_id, - ); - return startTurn(message, chat); - }, - [createChat, startTurn, currentConfig], - ); - - const selectChat = useCallback( - async (id: string | null) => { - if (currentChat) { - stopStream?.(true); - } - enterChat(id); - if (!id) { - enterConfig(configs?.[0]?.assistant_id ?? null); - window.scrollTo({ top: 0 }); - } - if (sidebarOpen) { - setSidebarOpen(false); - } - }, - [enterChat, stopStream, sidebarOpen, currentChat, enterConfig, configs], - ); - - const selectConfig = useCallback( - (id: string | null) => { - enterConfig(id); - enterChat(null); - }, - [enterConfig, enterChat], - ); - - const content = currentChat ? ( - - ) : currentConfig ? ( - - ) : ( - - ); - - const currentChatConfig = configs?.find( - (c) => c.assistant_id === currentChat?.assistant_id, - ); - +import React from "react"; +import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; +// import LoginPage from "./pages/LoginPage"; +// import RegisterPage from "./pages/RegisterPage"; +import Home from "./components/Home"; +import LoginSignUp from "./components/LoginSignUp"; + +const App: React.FC = () => { return ( - - {currentChatConfig.name} - { - selectConfig(currentChatConfig.assistant_id); - }} - /> - - ) : null - } - sidebarOpen={sidebarOpen} - setSidebarOpen={setSidebarOpen} - sidebar={ - { - if (configs === null || chats === null) return null; - return chats.filter((c) => - configs.some((config) => config.assistant_id === c.assistant_id), - ); - }, [chats, configs])} - currentChat={currentChat} - enterChat={selectChat} - currentConfig={currentConfig} - enterConfig={selectConfig} + + + + } /> - } - > - {configSchema ? content : null} - + } /> + {/* } /> */} + + ); -} +}; export default App; diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 551527a1..91b0849c 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -1,6 +1,8 @@ -import { Fragment } from "react"; +import { Fragment , useEffect} from "react"; import { Dialog, Transition } from "@headlessui/react"; import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline"; +import { useNavigate } from 'react-router-dom'; // Import useHistory hook from React Router + export function Layout(props: { sidebarOpen: boolean; @@ -9,6 +11,46 @@ export function Layout(props: { children: React.ReactNode; subtitle?: React.ReactNode; }) { + const navigate = useNavigate(); // Initialize useHistory hook + const authToken = sessionStorage.getItem('authToken'); + + useEffect(() => { + if (!sessionStorage.getItem('authToken')) { + navigate("/"); + }},[]) + + + const handleLogout = async (e) => { + e.preventDefault(); + + try { + const response = await fetch("http://localhost:8100/users/logout", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + + }); + + if (response.ok) { + // Login successful + const data = await response.json(); + + console.log("Login successful:", data.token); + // If authentication is successful: + sessionStorage.removeItem("authToken"); + // Redirect user to /home + navigate('/'); + } else { + // Login failed + console.error("Login failed"); + // Handle login failure (e.g., display error message to user) + } + } catch (error) { + console.error("Error:", error); + // Handle error (e.g., display error message to user) + } + }; return ( <> @@ -98,12 +140,14 @@ export function Layout(props: { Open sidebar