From 3d5cf8409582c3f22c03c7db62186ba20b2c74bf Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 21 Aug 2023 15:59:51 -0300 Subject: [PATCH 01/38] =?UTF-8?q?=F0=9F=93=A6=20feat(models):=20add=20Toke?= =?UTF-8?q?n=20model=20to=20represent=20access=20and=20refresh=20tokens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📦 feat(models): add User model to represent user data 📦 feat(models): add UserAddModel, UserListModel, UserPatchModel, and UsersResponse models for user CRUD operations 📦 feat(models): add get_user_by_username and get_user_by_id functions to retrieve user data from the database 📦 feat(models): add update_user function to update user data in the database 📦 feat(models): add update_user_last_login_at function to update the last login timestamp of a user --- .../{ => services}/database/models/token.py | 0 .../{ => services}/database/models/user.py | 21 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) rename src/backend/langflow/{ => services}/database/models/token.py (100%) rename src/backend/langflow/{ => services}/database/models/user.py (93%) diff --git a/src/backend/langflow/database/models/token.py b/src/backend/langflow/services/database/models/token.py similarity index 100% rename from src/backend/langflow/database/models/token.py rename to src/backend/langflow/services/database/models/token.py diff --git a/src/backend/langflow/database/models/user.py b/src/backend/langflow/services/database/models/user.py similarity index 93% rename from src/backend/langflow/database/models/user.py rename to src/backend/langflow/services/database/models/user.py index 94ceb4e15..f9a3c80f8 100644 --- a/src/backend/langflow/database/models/user.py +++ b/src/backend/langflow/services/database/models/user.py @@ -1,14 +1,15 @@ -from sqlmodel import Field -from uuid import UUID, uuid4 -from pydantic import BaseModel -from typing import Optional, List -from sqlalchemy.orm import Session -from datetime import timezone, datetime -from sqlalchemy.exc import IntegrityError -from fastapi import HTTPException, Depends - +from fastapi import Depends, HTTPException +from langflow.services.database.models.base import SQLModel, SQLModelSerializable from langflow.services.utils import get_session -from langflow.services.database.models.base import SQLModelSerializable, SQLModel +from pydantic import BaseModel +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import Session +from sqlmodel import Field + + +from datetime import datetime, timezone +from typing import List, Optional +from uuid import UUID, uuid4 class User(SQLModelSerializable, table=True): From f4d12f27e670bfb2de8f9ad185fbbdfa209dd0d8 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 21 Aug 2023 16:12:49 -0300 Subject: [PATCH 02/38] Adds new AuthSettings --- .../langflow/services/auth/__init__.py | 0 src/backend/langflow/services/auth/factory.py | 0 src/backend/langflow/services/auth/service.py | 228 ++++++++++++++++++ .../langflow/services/settings/auth.py | 29 +++ .../langflow/services/settings/base.py | 19 -- .../langflow/services/settings/manager.py | 7 +- 6 files changed, 262 insertions(+), 21 deletions(-) create mode 100644 src/backend/langflow/services/auth/__init__.py create mode 100644 src/backend/langflow/services/auth/factory.py create mode 100644 src/backend/langflow/services/auth/service.py create mode 100644 src/backend/langflow/services/settings/auth.py diff --git a/src/backend/langflow/services/auth/__init__.py b/src/backend/langflow/services/auth/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/langflow/services/auth/factory.py b/src/backend/langflow/services/auth/factory.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/langflow/services/auth/service.py b/src/backend/langflow/services/auth/service.py new file mode 100644 index 000000000..5e25aa207 --- /dev/null +++ b/src/backend/langflow/services/auth/service.py @@ -0,0 +1,228 @@ +from uuid import UUID +from typing import Annotated +from jose import JWTError, jwt +from langflow.services.base import Service +from langflow.services.database.models.user import ( + User, + get_user_by_id, + get_user_by_username, +) +from sqlalchemy.orm import Session +from passlib.context import CryptContext +from fastapi.security import OAuth2PasswordBearer +from fastapi import Depends, HTTPException, status +from datetime import datetime, timedelta, timezone + +from langflow.services.utils import get_settings_manager, get_session + +from langflow.services.database.models.user import ( + update_user_last_login_at, +) + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") + + +class AuthManager(Service): + name = "auth_manager" + + def __init__(self): + self.pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + self.oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") + + +async def get_current_user( + token: Annotated[str, Depends(oauth2_scheme)], db: Session = Depends(get_session) +) -> User: + settings_manager = get_settings_manager() + + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + try: + payload = jwt.decode( + token, + settings_manager.auth_settings.SECRET_KEY, + algorithms=[settings_manager.auth_settings.ALGORITHM], + ) + user_id: UUID = payload.get("sub") # type: ignore + token_type: str = payload.get("type") # type: ignore + + if user_id is None or token_type: + raise credentials_exception + except JWTError as e: + raise credentials_exception from e + + user = get_user_by_id(db, user_id) # type: ignore + if user is None: + raise credentials_exception + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if not current_user.is_active: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def create_token(data: dict, expires_delta: timedelta): + settings_manager = get_settings_manager() + + to_encode = data.copy() + expire = datetime.now(timezone.utc) + expires_delta + to_encode["exp"] = expire + + return jwt.encode( + to_encode, + settings_manager.auth_settings.SECRET_KEY, + algorithm=settings_manager.auth_settings.ALGORITHM, + ) + + +def create_super_user(db: Session = Depends(get_session)) -> User: + settings_manager = get_settings_manager() + + super_user = get_user_by_username( + db, settings_manager.auth_settings.FIRST_SUPERUSER + ) + + if not super_user: + super_user = User( + username=settings_manager.auth_settings.FIRST_SUPERUSER, + password=get_password_hash( + settings_manager.auth_settings.FIRST_SUPERUSER_PASSWORD + ), + is_superuser=True, + is_active=True, + last_login_at=None, + ) + + db.add(super_user) + db.commit() + db.refresh(super_user) + + return super_user + + +def create_user_longterm_token(db: Session = Depends(get_session)) -> dict: + super_user = create_super_user(db) + + access_token_expires_longterm = timedelta(days=365) + access_token = create_token( + data={"sub": str(super_user.id)}, + expires_delta=access_token_expires_longterm, + ) + + # Update: last_login_at + update_user_last_login_at(super_user.id, db) + + return { + "access_token": access_token, + "refresh_token": None, + "token_type": "bearer", + } + + +def create_user_api_key(user_id: UUID) -> dict: + access_token = create_token( + data={"sub": str(user_id), "role": "api_key"}, + expires_delta=timedelta(days=365 * 2), + ) + + return {"api_key": access_token} + + +def get_user_id_from_token(token: str) -> UUID: + try: + user_id = jwt.get_unverified_claims(token)["sub"] + return UUID(user_id) + except (KeyError, JWTError, ValueError): + return UUID(int=0) + + +def create_user_tokens( + user_id: UUID, db: Session = Depends(get_session), update_last_login: bool = False +) -> dict: + settings_manager = get_settings_manager() + + access_token_expires = timedelta( + minutes=settings_manager.auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES + ) + access_token = create_token( + data={"sub": str(user_id)}, + expires_delta=access_token_expires, + ) + + refresh_token_expires = timedelta( + minutes=settings_manager.auth_settings.REFRESH_TOKEN_EXPIRE_MINUTES + ) + refresh_token = create_token( + data={"sub": str(user_id), "type": "rf"}, + expires_delta=refresh_token_expires, + ) + + # Update: last_login_at + if update_last_login: + update_user_last_login_at(user_id, db) + + return { + "access_token": access_token, + "refresh_token": refresh_token, + "token_type": "bearer", + } + + +def create_refresh_token(refresh_token: str, db: Session = Depends(get_session)): + settings_manager = get_settings_manager() + + try: + payload = jwt.decode( + refresh_token, + settings_manager.auth_settings.SECRET_KEY, + algorithms=[settings_manager.auth_settings.ALGORITHM], + ) + user_id: UUID = payload.get("sub") # type: ignore + token_type: str = payload.get("type") # type: ignore + + if user_id is None or token_type is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token" + ) + + return create_user_tokens(user_id, db) + + except JWTError as e: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid refresh token", + ) from e + + +def authenticate_user( + username: str, password: str, db: Session = Depends(get_session) +) -> User | None: + user = get_user_by_username(db, username) + + if not user: + return None + + if not user.is_active: + if not user.last_login_at: + raise HTTPException(status_code=400, detail="Waiting for approval") + raise HTTPException(status_code=400, detail="Inactive user") + + return user if verify_password(password, user.password) else None diff --git a/src/backend/langflow/services/settings/auth.py b/src/backend/langflow/services/settings/auth.py new file mode 100644 index 000000000..d144b4347 --- /dev/null +++ b/src/backend/langflow/services/settings/auth.py @@ -0,0 +1,29 @@ +from typing import Optional +import secrets + +from pydantic import BaseSettings + + +class AuthSettings(BaseSettings): + # Login settings + SECRET_KEY: str = secrets.token_hex(32) + ALGORITHM: str = "HS256" + ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 + REFRESH_TOKEN_EXPIRE_MINUTES: int = 70 + + # API Key to execute /process endpoint + API_KEY_SECRET_KEY: Optional[ + str + ] = "b82818e0ad4ff76615c5721ee21004b07d84cd9b87ba4d9cb42374da134b841a" + API_KEY_ALGORITHM: str = "HS256" + + # If AUTO_LOGIN = True + # > The application does not request login and logs in automatically as a super user. + AUTO_LOGIN: bool = True + FIRST_SUPERUSER: str = "langflow" + FIRST_SUPERUSER_PASSWORD: str = "langflow" + + class Config: + validate_assignment = True + extra = "ignore" + env_prefix = "LANGFLOW_" diff --git a/src/backend/langflow/services/settings/base.py b/src/backend/langflow/services/settings/base.py index 65a1a2909..4e1e2d35b 100644 --- a/src/backend/langflow/services/settings/base.py +++ b/src/backend/langflow/services/settings/base.py @@ -2,7 +2,6 @@ import contextlib import json import os from shutil import copy2 -import secrets from typing import Optional, List from pathlib import Path @@ -41,24 +40,6 @@ class Settings(BaseSettings): REMOVE_API_KEYS: bool = False COMPONENTS_PATH: List[str] = [] - # Login settings - SECRET_KEY: str = secrets.token_hex(32) - ALGORITHM: str = "HS256" - ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 - REFRESH_TOKEN_EXPIRE_MINUTES: int = 70 - - # API Key to execute /process endpoint - API_KEY_SECRET_KEY: Optional[ - str - ] = "b82818e0ad4ff76615c5721ee21004b07d84cd9b87ba4d9cb42374da134b841a" - API_KEY_ALGORITHM: str = "HS256" - - # If AUTO_LOGIN = True - # > The application does not request login and logs in automatically as a super user. - AUTO_LOGIN: bool = True - FIRST_SUPERUSER: str = "langflow" - FIRST_SUPERUSER_PASSWORD: str = "langflow" - @validator("CONFIG_DIR", pre=True, allow_reuse=True) def set_langflow_dir(cls, value): if not value: diff --git a/src/backend/langflow/services/settings/manager.py b/src/backend/langflow/services/settings/manager.py index a357c4804..1a6c0feeb 100644 --- a/src/backend/langflow/services/settings/manager.py +++ b/src/backend/langflow/services/settings/manager.py @@ -1,4 +1,5 @@ from langflow.services.base import Service +from langflow.services.settings.auth import AuthSettings from langflow.services.settings.base import Settings from langflow.utils.logger import logger import os @@ -8,9 +9,10 @@ import yaml class SettingsManager(Service): name = "settings_manager" - def __init__(self, settings: Settings): + def __init__(self, settings: Settings, auth_settings: AuthSettings): super().__init__() self.settings = settings + self.auth_settings = auth_settings @classmethod def load_settings_from_yaml(cls, file_path: str) -> "SettingsManager": @@ -33,4 +35,5 @@ class SettingsManager(Service): ) settings = Settings(**settings_dict) - return cls(settings) + auth_settings = AuthSettings() + return cls(settings, auth_settings) From e6bd9a07d520d559ad9065b4f75ce286fef30f8a Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 21 Aug 2023 16:33:27 -0300 Subject: [PATCH 03/38] =?UTF-8?q?=F0=9F=94=A7=20fix(auth.py):=20update=20i?= =?UTF-8?q?mport=20statements=20for=20User=20model=20and=20related=20funct?= =?UTF-8?q?ions=20to=20match=20new=20file=20structure=20=F0=9F=94=A7=20fix?= =?UTF-8?q?(auth.py):=20update=20import=20statements=20for=20settings=5Fma?= =?UTF-8?q?nager=20and=20related=20settings=20to=20match=20new=20file=20st?= =?UTF-8?q?ructure=20=F0=9F=94=A7=20fix(auth.py):=20update=20references=20?= =?UTF-8?q?to=20settings=5Fmanager.settings=20to=20settings=5Fmanager.auth?= =?UTF-8?q?=5Fsettings=20to=20match=20new=20file=20structure=20?= =?UTF-8?q?=F0=9F=94=A7=20fix(auth.py):=20update=20references=20to=20setti?= =?UTF-8?q?ngs=5Fmanager.settings=20to=20settings=5Fmanager.auth=5Fsetting?= =?UTF-8?q?s=20to=20match=20new=20file=20structure=20=F0=9F=94=A7=20fix(au?= =?UTF-8?q?th.py):=20update=20references=20to=20settings=5Fmanager.setting?= =?UTF-8?q?s=20to=20settings=5Fmanager.auth=5Fsettings=20to=20match=20new?= =?UTF-8?q?=20file=20structure=20=F0=9F=94=A7=20fix(auth.py):=20update=20r?= =?UTF-8?q?eferences=20to=20settings=5Fmanager.settings=20to=20settings=5F?= =?UTF-8?q?manager.auth=5Fsettings=20to=20match=20new=20file=20structure?= =?UTF-8?q?=20=F0=9F=94=A7=20fix(auth.py):=20update=20references=20to=20se?= =?UTF-8?q?ttings=5Fmanager.settings=20to=20settings=5Fmanager.auth=5Fsett?= =?UTF-8?q?ings=20to=20match=20new=20file=20structure=20=F0=9F=94=A7=20fix?= =?UTF-8?q?(auth.py):=20update=20references=20to=20settings=5Fmanager.sett?= =?UTF-8?q?ings=20to=20settings=5Fmanager.auth=5Fsettings=20to=20match=20n?= =?UTF-8?q?ew=20file=20structure=20=F0=9F=94=A7=20fix(auth.py):=20update?= =?UTF-8?q?=20references=20to=20settings=5Fmanager.settings=20to=20setting?= =?UTF-8?q?s=5Fmanager.auth=5Fsettings=20to=20match=20new=20file=20structu?= =?UTF-8?q?re=20=F0=9F=94=A7=20fix(auth.py):=20update=20references=20to=20?= =?UTF-8?q?settings=5Fmanager.settings=20to=20settings=5Fmanager.auth=5Fse?= =?UTF-8?q?ttings=20to=20match=20new=20file=20structure=20=F0=9F=94=A7=20f?= =?UTF-8?q?ix(auth.py):=20update=20references=20to=20settings=5Fmanager.se?= =?UTF-8?q?ttings=20to=20settings=5Fmanager.auth=5Fsettings=20to=20match?= =?UTF-8?q?=20new=20file=20structure=20=F0=9F=94=A7=20fix(auth.py):=20upda?= =?UTF-8?q?te=20references=20to=20settings=5Fmanager.settings=20to=20setti?= =?UTF-8?q?ngs=5Fmanager.auth=5Fsettings=20to=20match=20new=20file=20struc?= =?UTF-8?q?ture=20=F0=9F=94=A7=20fix(auth.py):=20update=20references=20to?= =?UTF-8?q?=20settings=5Fmanager.settings=20to=20settings=5Fmanager.auth?= =?UTF-8?q?=5Fsettings=20to=20match=20new=20file=20structure=20?= =?UTF-8?q?=F0=9F=94=A7=20fix(auth.py):=20update=20references=20to=20setti?= =?UTF-8?q?ngs=5Fmanager.settings=20to=20settings=5Fmanager.auth=5Fsetting?= =?UTF-8?q?s=20to=20match=20new=20file=20structure=20=F0=9F=94=A7=20fix(au?= =?UTF-8?q?th.py):=20update=20references=20to=20settings=5Fmanager.setting?= =?UTF-8?q?s=20to=20settings=5Fmanager.auth=5Fsettings=20to=20match=20new?= =?UTF-8?q?=20file=20structure=20=F0=9F=94=A7=20fix(auth.py):=20update=20r?= =?UTF-8?q?eferences=20to=20settings=5Fmanager.settings=20to=20settings=5F?= =?UTF-8?q?manager.auth=5Fsettings=20to=20match=20new=20file=20structure?= =?UTF-8?q?=20=F0=9F=94=A7=20fix(auth.py):=20update=20references=20to=20se?= =?UTF-8?q?ttings=5Fmanager.settings=20to=20settings=5Fmanager.auth=5Fsett?= =?UTF-8?q?ings=20to=20match=20new=20file=20structure=20=F0=9F=94=A7=20fix?= =?UTF-8?q?(auth.py):=20update=20references=20to=20settings=5Fmanager.sett?= =?UTF-8?q?ings=20to=20settings=5Fmanager.auth=5Fsettings=20to=20match=20n?= =?UTF-8?q?ew=20file=20structure=20=F0=9F=94=A7=20fix(auth.py):=20update?= =?UTF-8?q?=20references=20to=20settings=5Fmanager.settings=20to=20setting?= =?UTF-8?q?s=5Fmanager.auth=5Fsettings=20to=20match=20new=20file=20structu?= =?UTF-8?q?re=20=F0=9F=94=A7=20fix(auth.py):=20update=20references=20to=20?= =?UTF-8?q?settings=5Fmanager.settings=20to=20settings=5Fmanager.auth=5Fse?= =?UTF-8?q?ttings=20to=20match=20new=20file=20structure=20=F0=9F=94=A7=20f?= =?UTF-8?q?ix(auth.py):=20update=20references=20to=20settings=5Fmanager.se?= =?UTF-8?q?ttings=20to=20settings=5Fmanager.auth=5Fsettings=20to=20match?= =?UTF-8?q?=20new=20file=20structure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/auth/auth.py | 34 +++++++++++++++------------ src/backend/langflow/routers/login.py | 4 ++-- src/backend/langflow/routers/users.py | 14 ++++++----- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/backend/langflow/auth/auth.py b/src/backend/langflow/auth/auth.py index 9d4f12862..f274a9523 100644 --- a/src/backend/langflow/auth/auth.py +++ b/src/backend/langflow/auth/auth.py @@ -1,6 +1,11 @@ from uuid import UUID from typing import Annotated from jose import JWTError, jwt +from langflow.services.database.models.user import ( + User, + get_user_by_id, + get_user_by_username, +) from sqlalchemy.orm import Session from passlib.context import CryptContext from fastapi.security import OAuth2PasswordBearer @@ -9,10 +14,7 @@ from datetime import datetime, timedelta, timezone from langflow.services.utils import get_settings_manager, get_session -from langflow.database.models.user import ( - User, - get_user_by_id, - get_user_by_username, +from langflow.services.database.models.user import ( update_user_last_login_at, ) @@ -35,8 +37,8 @@ async def get_current_user( try: payload = jwt.decode( token, - settings_manager.settings.SECRET_KEY, - algorithms=[settings_manager.settings.ALGORITHM], + settings_manager.auth_settings.SECRET_KEY, + algorithms=[settings_manager.auth_settings.ALGORITHM], ) user_id: UUID = payload.get("sub") # type: ignore token_type: str = payload.get("type") # type: ignore @@ -77,21 +79,23 @@ def create_token(data: dict, expires_delta: timedelta): return jwt.encode( to_encode, - settings_manager.settings.SECRET_KEY, - algorithm=settings_manager.settings.ALGORITHM, + settings_manager.auth_settings.SECRET_KEY, + algorithm=settings_manager.auth_settings.ALGORITHM, ) def create_super_user(db: Session = Depends(get_session)) -> User: settings_manager = get_settings_manager() - super_user = get_user_by_username(db, settings_manager.settings.FIRST_SUPERUSER) + super_user = get_user_by_username( + db, settings_manager.auth_settings.FIRST_SUPERUSER + ) if not super_user: super_user = User( - username=settings_manager.settings.FIRST_SUPERUSER, + username=settings_manager.auth_settings.FIRST_SUPERUSER, password=get_password_hash( - settings_manager.settings.FIRST_SUPERUSER_PASSWORD + settings_manager.auth_settings.FIRST_SUPERUSER_PASSWORD ), is_superuser=True, is_active=True, @@ -147,7 +151,7 @@ def create_user_tokens( settings_manager = get_settings_manager() access_token_expires = timedelta( - minutes=settings_manager.settings.ACCESS_TOKEN_EXPIRE_MINUTES + minutes=settings_manager.auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES ) access_token = create_token( data={"sub": str(user_id)}, @@ -155,7 +159,7 @@ def create_user_tokens( ) refresh_token_expires = timedelta( - minutes=settings_manager.settings.REFRESH_TOKEN_EXPIRE_MINUTES + minutes=settings_manager.auth_settings.REFRESH_TOKEN_EXPIRE_MINUTES ) refresh_token = create_token( data={"sub": str(user_id), "type": "rf"}, @@ -179,8 +183,8 @@ def create_refresh_token(refresh_token: str, db: Session = Depends(get_session)) try: payload = jwt.decode( refresh_token, - settings_manager.settings.SECRET_KEY, - algorithms=[settings_manager.settings.ALGORITHM], + settings_manager.auth_settings.SECRET_KEY, + algorithms=[settings_manager.auth_settings.ALGORITHM], ) user_id: UUID = payload.get("sub") # type: ignore token_type: str = payload.get("type") # type: ignore diff --git a/src/backend/langflow/routers/login.py b/src/backend/langflow/routers/login.py index de255a0d5..6a103d646 100644 --- a/src/backend/langflow/routers/login.py +++ b/src/backend/langflow/routers/login.py @@ -3,7 +3,7 @@ from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm from langflow.services.utils import get_session -from langflow.database.models.token import Token +from langflow.services.database.models import Token from langflow.auth.auth import ( authenticate_user, create_user_tokens, @@ -36,7 +36,7 @@ async def login_to_get_access_token( async def auto_login(db: Session = Depends(get_session)): settings_manager = get_settings_manager() - if settings_manager.settings.AUTO_LOGIN: + if settings_manager.auth_settings.AUTO_LOGIN: return create_user_longterm_token(db) raise HTTPException( diff --git a/src/backend/langflow/routers/users.py b/src/backend/langflow/routers/users.py index 04972c976..135437b74 100644 --- a/src/backend/langflow/routers/users.py +++ b/src/backend/langflow/routers/users.py @@ -1,4 +1,11 @@ from uuid import UUID +from langflow.services.database.models.user import ( + User, + UserAddModel, + UserListModel, + UserPatchModel, + UsersResponse, +) from sqlalchemy import func from sqlalchemy.exc import IntegrityError @@ -8,12 +15,7 @@ from fastapi import APIRouter, Depends, HTTPException from langflow.services.utils import get_session from langflow.auth.auth import get_current_active_user, get_password_hash -from langflow.database.models.user import ( - User, - UserAddModel, - UserListModel, - UserPatchModel, - UsersResponse, +from langflow.services.database.models.user import ( update_user, ) From 244a967517ec9538c252d588a746a39ce2e7745f Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 21 Aug 2023 16:33:56 -0300 Subject: [PATCH 04/38] =?UTF-8?q?=F0=9F=94=A8=20refactor(service.py):=20re?= =?UTF-8?q?move=20unused=20imports=20and=20unused=20functions=20in=20the?= =?UTF-8?q?=20AuthManager=20class=20=F0=9F=94=A7=20chore(service.py):=20re?= =?UTF-8?q?factor=20the=20AuthManager=20class=20to=20accept=20a=20settings?= =?UTF-8?q?=5Fmanager=20parameter=20in=20the=20constructor=20for=20better?= =?UTF-8?q?=20dependency=20injection=20=F0=9F=94=A7=20chore(service.py):?= =?UTF-8?q?=20refactor=20the=20run=5Foauth2=5Fscheme=20method=20in=20the?= =?UTF-8?q?=20AuthManager=20class=20to=20use=20the=20oauth2=5Fscheme=20fro?= =?UTF-8?q?m=20the=20settings=5Fmanager=20instead=20of=20a=20hardcoded=20v?= =?UTF-8?q?alue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ feat(auth/utils.py): add authentication and authorization utilities for user authentication and token generation 🔒 chore(auth/utils.py): add auth_scheme_dependency function to handle authentication scheme dependency 🔒 chore(auth/utils.py): add get_current_user function to retrieve the current authenticated user 🔒 chore(auth/utils.py): add get_current_active_user function to retrieve the current active authenticated user 🔒 chore(auth/utils.py): add verify_password function to verify the password 🔒 chore(auth/utils.py): add get_password_hash function to get the hashed password 🔒 chore(auth/utils.py): add create_token function to create a JWT token 🔒 chore(auth/utils.py): add create_super_user function to create a super user 🔒 chore(auth/utils.py): add create_user_longterm_token function to create a long-term token for a user 🔒 chore(auth/utils.py): add create_user_api_key function to create an API key for a user 🔒 chore(auth/utils.py): add get_user_id_from_token function to get the user ID from a token 🔒 chore(auth/utils.py): add create_user_tokens function to create access and refresh tokens for a user 🔒 chore(auth/utils.py): add create_refresh_token function to create new access and refresh tokens using a refresh token 🔒 chore(auth/utils.py): add authenticate_user function to authenticate a user with username and password --- src/backend/langflow/services/auth/service.py | 227 +----------------- src/backend/langflow/services/auth/utils.py | 222 +++++++++++++++++ 2 files changed, 228 insertions(+), 221 deletions(-) create mode 100644 src/backend/langflow/services/auth/utils.py diff --git a/src/backend/langflow/services/auth/service.py b/src/backend/langflow/services/auth/service.py index 5e25aa207..c5f380298 100644 --- a/src/backend/langflow/services/auth/service.py +++ b/src/backend/langflow/services/auth/service.py @@ -1,228 +1,13 @@ -from uuid import UUID -from typing import Annotated -from jose import JWTError, jwt from langflow.services.base import Service -from langflow.services.database.models.user import ( - User, - get_user_by_id, - get_user_by_username, -) -from sqlalchemy.orm import Session -from passlib.context import CryptContext -from fastapi.security import OAuth2PasswordBearer -from fastapi import Depends, HTTPException, status -from datetime import datetime, timedelta, timezone - -from langflow.services.utils import get_settings_manager, get_session - -from langflow.services.database.models.user import ( - update_user_last_login_at, -) - - -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") class AuthManager(Service): name = "auth_manager" - def __init__(self): - self.pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") - self.oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") + def __init__(self, settings_manager): + self.settings_manager = settings_manager - -async def get_current_user( - token: Annotated[str, Depends(oauth2_scheme)], db: Session = Depends(get_session) -) -> User: - settings_manager = get_settings_manager() - - credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) - - try: - payload = jwt.decode( - token, - settings_manager.auth_settings.SECRET_KEY, - algorithms=[settings_manager.auth_settings.ALGORITHM], - ) - user_id: UUID = payload.get("sub") # type: ignore - token_type: str = payload.get("type") # type: ignore - - if user_id is None or token_type: - raise credentials_exception - except JWTError as e: - raise credentials_exception from e - - user = get_user_by_id(db, user_id) # type: ignore - if user is None: - raise credentials_exception - return user - - -async def get_current_active_user( - current_user: Annotated[User, Depends(get_current_user)] -): - if not current_user.is_active: - raise HTTPException(status_code=400, detail="Inactive user") - return current_user - - -def verify_password(plain_password, hashed_password): - return pwd_context.verify(plain_password, hashed_password) - - -def get_password_hash(password): - return pwd_context.hash(password) - - -def create_token(data: dict, expires_delta: timedelta): - settings_manager = get_settings_manager() - - to_encode = data.copy() - expire = datetime.now(timezone.utc) + expires_delta - to_encode["exp"] = expire - - return jwt.encode( - to_encode, - settings_manager.auth_settings.SECRET_KEY, - algorithm=settings_manager.auth_settings.ALGORITHM, - ) - - -def create_super_user(db: Session = Depends(get_session)) -> User: - settings_manager = get_settings_manager() - - super_user = get_user_by_username( - db, settings_manager.auth_settings.FIRST_SUPERUSER - ) - - if not super_user: - super_user = User( - username=settings_manager.auth_settings.FIRST_SUPERUSER, - password=get_password_hash( - settings_manager.auth_settings.FIRST_SUPERUSER_PASSWORD - ), - is_superuser=True, - is_active=True, - last_login_at=None, - ) - - db.add(super_user) - db.commit() - db.refresh(super_user) - - return super_user - - -def create_user_longterm_token(db: Session = Depends(get_session)) -> dict: - super_user = create_super_user(db) - - access_token_expires_longterm = timedelta(days=365) - access_token = create_token( - data={"sub": str(super_user.id)}, - expires_delta=access_token_expires_longterm, - ) - - # Update: last_login_at - update_user_last_login_at(super_user.id, db) - - return { - "access_token": access_token, - "refresh_token": None, - "token_type": "bearer", - } - - -def create_user_api_key(user_id: UUID) -> dict: - access_token = create_token( - data={"sub": str(user_id), "role": "api_key"}, - expires_delta=timedelta(days=365 * 2), - ) - - return {"api_key": access_token} - - -def get_user_id_from_token(token: str) -> UUID: - try: - user_id = jwt.get_unverified_claims(token)["sub"] - return UUID(user_id) - except (KeyError, JWTError, ValueError): - return UUID(int=0) - - -def create_user_tokens( - user_id: UUID, db: Session = Depends(get_session), update_last_login: bool = False -) -> dict: - settings_manager = get_settings_manager() - - access_token_expires = timedelta( - minutes=settings_manager.auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES - ) - access_token = create_token( - data={"sub": str(user_id)}, - expires_delta=access_token_expires, - ) - - refresh_token_expires = timedelta( - minutes=settings_manager.auth_settings.REFRESH_TOKEN_EXPIRE_MINUTES - ) - refresh_token = create_token( - data={"sub": str(user_id), "type": "rf"}, - expires_delta=refresh_token_expires, - ) - - # Update: last_login_at - if update_last_login: - update_user_last_login_at(user_id, db) - - return { - "access_token": access_token, - "refresh_token": refresh_token, - "token_type": "bearer", - } - - -def create_refresh_token(refresh_token: str, db: Session = Depends(get_session)): - settings_manager = get_settings_manager() - - try: - payload = jwt.decode( - refresh_token, - settings_manager.auth_settings.SECRET_KEY, - algorithms=[settings_manager.auth_settings.ALGORITHM], - ) - user_id: UUID = payload.get("sub") # type: ignore - token_type: str = payload.get("type") # type: ignore - - if user_id is None or token_type is None: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token" - ) - - return create_user_tokens(user_id, db) - - except JWTError as e: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid refresh token", - ) from e - - -def authenticate_user( - username: str, password: str, db: Session = Depends(get_session) -) -> User | None: - user = get_user_by_username(db, username) - - if not user: - return None - - if not user.is_active: - if not user.last_login_at: - raise HTTPException(status_code=400, detail="Waiting for approval") - raise HTTPException(status_code=400, detail="Inactive user") - - return user if verify_password(password, user.password) else None + # We need to define a function that can be passed to the Depends() function. + # This function will be called by FastAPI to run oauth2_scheme + def run_oauth2_scheme(self, *args, **kwargs): + return self.settings_manager.auth_settings.oauth2_scheme(*args, **kwargs) diff --git a/src/backend/langflow/services/auth/utils.py b/src/backend/langflow/services/auth/utils.py new file mode 100644 index 000000000..f897151e7 --- /dev/null +++ b/src/backend/langflow/services/auth/utils.py @@ -0,0 +1,222 @@ +from datetime import datetime, timedelta, timezone +from fastapi import Depends, HTTPException, status +from jose import JWTError, jwt +from typing import Annotated +from uuid import UUID +from langflow.services.auth.service import AuthManager +from langflow.services.database.models.user import ( + User, + get_user_by_id, + get_user_by_username, + update_user_last_login_at, +) +from langflow.services.utils import get_session, get_settings_manager +from sqlalchemy.orm import Session + + +def auth_scheme_dependency(*args, **kwargs): + settings_manager = ( + get_settings_manager() + ) # Assuming get_settings_manager is defined + + return AuthManager(settings_manager).run_oauth2_scheme(*args, **kwargs) + + +async def get_current_user( + token: Annotated[str, Depends(auth_scheme_dependency)], + db: Session = Depends(get_session), +) -> User: + settings_manager = get_settings_manager() + + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode( + token, + settings_manager.auth_settings.SECRET_KEY, + algorithms=[settings_manager.auth_settings.ALGORITHM], + ) + user_id: UUID = payload.get("sub") # type: ignore + token_type: str = payload.get("type") # type: ignore + + if user_id is None or token_type: + raise credentials_exception + except JWTError as e: + raise credentials_exception from e + + user = get_user_by_id(db, user_id) # type: ignore + if user is None: + raise credentials_exception + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if not current_user.is_active: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +def verify_password(plain_password, hashed_password): + settings_manager = get_settings_manager() + return settings_manager.auth_settings.pwd_context.verify( + plain_password, hashed_password + ) + + +def get_password_hash(password): + settings_manager = get_settings_manager() + return settings_manager.auth_settings.pwd_context.hash(password) + + +def create_token(data: dict, expires_delta: timedelta): + settings_manager = get_settings_manager() + + to_encode = data.copy() + expire = datetime.now(timezone.utc) + expires_delta + to_encode["exp"] = expire + + return jwt.encode( + to_encode, + settings_manager.auth_settings.SECRET_KEY, + algorithm=settings_manager.auth_settings.ALGORITHM, + ) + + +def create_super_user(db: Session = Depends(get_session)) -> User: + settings_manager = get_settings_manager() + + super_user = get_user_by_username( + db, settings_manager.auth_settings.FIRST_SUPERUSER + ) + + if not super_user: + super_user = User( + username=settings_manager.auth_settings.FIRST_SUPERUSER, + password=get_password_hash( + settings_manager.auth_settings.FIRST_SUPERUSER_PASSWORD + ), + is_superuser=True, + is_active=True, + last_login_at=None, + ) + + db.add(super_user) + db.commit() + db.refresh(super_user) + + return super_user + + +def create_user_longterm_token(db: Session = Depends(get_session)) -> dict: + super_user = create_super_user(db) + + access_token_expires_longterm = timedelta(days=365) + access_token = create_token( + data={"sub": str(super_user.id)}, + expires_delta=access_token_expires_longterm, + ) + + # Update: last_login_at + update_user_last_login_at(super_user.id, db) + + return { + "access_token": access_token, + "refresh_token": None, + "token_type": "bearer", + } + + +def create_user_api_key(user_id: UUID) -> dict: + access_token = create_token( + data={"sub": str(user_id), "role": "api_key"}, + expires_delta=timedelta(days=365 * 2), + ) + + return {"api_key": access_token} + + +def get_user_id_from_token(token: str) -> UUID: + try: + user_id = jwt.get_unverified_claims(token)["sub"] + return UUID(user_id) + except (KeyError, JWTError, ValueError): + return UUID(int=0) + + +def create_user_tokens( + user_id: UUID, db: Session = Depends(get_session), update_last_login: bool = False +) -> dict: + settings_manager = get_settings_manager() + + access_token_expires = timedelta( + minutes=settings_manager.auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES + ) + access_token = create_token( + data={"sub": str(user_id)}, + expires_delta=access_token_expires, + ) + + refresh_token_expires = timedelta( + minutes=settings_manager.auth_settings.REFRESH_TOKEN_EXPIRE_MINUTES + ) + refresh_token = create_token( + data={"sub": str(user_id), "type": "rf"}, + expires_delta=refresh_token_expires, + ) + + # Update: last_login_at + if update_last_login: + update_user_last_login_at(user_id, db) + + return { + "access_token": access_token, + "refresh_token": refresh_token, + "token_type": "bearer", + } + + +def create_refresh_token(refresh_token: str, db: Session = Depends(get_session)): + settings_manager = get_settings_manager() + + try: + payload = jwt.decode( + refresh_token, + settings_manager.auth_settings.SECRET_KEY, + algorithms=[settings_manager.auth_settings.ALGORITHM], + ) + user_id: UUID = payload.get("sub") # type: ignore + token_type: str = payload.get("type") # type: ignore + + if user_id is None or token_type is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token" + ) + + return create_user_tokens(user_id, db) + + except JWTError as e: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid refresh token", + ) from e + + +def authenticate_user( + username: str, password: str, db: Session = Depends(get_session) +) -> User | None: + user = get_user_by_username(db, username) + + if not user: + return None + + if not user.is_active: + if not user.last_login_at: + raise HTTPException(status_code=400, detail="Waiting for approval") + raise HTTPException(status_code=400, detail="Inactive user") + + return user if verify_password(password, user.password) else None From b309d8be0ed074ef5e6e50fb8374555199e2ecd6 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 21 Aug 2023 16:34:23 -0300 Subject: [PATCH 05/38] =?UTF-8?q?=F0=9F=94=92=20chore(auth.py):=20import?= =?UTF-8?q?=20necessary=20modules=20and=20add=20password=20context=20and?= =?UTF-8?q?=20OAuth2=20password=20bearer=20scheme=20for=20authentication?= =?UTF-8?q?=20=F0=9F=94=92=20chore(auth.py):=20add=20password=20context=20?= =?UTF-8?q?using=20bcrypt=20scheme=20and=20OAuth2=20password=20bearer=20sc?= =?UTF-8?q?heme=20for=20authentication?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/services/settings/auth.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/backend/langflow/services/settings/auth.py b/src/backend/langflow/services/settings/auth.py index d144b4347..a00ebf31e 100644 --- a/src/backend/langflow/services/settings/auth.py +++ b/src/backend/langflow/services/settings/auth.py @@ -2,6 +2,8 @@ from typing import Optional import secrets from pydantic import BaseSettings +from passlib.context import CryptContext +from fastapi.security import OAuth2PasswordBearer class AuthSettings(BaseSettings): @@ -23,6 +25,9 @@ class AuthSettings(BaseSettings): FIRST_SUPERUSER: str = "langflow" FIRST_SUPERUSER_PASSWORD: str = "langflow" + pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") + class Config: validate_assignment = True extra = "ignore" From 181620e538cc175b80507f0c06248601722c8329 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 21 Aug 2023 16:37:56 -0300 Subject: [PATCH 06/38] =?UTF-8?q?=F0=9F=94=A7=20fix(manager.py):=20import?= =?UTF-8?q?=20List=20from=20typing=20module=20to=20fix=20type=20hinting=20?= =?UTF-8?q?error=20=E2=9C=A8=20feat(manager.py):=20add=20support=20for=20r?= =?UTF-8?q?egistering=20factories=20with=20dependencies=20to=20handle=20se?= =?UTF-8?q?rvice=20creation=20=F0=9F=94=A7=20fix(manager.py):=20fix=20serv?= =?UTF-8?q?ice=20creation=20logic=20to=20handle=20dependencies=20and=20cre?= =?UTF-8?q?ate=20services=20in=20the=20correct=20order=20=E2=9C=A8=20feat(?= =?UTF-8?q?manager.py):=20add=20support=20for=20initializing=20session=20m?= =?UTF-8?q?anager=20with=20dependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/services/manager.py | 86 +++++++++++++++++------- 1 file changed, 63 insertions(+), 23 deletions(-) diff --git a/src/backend/langflow/services/manager.py b/src/backend/langflow/services/manager.py index f05102d0e..f40ae1f25 100644 --- a/src/backend/langflow/services/manager.py +++ b/src/backend/langflow/services/manager.py @@ -1,5 +1,5 @@ from langflow.services.schema import ServiceType -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List if TYPE_CHECKING: from langflow.services.factory import ServiceFactory @@ -13,13 +13,19 @@ class ServiceManager: def __init__(self): self.services = {} self.factories = {} + self.dependencies = {} - def register_factory(self, service_factory: "ServiceFactory"): + def register_factory( + self, service_factory: "ServiceFactory", dependencies: List[ServiceType] = None + ): """ - Registers a new factory. + Registers a new factory with dependencies. """ - if service_factory.service_class.name not in self.factories: - self.factories[service_factory.service_class.name] = service_factory + if dependencies is None: + dependencies = [] + service_name = service_factory.service_class.name + self.factories[service_name] = service_factory + self.dependencies[service_name] = dependencies def get(self, service_name: ServiceType): """ @@ -32,17 +38,25 @@ class ServiceManager: def _create_service(self, service_name: ServiceType): """ - Create a new service given its name. + Create a new service given its name, handling dependencies. """ self._validate_service_creation(service_name) - if service_name == ServiceType.SETTINGS_MANAGER: - self.services[service_name] = self.factories[service_name].create() - else: - settings_service = self.get(ServiceType.SETTINGS_MANAGER) - self.services[service_name] = self.factories[service_name].create( - settings_service - ) + # Create dependencies first + for dependency in self.dependencies.get(service_name, []): + if dependency not in self.services: + self._create_service(dependency) + + # Collect the dependent services + dependent_services = { + dep.value: self.services[dep] + for dep in self.dependencies.get(service_name, []) + } + + # Create the actual service + self.services[service_name] = self.factories[service_name].create( + **dependent_services + ) def _validate_service_creation(self, service_name: ServiceType): """ @@ -53,14 +67,6 @@ class ServiceManager: f"No factory registered for the service class '{service_name.name}'" ) - if ( - ServiceType.SETTINGS_MANAGER not in self.factories - and service_name != ServiceType.SETTINGS_MANAGER - ): - raise ValueError( - f"Cannot create service '{service_name.name}' before the settings service" - ) - def update(self, service_name: ServiceType): """ Update a service by its name. @@ -81,11 +87,26 @@ def initialize_services(): from langflow.services.cache import factory as cache_factory from langflow.services.chat import factory as chat_factory from langflow.services.settings import factory as settings_factory + from langflow.services.session import factory as session_manager_factory service_manager.register_factory(settings_factory.SettingsManagerFactory()) - service_manager.register_factory(database_factory.DatabaseManagerFactory()) - service_manager.register_factory(cache_factory.CacheManagerFactory()) + service_manager.register_factory( + database_factory.DatabaseManagerFactory(), + dependencies=[ServiceType.SETTINGS_MANAGER], + ) + service_manager.register_factory( + cache_factory.CacheManagerFactory(), dependencies=[ServiceType.SETTINGS_MANAGER] + ) service_manager.register_factory(chat_factory.ChatManagerFactory()) + service_manager.register_factory( + session_manager_factory.SessionManagerFactory(), + dependencies=[ServiceType.CACHE_MANAGER], + ) + + # Test cache connection + service_manager.get(ServiceType.CACHE_MANAGER) + # Test database connection + service_manager.get(ServiceType.DATABASE_MANAGER) def initialize_settings_manager(): @@ -95,3 +116,22 @@ def initialize_settings_manager(): from langflow.services.settings import factory as settings_factory service_manager.register_factory(settings_factory.SettingsManagerFactory()) + + +def initialize_session_manager(): + """ + Initialize the session manager. + """ + from langflow.services.session import factory as session_manager_factory + from langflow.services.cache import factory as cache_factory + + initialize_settings_manager() + + service_manager.register_factory( + cache_factory.CacheManagerFactory(), dependencies=[ServiceType.SETTINGS_MANAGER] + ) + + service_manager.register_factory( + session_manager_factory.SessionManagerFactory(), + dependencies=[ServiceType.CACHE_MANAGER], + ) From fa9ecd003921b7c35d7f2a79e2c2d1f0dce5a07e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 21 Aug 2023 16:55:45 -0300 Subject: [PATCH 07/38] =?UTF-8?q?=F0=9F=9A=80=20feat(models/=5F=5Finit=5F?= =?UTF-8?q?=5F.py):=20add=20User=20and=20Token=20models=20to=20=5F=5Fall?= =?UTF-8?q?=5F=5F=20list=20for=20better=20module=20importability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/services/database/models/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/backend/langflow/services/database/models/__init__.py b/src/backend/langflow/services/database/models/__init__.py index da47bc5fe..28d2b4af8 100644 --- a/src/backend/langflow/services/database/models/__init__.py +++ b/src/backend/langflow/services/database/models/__init__.py @@ -1,4 +1,5 @@ from .flow import Flow +from .user import User +from .token import Token - -__all__ = ["Flow"] +__all__ = ["Flow", "User", "Token"] From 712d57be53964fba8c3762e5920a7801d8c251b7 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 21 Aug 2023 16:56:07 -0300 Subject: [PATCH 08/38] =?UTF-8?q?=F0=9F=94=84=20refactor(factory.py):=20re?= =?UTF-8?q?name=20settings=5Fservice=20parameter=20to=20settings=5Fmanager?= =?UTF-8?q?=20for=20better=20clarity=20and=20consistency=20=F0=9F=90=9B=20?= =?UTF-8?q?fix(factory.py):=20update=20variable=20name=20in=20if=20conditi?= =?UTF-8?q?on=20from=20settings=5Fservice.settings.DATABASE=5FURL=20to=20s?= =?UTF-8?q?ettings=5Fmanager.settings.DATABASE=5FURL=20to=20fix=20incorrec?= =?UTF-8?q?t=20variable=20reference?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/services/database/factory.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/langflow/services/database/factory.py b/src/backend/langflow/services/database/factory.py index fecf24543..25427b7b9 100644 --- a/src/backend/langflow/services/database/factory.py +++ b/src/backend/langflow/services/database/factory.py @@ -10,8 +10,8 @@ class DatabaseManagerFactory(ServiceFactory): def __init__(self): super().__init__(DatabaseManager) - def create(self, settings_service: "SettingsManager"): + def create(self, settings_manager: "SettingsManager"): # Here you would have logic to create and configure a DatabaseManager - if not settings_service.settings.DATABASE_URL: + if not settings_manager.settings.DATABASE_URL: raise ValueError("No database URL provided") - return DatabaseManager(settings_service.settings.DATABASE_URL) + return DatabaseManager(settings_manager.settings.DATABASE_URL) From 5d9e631ddf81adb6f0030b9e27e8e99e5418b462 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 21 Aug 2023 16:56:20 -0300 Subject: [PATCH 09/38] =?UTF-8?q?=F0=9F=94=A8=20refactor(factory.py):=20re?= =?UTF-8?q?move=20unused=20parameter=20'settings=5Fservice'=20from=20creat?= =?UTF-8?q?e=20method=20in=20CacheManagerFactory=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/services/cache/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/langflow/services/cache/factory.py b/src/backend/langflow/services/cache/factory.py index 77f8d58d1..f180f67c0 100644 --- a/src/backend/langflow/services/cache/factory.py +++ b/src/backend/langflow/services/cache/factory.py @@ -6,6 +6,6 @@ class CacheManagerFactory(ServiceFactory): def __init__(self): super().__init__(CacheManager) - def create(self, settings_service): + def create(self): # Here you would have logic to create and configure a CacheManager return CacheManager() From 031ab7e4b7fae2f21da00c5fd985d55ad02e16b1 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 21 Aug 2023 16:58:09 -0300 Subject: [PATCH 10/38] =?UTF-8?q?=F0=9F=93=A6=20chore(factory.py):=20add?= =?UTF-8?q?=20AuthManagerFactory=20class=20to=20handle=20creation=20of=20A?= =?UTF-8?q?uthManager=20service=20=F0=9F=90=9B=20fix(service.py):=20add=20?= =?UTF-8?q?type=20hints=20and=20import=20for=20SettingsManager=20in=20Auth?= =?UTF-8?q?Manager=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/services/auth/factory.py | 12 ++++++++++++ src/backend/langflow/services/auth/service.py | 6 +++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/services/auth/factory.py b/src/backend/langflow/services/auth/factory.py index e69de29bb..4914ce645 100644 --- a/src/backend/langflow/services/auth/factory.py +++ b/src/backend/langflow/services/auth/factory.py @@ -0,0 +1,12 @@ +from langflow.services.factory import ServiceFactory +from langflow.services.auth.service import AuthManager + + +class AuthManagerFactory(ServiceFactory): + name = "auth_manager" + + def __init__(self): + super().__init__(AuthManager) + + def create(self, settings_manager): + return AuthManager(settings_manager) diff --git a/src/backend/langflow/services/auth/service.py b/src/backend/langflow/services/auth/service.py index c5f380298..57c586c94 100644 --- a/src/backend/langflow/services/auth/service.py +++ b/src/backend/langflow/services/auth/service.py @@ -1,10 +1,14 @@ from langflow.services.base import Service +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from langflow.services.settings.manager import SettingsManager class AuthManager(Service): name = "auth_manager" - def __init__(self, settings_manager): + def __init__(self, settings_manager: "SettingsManager"): self.settings_manager = settings_manager # We need to define a function that can be passed to the Depends() function. From 0a235ef8d553279e3ad731d3f88a3839b6837fec Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 21 Aug 2023 16:58:22 -0300 Subject: [PATCH 11/38] =?UTF-8?q?=F0=9F=94=80=20refactor(manager.py):=20re?= =?UTF-8?q?factor=20service=20initialization=20in=20manager.py=20for=20bet?= =?UTF-8?q?ter=20organization=20and=20readability=20=F0=9F=94=80=20refacto?= =?UTF-8?q?r(schema.py):=20add=20AUTH=5FMANAGER=20service=20type=20to=20th?= =?UTF-8?q?e=20ServiceType=20enum=20for=20better=20organization=20and=20co?= =?UTF-8?q?nsistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/services/manager.py | 13 +++++-------- src/backend/langflow/services/schema.py | 1 + 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/backend/langflow/services/manager.py b/src/backend/langflow/services/manager.py index f40ae1f25..1592e7612 100644 --- a/src/backend/langflow/services/manager.py +++ b/src/backend/langflow/services/manager.py @@ -87,21 +87,18 @@ def initialize_services(): from langflow.services.cache import factory as cache_factory from langflow.services.chat import factory as chat_factory from langflow.services.settings import factory as settings_factory - from langflow.services.session import factory as session_manager_factory + from langflow.services.auth import factory as auth_factory service_manager.register_factory(settings_factory.SettingsManagerFactory()) + service_manager.register_factory( + auth_factory.AuthManagerFactory(), dependencies=[ServiceType.SETTINGS_MANAGER] + ) service_manager.register_factory( database_factory.DatabaseManagerFactory(), dependencies=[ServiceType.SETTINGS_MANAGER], ) - service_manager.register_factory( - cache_factory.CacheManagerFactory(), dependencies=[ServiceType.SETTINGS_MANAGER] - ) + service_manager.register_factory(cache_factory.CacheManagerFactory()) service_manager.register_factory(chat_factory.ChatManagerFactory()) - service_manager.register_factory( - session_manager_factory.SessionManagerFactory(), - dependencies=[ServiceType.CACHE_MANAGER], - ) # Test cache connection service_manager.get(ServiceType.CACHE_MANAGER) diff --git a/src/backend/langflow/services/schema.py b/src/backend/langflow/services/schema.py index 695763afc..6291a0d0b 100644 --- a/src/backend/langflow/services/schema.py +++ b/src/backend/langflow/services/schema.py @@ -7,6 +7,7 @@ class ServiceType(str, Enum): registered with the service manager. """ + AUTH_MANAGER = "auth_manager" CACHE_MANAGER = "cache_manager" SETTINGS_MANAGER = "settings_manager" DATABASE_MANAGER = "database_manager" From 48965a8ba8b6622792503e3a67e8029866715de2 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 21 Aug 2023 19:14:58 -0300 Subject: [PATCH 12/38] =?UTF-8?q?=F0=9F=90=9B=20fix(endpoints.py):=20add?= =?UTF-8?q?=20check=20for=20empty=20custom=5Fcomponent=5Fdict=20to=20preve?= =?UTF-8?q?nt=20errors=20=F0=9F=90=9B=20fix(login.py):=20update=20import?= =?UTF-8?q?=20statement=20for=20auth=20utils=20module=20=F0=9F=90=9B=20fix?= =?UTF-8?q?(users.py):=20update=20import=20statement=20for=20auth=20utils?= =?UTF-8?q?=20module=20=F0=9F=90=9B=20fix(factory.py):=20remove=20unnecess?= =?UTF-8?q?ary=20argument=20from=20create=20method=20=F0=9F=90=9B=20fix(te?= =?UTF-8?q?st=5Fcli.py):=20convert=20temp=5Fdir=20to=20string=20before=20c?= =?UTF-8?q?hecking=20if=20it's=20in=20COMPONENTS=5FPATH?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/api/v1/endpoints.py | 2 ++ src/backend/langflow/routers/login.py | 2 +- src/backend/langflow/routers/users.py | 2 +- src/backend/langflow/services/chat/factory.py | 2 +- tests/test_cli.py | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/backend/langflow/api/v1/endpoints.py b/src/backend/langflow/api/v1/endpoints.py index 928d6609c..221091b7f 100644 --- a/src/backend/langflow/api/v1/endpoints.py +++ b/src/backend/langflow/api/v1/endpoints.py @@ -59,6 +59,8 @@ def get_all(): logger.info(f"Loading {len(custom_component_dicts)} category(ies)") for custom_component_dict in custom_component_dicts: # custom_component_dict is a dict of dicts + if not custom_component_dict: + continue category = list(custom_component_dict.keys())[0] logger.info( f"Loading {len(custom_component_dict[category])} component(s) from category {category}" diff --git a/src/backend/langflow/routers/login.py b/src/backend/langflow/routers/login.py index 6a103d646..b559b0a23 100644 --- a/src/backend/langflow/routers/login.py +++ b/src/backend/langflow/routers/login.py @@ -4,7 +4,7 @@ from fastapi.security import OAuth2PasswordRequestForm from langflow.services.utils import get_session from langflow.services.database.models import Token -from langflow.auth.auth import ( +from langflow.services.auth.utils import ( authenticate_user, create_user_tokens, create_refresh_token, diff --git a/src/backend/langflow/routers/users.py b/src/backend/langflow/routers/users.py index 135437b74..2a944cb64 100644 --- a/src/backend/langflow/routers/users.py +++ b/src/backend/langflow/routers/users.py @@ -14,7 +14,7 @@ from sqlmodel import Session, select from fastapi import APIRouter, Depends, HTTPException from langflow.services.utils import get_session -from langflow.auth.auth import get_current_active_user, get_password_hash +from langflow.services.auth.utils import get_current_active_user, get_password_hash from langflow.services.database.models.user import ( update_user, ) diff --git a/src/backend/langflow/services/chat/factory.py b/src/backend/langflow/services/chat/factory.py index 03597ed11..ca844893a 100644 --- a/src/backend/langflow/services/chat/factory.py +++ b/src/backend/langflow/services/chat/factory.py @@ -6,6 +6,6 @@ class ChatManagerFactory(ServiceFactory): def __init__(self): super().__init__(ChatManager) - def create(self, settings_service): + def create(self): # Here you would have logic to create and configure a ChatManager return ChatManager() diff --git a/tests/test_cli.py b/tests/test_cli.py index 408500d7a..c990ef9e8 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -27,4 +27,4 @@ def test_components_path(runner, client, default_settings): ) assert result.exit_code == 0, result.stdout settings_manager = utils.get_settings_manager() - assert temp_dir in settings_manager.settings.COMPONENTS_PATH + assert str(temp_dir) in settings_manager.settings.COMPONENTS_PATH From 130dc7ead6b084ac64a887633f4823e8770c72fd Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 23 Aug 2023 21:12:11 -0300 Subject: [PATCH 13/38] =?UTF-8?q?=F0=9F=94=A7=20fix(schemas.py):=20import?= =?UTF-8?q?=20ApiKeyRead=20from=20api=5Fkey=20module=20to=20fix=20missing?= =?UTF-8?q?=20import=20error=20=F0=9F=94=A7=20fix(models/=5F=5Finit=5F=5F.?= =?UTF-8?q?py):=20add=20ApiKey=20to=20=5F=5Fall=5F=5F=20list=20to=20fix=20?= =?UTF-8?q?missing=20import=20error=20=E2=9C=A8=20feat(models/api=5Fkey.py?= =?UTF-8?q?):=20add=20ApiKey=20model=20and=20its=20related=20classes=20to?= =?UTF-8?q?=20support=20API=20key=20functionality?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/api/v1/schemas.py | 7 +++++ .../services/database/models/__init__.py | 3 +- .../services/database/models/api_key.py | 28 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 src/backend/langflow/services/database/models/api_key.py diff --git a/src/backend/langflow/api/v1/schemas.py b/src/backend/langflow/api/v1/schemas.py index 65bf64dca..d188e5c7a 100644 --- a/src/backend/langflow/api/v1/schemas.py +++ b/src/backend/langflow/api/v1/schemas.py @@ -1,6 +1,7 @@ from enum import Enum from pathlib import Path from typing import Any, Dict, List, Optional, Union +from langflow.services.database.models.api_key import ApiKeyRead from langflow.services.database.models.flow import FlowCreate, FlowRead from pydantic import BaseModel, Field, validator import json @@ -134,3 +135,9 @@ class ComponentListCreate(BaseModel): class ComponentListRead(BaseModel): flows: List[FlowRead] + + +class ApiKeyResponse(BaseModel): + total_count: int + user_id: str + api_keys: List[ApiKeyRead] diff --git a/src/backend/langflow/services/database/models/__init__.py b/src/backend/langflow/services/database/models/__init__.py index 28d2b4af8..01e81e277 100644 --- a/src/backend/langflow/services/database/models/__init__.py +++ b/src/backend/langflow/services/database/models/__init__.py @@ -1,5 +1,6 @@ from .flow import Flow from .user import User from .token import Token +from .api_key import ApiKey -__all__ = ["Flow", "User", "Token"] +__all__ = ["Flow", "User", "Token", "ApiKey"] diff --git a/src/backend/langflow/services/database/models/api_key.py b/src/backend/langflow/services/database/models/api_key.py new file mode 100644 index 000000000..3549c10c4 --- /dev/null +++ b/src/backend/langflow/services/database/models/api_key.py @@ -0,0 +1,28 @@ +from sqlmodel import Field +from uuid import UUID, uuid4 +from typing import Optional +from datetime import datetime +from langflow.services.database.models.base import SQLModelSerializable, SQLModel + + +class ApiKeyBase(SQLModelSerializable): + api_key: str = Field(index=True, unique=True) + name: str = Field() + create_at: datetime = Field(default_factory=datetime.utcnow) + last_used_at: Optional[datetime] = Field() + + +class ApiKey(ApiKeyBase, table=True): + id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True) + + +class ApiKeyCreate(SQLModel): + name: str = Field() + + +class ApiKeyRead(SQLModel): + id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True) + api_key: str = Field(index=True, unique=True) + name: str = Field() + create_at: datetime = Field(default_factory=datetime.utcnow) + last_used_at: Optional[datetime] = Field() From 63ca40850631a8a3b2c5f2234d70101c6095817e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 23 Aug 2023 21:43:14 -0300 Subject: [PATCH 14/38] =?UTF-8?q?=F0=9F=94=A7=20chore(alembic):=20add=20Ap?= =?UTF-8?q?iKey=20table=20and=20remove=20FlowStyle=20and=20Component=20tab?= =?UTF-8?q?les?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🔄 refactor(alembic): downgrade migration to recreate FlowStyle and Component tables and remove User and ApiKey tables --- .../versions/5512e39b4012_add_apikey_table.py | 84 +++++++++++++++++++ .../services/database/models/api_key.py | 18 ++-- 2 files changed, 91 insertions(+), 11 deletions(-) create mode 100644 src/backend/langflow/alembic/versions/5512e39b4012_add_apikey_table.py diff --git a/src/backend/langflow/alembic/versions/5512e39b4012_add_apikey_table.py b/src/backend/langflow/alembic/versions/5512e39b4012_add_apikey_table.py new file mode 100644 index 000000000..8c8ae75fd --- /dev/null +++ b/src/backend/langflow/alembic/versions/5512e39b4012_add_apikey_table.py @@ -0,0 +1,84 @@ +"""Add ApiKey table + +Revision ID: 5512e39b4012 +Revises: 0a534bdfd84b +Create Date: 2023-08-23 21:05:51.042203 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel + + +# revision identifiers, used by Alembic. +revision: str = '5512e39b4012' +down_revision: Union[str, None] = '0a534bdfd84b' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('apikey', + sa.Column('api_key', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('create_at', sa.DateTime(), nullable=False), + sa.Column('last_used_at', sa.DateTime(), nullable=True), + sa.Column('id', sqlmodel.sql.sqltypes.GUID(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id') + ) + op.create_index(op.f('ix_apikey_api_key'), 'apikey', ['api_key'], unique=True) + op.create_table('user', + sa.Column('id', sqlmodel.sql.sqltypes.GUID(), nullable=False), + sa.Column('username', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('password', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('is_superuser', sa.Boolean(), nullable=False), + sa.Column('create_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('last_login_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id') + ) + op.create_index(op.f('ix_user_username'), 'user', ['username'], unique=True) + op.drop_table('flowstyle') + op.drop_index('ix_component_frontend_node_id', table_name='component') + op.drop_index('ix_component_name', table_name='component') + op.drop_table('component') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('component', + sa.Column('id', sa.CHAR(length=32), nullable=False), + sa.Column('frontend_node_id', sa.CHAR(length=32), nullable=False), + sa.Column('name', sa.VARCHAR(), nullable=False), + sa.Column('description', sa.VARCHAR(), nullable=True), + sa.Column('python_code', sa.VARCHAR(), nullable=True), + sa.Column('return_type', sa.VARCHAR(), nullable=True), + sa.Column('is_disabled', sa.BOOLEAN(), nullable=False), + sa.Column('is_read_only', sa.BOOLEAN(), nullable=False), + sa.Column('create_at', sa.DATETIME(), nullable=False), + sa.Column('update_at', sa.DATETIME(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_component_name', 'component', ['name'], unique=False) + op.create_index('ix_component_frontend_node_id', 'component', ['frontend_node_id'], unique=False) + op.create_table('flowstyle', + sa.Column('color', sa.VARCHAR(), nullable=False), + sa.Column('emoji', sa.VARCHAR(), nullable=False), + sa.Column('flow_id', sa.CHAR(length=32), nullable=True), + sa.Column('id', sa.CHAR(length=32), nullable=False), + sa.ForeignKeyConstraint(['flow_id'], ['flow.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id') + ) + op.drop_index(op.f('ix_user_username'), table_name='user') + op.drop_table('user') + op.drop_index(op.f('ix_apikey_api_key'), table_name='apikey') + op.drop_table('apikey') + # ### end Alembic commands ### diff --git a/src/backend/langflow/services/database/models/api_key.py b/src/backend/langflow/services/database/models/api_key.py index 3549c10c4..784b25229 100644 --- a/src/backend/langflow/services/database/models/api_key.py +++ b/src/backend/langflow/services/database/models/api_key.py @@ -2,27 +2,23 @@ from sqlmodel import Field from uuid import UUID, uuid4 from typing import Optional from datetime import datetime -from langflow.services.database.models.base import SQLModelSerializable, SQLModel +from langflow.services.database.models.base import SQLModelSerializable class ApiKeyBase(SQLModelSerializable): api_key: str = Field(index=True, unique=True) - name: str = Field() + name: Optional[str] = Field(index=True) create_at: datetime = Field(default_factory=datetime.utcnow) - last_used_at: Optional[datetime] = Field() + last_used_at: Optional[datetime] = Field(default=None) class ApiKey(ApiKeyBase, table=True): id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True) -class ApiKeyCreate(SQLModel): - name: str = Field() +class ApiKeyCreate(ApiKeyBase): + pass -class ApiKeyRead(SQLModel): - id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True) - api_key: str = Field(index=True, unique=True) - name: str = Field() - create_at: datetime = Field(default_factory=datetime.utcnow) - last_used_at: Optional[datetime] = Field() +class ApiKeyRead(ApiKeyBase): + id: UUID From fd6ef1815b964839d07e118aef77883c19ef44eb Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 23 Aug 2023 21:43:23 -0300 Subject: [PATCH 15/38] format --- docker-compose.debug.yml | 66 ++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/docker-compose.debug.yml b/docker-compose.debug.yml index 6a9802e38..c73ff2a60 100644 --- a/docker-compose.debug.yml +++ b/docker-compose.debug.yml @@ -1,33 +1,33 @@ -version: "3.4" - -services: - backend: - volumes: - - ./:/app - build: - context: ./ - dockerfile: ./dev.Dockerfile - command: - [ - "sh", - "-c", - "pip install debugpy -t /tmp && python /tmp/debugpy --wait-for-client --listen 0.0.0.0:5678 -m uvicorn --factory src.backend.langflow.main:create_app --host 0.0.0.0 --port 7860 --reload", - ] - ports: - - 7860:7860 - - 5678:5678 - restart: on-failure - - frontend: - build: - context: ./src/frontend - dockerfile: ./dev.Dockerfile - args: - - BACKEND_URL=http://backend:7860 - ports: - - "3000:3000" - volumes: - - ./src/frontend/public:/home/node/app/public - - ./src/frontend/src:/home/node/app/src - - ./src/frontend/package.json:/home/node/app/package.json - restart: on-failure +version: "3.4" + +services: + backend: + volumes: + - ./:/app + build: + context: ./ + dockerfile: ./dev.Dockerfile + command: + [ + "sh", + "-c", + "pip install debugpy -t /tmp && python /tmp/debugpy --wait-for-client --listen 0.0.0.0:5678 -m uvicorn --factory src.backend.langflow.main:create_app --host 0.0.0.0 --port 7860 --reload", + ] + ports: + - 7860:7860 + - 5678:5678 + restart: on-failure + + frontend: + build: + context: ./src/frontend + dockerfile: ./dev.Dockerfile + args: + - BACKEND_URL=http://backend:7860 + ports: + - "3000:3000" + volumes: + - ./src/frontend/public:/home/node/app/public + - ./src/frontend/src:/home/node/app/src + - ./src/frontend/package.json:/home/node/app/package.json + restart: on-failure From 1a51cc0848211b8133da1994aaff90b740a40f9f Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 24 Aug 2023 17:32:26 -0300 Subject: [PATCH 16/38] =?UTF-8?q?=F0=9F=93=A6=20feat(api=5Fkey):=20add=20A?= =?UTF-8?q?piKey=20model=20and=20related=20classes=20for=20database=20oper?= =?UTF-8?q?ations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📦 feat(component): add Component model and related classes for database operations 📦 feat(flow): add Flow model and related classes for database operations 📦 feat(token): add Token model for authentication 📦 feat(user): add User model and related classes for database operations 📦 feat(user): add utility functions for user operations --- .../database/models/api_key/__init__.py | 3 ++ .../database/models/{ => api_key}/api_key.py | 0 .../database/models/component/__init__.py | 3 ++ .../models/{ => component}/component.py | 0 .../services/database/models/flow/__init__.py | 3 ++ .../database/models/{ => flow}/flow.py | 3 -- .../database/models/token/__init__.py | 5 ++ .../database/models/{ => token}/token.py | 0 .../services/database/models/user/__init__.py | 8 +++ .../services/database/models/user/user.py | 40 ++++++++++++++ .../models/{user.py => user/utils.py} | 53 ++----------------- 11 files changed, 67 insertions(+), 51 deletions(-) create mode 100644 src/backend/langflow/services/database/models/api_key/__init__.py rename src/backend/langflow/services/database/models/{ => api_key}/api_key.py (100%) create mode 100644 src/backend/langflow/services/database/models/component/__init__.py rename src/backend/langflow/services/database/models/{ => component}/component.py (100%) create mode 100644 src/backend/langflow/services/database/models/flow/__init__.py rename src/backend/langflow/services/database/models/{ => flow}/flow.py (94%) create mode 100644 src/backend/langflow/services/database/models/token/__init__.py rename src/backend/langflow/services/database/models/{ => token}/token.py (100%) create mode 100644 src/backend/langflow/services/database/models/user/__init__.py create mode 100644 src/backend/langflow/services/database/models/user/user.py rename src/backend/langflow/services/database/models/{user.py => user/utils.py} (51%) diff --git a/src/backend/langflow/services/database/models/api_key/__init__.py b/src/backend/langflow/services/database/models/api_key/__init__.py new file mode 100644 index 000000000..c97425ee8 --- /dev/null +++ b/src/backend/langflow/services/database/models/api_key/__init__.py @@ -0,0 +1,3 @@ +from .api_key import ApiKey, ApiKeyCreate, ApiKeyRead + +__all__ = ["ApiKey", "ApiKeyCreate", "ApiKeyRead"] diff --git a/src/backend/langflow/services/database/models/api_key.py b/src/backend/langflow/services/database/models/api_key/api_key.py similarity index 100% rename from src/backend/langflow/services/database/models/api_key.py rename to src/backend/langflow/services/database/models/api_key/api_key.py diff --git a/src/backend/langflow/services/database/models/component/__init__.py b/src/backend/langflow/services/database/models/component/__init__.py new file mode 100644 index 000000000..c787c3e04 --- /dev/null +++ b/src/backend/langflow/services/database/models/component/__init__.py @@ -0,0 +1,3 @@ +from .component import Component, ComponentModel + +__all__ = ["Component", "ComponentModel"] diff --git a/src/backend/langflow/services/database/models/component.py b/src/backend/langflow/services/database/models/component/component.py similarity index 100% rename from src/backend/langflow/services/database/models/component.py rename to src/backend/langflow/services/database/models/component/component.py diff --git a/src/backend/langflow/services/database/models/flow/__init__.py b/src/backend/langflow/services/database/models/flow/__init__.py new file mode 100644 index 000000000..7c7cc0172 --- /dev/null +++ b/src/backend/langflow/services/database/models/flow/__init__.py @@ -0,0 +1,3 @@ +from .flow import Flow, FlowCreate, FlowRead, FlowUpdate + +__all__ = ["Flow", "FlowCreate", "FlowRead", "FlowUpdate"] diff --git a/src/backend/langflow/services/database/models/flow.py b/src/backend/langflow/services/database/models/flow/flow.py similarity index 94% rename from src/backend/langflow/services/database/models/flow.py rename to src/backend/langflow/services/database/models/flow/flow.py index 2bc83f9dc..a05de5791 100644 --- a/src/backend/langflow/services/database/models/flow.py +++ b/src/backend/langflow/services/database/models/flow/flow.py @@ -6,8 +6,6 @@ from sqlmodel import Field, JSON, Column from uuid import UUID, uuid4 from typing import Dict, Optional -# if TYPE_CHECKING: - class FlowBase(SQLModelSerializable): name: str = Field(index=True) @@ -16,7 +14,6 @@ class FlowBase(SQLModelSerializable): @validator("data") def validate_json(v): - # dict_keys(['description', 'name', 'id', 'data']) if not v: return v if not isinstance(v, dict): diff --git a/src/backend/langflow/services/database/models/token/__init__.py b/src/backend/langflow/services/database/models/token/__init__.py new file mode 100644 index 000000000..9b9fa397d --- /dev/null +++ b/src/backend/langflow/services/database/models/token/__init__.py @@ -0,0 +1,5 @@ +from .token import Token + +__all__ = [ + "Token", +] diff --git a/src/backend/langflow/services/database/models/token.py b/src/backend/langflow/services/database/models/token/token.py similarity index 100% rename from src/backend/langflow/services/database/models/token.py rename to src/backend/langflow/services/database/models/token/token.py diff --git a/src/backend/langflow/services/database/models/user/__init__.py b/src/backend/langflow/services/database/models/user/__init__.py new file mode 100644 index 000000000..da9170eb7 --- /dev/null +++ b/src/backend/langflow/services/database/models/user/__init__.py @@ -0,0 +1,8 @@ +from .user import User, UserCreate, UserRead, UserUpdate + +__all__ = [ + "User", + "UserCreate", + "UserRead", + "UserUpdate", +] diff --git a/src/backend/langflow/services/database/models/user/user.py b/src/backend/langflow/services/database/models/user/user.py new file mode 100644 index 000000000..3a4308b42 --- /dev/null +++ b/src/backend/langflow/services/database/models/user/user.py @@ -0,0 +1,40 @@ +from langflow.services.database.models.base import SQLModel, SQLModelSerializable +from sqlmodel import Field + + +from datetime import datetime +from typing import Optional +from uuid import UUID, uuid4 + + +class User(SQLModelSerializable, table=True): + id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True) + username: str = Field(index=True, unique=True) + password: str = Field() + is_active: bool = Field(default=False) + is_superuser: bool = Field(default=False) + create_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: datetime = Field(default_factory=datetime.utcnow) + last_login_at: Optional[datetime] = Field() + + +class UserCreate(SQLModel): + username: str = Field() + password: str = Field() + + +class UserRead(SQLModel): + id: UUID = Field(default_factory=uuid4) + username: str = Field() + is_active: bool = Field() + is_superuser: bool = Field() + create_at: datetime = Field() + updated_at: datetime = Field() + last_login_at: Optional[datetime] = Field() + + +class UserUpdate(SQLModel): + username: Optional[str] = Field() + is_active: Optional[bool] = Field() + is_superuser: Optional[bool] = Field() + last_login_at: Optional[datetime] = Field() diff --git a/src/backend/langflow/services/database/models/user.py b/src/backend/langflow/services/database/models/user/utils.py similarity index 51% rename from src/backend/langflow/services/database/models/user.py rename to src/backend/langflow/services/database/models/user/utils.py index f9a3c80f8..3b600bd9a 100644 --- a/src/backend/langflow/services/database/models/user.py +++ b/src/backend/langflow/services/database/models/user/utils.py @@ -1,53 +1,10 @@ +from datetime import datetime, timezone +from uuid import UUID from fastapi import Depends, HTTPException -from langflow.services.database.models.base import SQLModel, SQLModelSerializable +from langflow.services.database.models.user.user import User, UserUpdate from langflow.services.utils import get_session -from pydantic import BaseModel from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session -from sqlmodel import Field - - -from datetime import datetime, timezone -from typing import List, Optional -from uuid import UUID, uuid4 - - -class User(SQLModelSerializable, table=True): - id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True) - username: str = Field(index=True, unique=True) - password: str = Field() - is_active: bool = Field(default=False) - is_superuser: bool = Field(default=False) - create_at: datetime = Field(default_factory=datetime.utcnow) - updated_at: datetime = Field(default_factory=datetime.utcnow) - last_login_at: Optional[datetime] = Field() - - -class UserAddModel(SQLModel): - username: str = Field() - password: str = Field() - - -class UserListModel(SQLModel): - id: UUID = Field(default_factory=uuid4) - username: str = Field() - is_active: bool = Field() - is_superuser: bool = Field() - create_at: datetime = Field() - updated_at: datetime = Field() - last_login_at: Optional[datetime] = Field() - - -class UserPatchModel(SQLModel): - username: Optional[str] = Field() - is_active: Optional[bool] = Field() - is_superuser: Optional[bool] = Field() - last_login_at: Optional[datetime] = Field() - - -class UsersResponse(BaseModel): - total_count: int - users: List[UserListModel] def get_user_by_username(db: Session, username: str) -> User: @@ -61,7 +18,7 @@ def get_user_by_id(db: Session, id: UUID) -> User: def update_user( - user_id: UUID, user: UserPatchModel, db: Session = Depends(get_session) + user_id: UUID, user: UserUpdate, db: Session = Depends(get_session) ) -> User: user_db = get_user_by_username(db, user.username) # type: ignore if user_db and user_db.id != user_id: @@ -90,6 +47,6 @@ def update_user( def update_user_last_login_at(user_id: UUID, db: Session = Depends(get_session)): - user_data = UserPatchModel(last_login_at=datetime.now(timezone.utc)) # type: ignore + user_data = UserUpdate(last_login_at=datetime.now(timezone.utc)) # type: ignore return update_user(user_id, user_data, db) From d9cbf17b1a779f5a05cbf29798edea56e4823aa1 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 24 Aug 2023 17:41:41 -0300 Subject: [PATCH 17/38] =?UTF-8?q?=F0=9F=94=84=20chore(router.py):=20add=20?= =?UTF-8?q?users=5Frouter=20and=20api=5Fkey=5Frouter=20to=20the=20APIRoute?= =?UTF-8?q?r=20to=20include=20the=20new=20routes=20for=20users=20and=20api?= =?UTF-8?q?=20keys=20=F0=9F=94=84=20chore(=5F=5Finit=5F=5F.py):=20add=20us?= =?UTF-8?q?ers=5Frouter=20and=20api=5Fkey=5Frouter=20to=20the=20=5F=5Fall?= =?UTF-8?q?=5F=5F=20list=20to=20include=20the=20new=20routes=20for=20users?= =?UTF-8?q?=20and=20api=20keys=20=F0=9F=86=95=20feat(api=5Fkey.py):=20add?= =?UTF-8?q?=20new=20routes=20for=20retrieving,=20creating,=20and=20deletin?= =?UTF-8?q?g=20API=20keys=20=F0=9F=86=95=20feat(login.py):=20add=20new=20r?= =?UTF-8?q?outes=20for=20user=20login,=20auto=20login,=20and=20token=20ref?= =?UTF-8?q?resh=20=F0=9F=86=95=20feat(schemas.py):=20add=20new=20schemas?= =?UTF-8?q?=20for=20API=20key=20response=20and=20users=20response=20?= =?UTF-8?q?=F0=9F=86=95=20feat(users.py):=20add=20new=20routes=20for=20add?= =?UTF-8?q?ing,=20reading,=20updating,=20and=20deleting=20users=20?= =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20chore(health.py):=20remove=20health=20r?= =?UTF-8?q?outer=20as=20it=20is=20no=20longer=20needed=20=F0=9F=94=84=20ch?= =?UTF-8?q?ore(utils.py):=20update=20import=20statements=20for=20User=20mo?= =?UTF-8?q?del=20and=20update=5Fuser=5Flast=5Flogin=5Fat=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/api/router.py | 4 ++++ src/backend/langflow/api/v1/__init__.py | 4 ++++ .../langflow/{routers => api/v1}/api_key.py | 0 .../langflow/{routers => api/v1}/login.py | 0 src/backend/langflow/api/v1/schemas.py | 6 +++++ .../langflow/{routers => api/v1}/users.py | 22 +++++++++---------- src/backend/langflow/routers/health.py | 8 ------- src/backend/langflow/services/auth/utils.py | 4 ++-- 8 files changed, 27 insertions(+), 21 deletions(-) rename src/backend/langflow/{routers => api/v1}/api_key.py (100%) rename src/backend/langflow/{routers => api/v1}/login.py (100%) rename src/backend/langflow/{routers => api/v1}/users.py (88%) delete mode 100644 src/backend/langflow/routers/health.py diff --git a/src/backend/langflow/api/router.py b/src/backend/langflow/api/router.py index ea1938a75..70cce437d 100644 --- a/src/backend/langflow/api/router.py +++ b/src/backend/langflow/api/router.py @@ -6,6 +6,8 @@ from langflow.api.v1 import ( validate_router, flows_router, component_router, + users_router, + api_key_router, ) router = APIRouter( @@ -16,3 +18,5 @@ router.include_router(endpoints_router) router.include_router(validate_router) router.include_router(component_router) router.include_router(flows_router) +router.include_router(users_router) +router.include_router(api_key_router) diff --git a/src/backend/langflow/api/v1/__init__.py b/src/backend/langflow/api/v1/__init__.py index b6e7b36d8..38f1c9148 100644 --- a/src/backend/langflow/api/v1/__init__.py +++ b/src/backend/langflow/api/v1/__init__.py @@ -3,6 +3,8 @@ from langflow.api.v1.validate import router as validate_router from langflow.api.v1.chat import router as chat_router from langflow.api.v1.flows import router as flows_router from langflow.api.v1.components import router as component_router +from langflow.api.v1.users import router as users_router +from langflow.api.v1.api_key import router as api_key_router __all__ = [ "chat_router", @@ -10,4 +12,6 @@ __all__ = [ "component_router", "validate_router", "flows_router", + "users_router", + "api_key_router", ] diff --git a/src/backend/langflow/routers/api_key.py b/src/backend/langflow/api/v1/api_key.py similarity index 100% rename from src/backend/langflow/routers/api_key.py rename to src/backend/langflow/api/v1/api_key.py diff --git a/src/backend/langflow/routers/login.py b/src/backend/langflow/api/v1/login.py similarity index 100% rename from src/backend/langflow/routers/login.py rename to src/backend/langflow/api/v1/login.py diff --git a/src/backend/langflow/api/v1/schemas.py b/src/backend/langflow/api/v1/schemas.py index d188e5c7a..d788469fa 100644 --- a/src/backend/langflow/api/v1/schemas.py +++ b/src/backend/langflow/api/v1/schemas.py @@ -3,6 +3,7 @@ from pathlib import Path from typing import Any, Dict, List, Optional, Union from langflow.services.database.models.api_key import ApiKeyRead from langflow.services.database.models.flow import FlowCreate, FlowRead +from langflow.services.database.models.user import UserRead from pydantic import BaseModel, Field, validator import json @@ -141,3 +142,8 @@ class ApiKeyResponse(BaseModel): total_count: int user_id: str api_keys: List[ApiKeyRead] + + +class UsersResponse(BaseModel): + total_count: int + users: List[UserRead] diff --git a/src/backend/langflow/routers/users.py b/src/backend/langflow/api/v1/users.py similarity index 88% rename from src/backend/langflow/routers/users.py rename to src/backend/langflow/api/v1/users.py index 2a944cb64..33ddc9763 100644 --- a/src/backend/langflow/routers/users.py +++ b/src/backend/langflow/api/v1/users.py @@ -1,10 +1,10 @@ from uuid import UUID +from langflow.api.v1.schemas import UsersResponse from langflow.services.database.models.user import ( User, - UserAddModel, - UserListModel, - UserPatchModel, - UsersResponse, + UserCreate, + UserRead, + UserUpdate, ) from sqlalchemy import func @@ -15,16 +15,16 @@ from fastapi import APIRouter, Depends, HTTPException from langflow.services.utils import get_session from langflow.services.auth.utils import get_current_active_user, get_password_hash -from langflow.services.database.models.user import ( +from langflow.services.database.models.user.utils import ( update_user, ) router = APIRouter(tags=["Login"]) -@router.post("/user", response_model=UserListModel) +@router.post("/user", response_model=UserRead) def add_user( - user: UserAddModel, + user: UserCreate, db: Session = Depends(get_session), ) -> User: """ @@ -44,7 +44,7 @@ def add_user( return new_user -@router.get("/user", response_model=UserListModel) +@router.get("/user", response_model=UserRead) def read_current_user(current_user: User = Depends(get_current_active_user)) -> User: """ Retrieve the current user's data. @@ -70,14 +70,14 @@ def read_all_users( return UsersResponse( total_count=total_count, # type: ignore - users=[UserListModel(**dict(user.User)) for user in users], + users=[UserRead(**dict(user.User)) for user in users], ) -@router.patch("/user/{user_id}", response_model=UserListModel) +@router.patch("/user/{user_id}", response_model=UserRead) def patch_user( user_id: UUID, - user: UserPatchModel, + user: UserUpdate, _: Session = Depends(get_current_active_user), db: Session = Depends(get_session), ) -> User: diff --git a/src/backend/langflow/routers/health.py b/src/backend/langflow/routers/health.py deleted file mode 100644 index 244ef001d..000000000 --- a/src/backend/langflow/routers/health.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import APIRouter - -router = APIRouter() - - -@router.get("/health") -def get_health(): - return {"status": "OK"} diff --git a/src/backend/langflow/services/auth/utils.py b/src/backend/langflow/services/auth/utils.py index f897151e7..aa4bec669 100644 --- a/src/backend/langflow/services/auth/utils.py +++ b/src/backend/langflow/services/auth/utils.py @@ -4,8 +4,8 @@ from jose import JWTError, jwt from typing import Annotated from uuid import UUID from langflow.services.auth.service import AuthManager -from langflow.services.database.models.user import ( - User, +from langflow.services.database.models.user.user import User +from langflow.services.database.models.user.utils import ( get_user_by_id, get_user_by_username, update_user_last_login_at, From 58121cc6ca7b53a69ff2e45cfd45913a1ca955a5 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 24 Aug 2023 17:42:33 -0300 Subject: [PATCH 18/38] =?UTF-8?q?=F0=9F=94=A5=20refactor(main.py):=20remov?= =?UTF-8?q?e=20unused=20routers=20from=20the=20app=20to=20improve=20code?= =?UTF-8?q?=20cleanliness=20and=20reduce=20unnecessary=20imports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/main.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/backend/langflow/main.py b/src/backend/langflow/main.py index 7045ec99d..a383a2afa 100644 --- a/src/backend/langflow/main.py +++ b/src/backend/langflow/main.py @@ -6,7 +6,7 @@ from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from langflow.api import router -from langflow.routers import api_key, login, users, health + from langflow.interface.utils import setup_llm_caching from langflow.services.database.utils import initialize_database @@ -31,11 +31,6 @@ def create_app(): allow_headers=["*"], ) - app.include_router(login.router) - app.include_router(api_key.router) - app.include_router(users.router) - app.include_router(health.router) - app.include_router(router) app.on_event("startup")(initialize_services) From 41ef2fd2f72cc82d5bd4314704b7cf60b84a1311 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Thu, 24 Aug 2023 17:42:53 -0300 Subject: [PATCH 19/38] =?UTF-8?q?=F0=9F=94=A7=20fix(alembic):=20fix=20inde?= =?UTF-8?q?ntation=20and=20formatting=20issues=20in=20add=5Fapikey=5Ftable?= =?UTF-8?q?=20migration=20script=20=E2=9C=A8=20feat(alembic):=20add=20supp?= =?UTF-8?q?ort=20for=20creating=20apikey=20and=20user=20tables=20in=20the?= =?UTF-8?q?=20database=20=F0=9F=94=A5=20chore(alembic):=20remove=20flowsty?= =?UTF-8?q?le=20and=20component=20tables=20from=20the=20database=20schema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../versions/5512e39b4012_add_apikey_table.py | 126 ++++++++++-------- 1 file changed, 71 insertions(+), 55 deletions(-) diff --git a/src/backend/langflow/alembic/versions/5512e39b4012_add_apikey_table.py b/src/backend/langflow/alembic/versions/5512e39b4012_add_apikey_table.py index 8c8ae75fd..02db82e71 100644 --- a/src/backend/langflow/alembic/versions/5512e39b4012_add_apikey_table.py +++ b/src/backend/langflow/alembic/versions/5512e39b4012_add_apikey_table.py @@ -5,6 +5,8 @@ Revises: 0a534bdfd84b Create Date: 2023-08-23 21:05:51.042203 """ + +import contextlib from typing import Sequence, Union from alembic import op @@ -13,72 +15,86 @@ import sqlmodel # revision identifiers, used by Alembic. -revision: str = '5512e39b4012' -down_revision: Union[str, None] = '0a534bdfd84b' +revision: str = "5512e39b4012" +down_revision: Union[str, None] = "0a534bdfd84b" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.create_table('apikey', - sa.Column('api_key', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('create_at', sa.DateTime(), nullable=False), - sa.Column('last_used_at', sa.DateTime(), nullable=True), - sa.Column('id', sqlmodel.sql.sqltypes.GUID(), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('id') - ) - op.create_index(op.f('ix_apikey_api_key'), 'apikey', ['api_key'], unique=True) - op.create_table('user', - sa.Column('id', sqlmodel.sql.sqltypes.GUID(), nullable=False), - sa.Column('username', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('password', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.Column('is_superuser', sa.Boolean(), nullable=False), - sa.Column('create_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.Column('last_login_at', sa.DateTime(), nullable=True), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('id') - ) - op.create_index(op.f('ix_user_username'), 'user', ['username'], unique=True) - op.drop_table('flowstyle') - op.drop_index('ix_component_frontend_node_id', table_name='component') - op.drop_index('ix_component_name', table_name='component') - op.drop_table('component') + with contextlib.suppress(sa.exc.OperationalError): + op.create_table( + "apikey", + sa.Column("api_key", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("create_at", sa.DateTime(), nullable=False), + sa.Column("last_used_at", sa.DateTime(), nullable=True), + sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("id"), + ) + op.create_index(op.f("ix_apikey_api_key"), "apikey", ["api_key"], unique=True) + + with contextlib.suppress(sa.exc.OperationalError): + op.create_table( + "user", + sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False), + sa.Column("username", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("password", sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column("is_active", sa.Boolean(), nullable=False), + sa.Column("is_superuser", sa.Boolean(), nullable=False), + sa.Column("create_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.Column("last_login_at", sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("id"), + ) + op.create_index(op.f("ix_user_username"), "user", ["username"], unique=True) + with contextlib.suppress(sa.exc.OperationalError): + op.drop_table("flowstyle") + with contextlib.suppress(sa.exc.OperationalError): + op.drop_index("ix_component_frontend_node_id", table_name="component") + op.drop_index("ix_component_name", table_name="component") + op.drop_table("component") # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.create_table('component', - sa.Column('id', sa.CHAR(length=32), nullable=False), - sa.Column('frontend_node_id', sa.CHAR(length=32), nullable=False), - sa.Column('name', sa.VARCHAR(), nullable=False), - sa.Column('description', sa.VARCHAR(), nullable=True), - sa.Column('python_code', sa.VARCHAR(), nullable=True), - sa.Column('return_type', sa.VARCHAR(), nullable=True), - sa.Column('is_disabled', sa.BOOLEAN(), nullable=False), - sa.Column('is_read_only', sa.BOOLEAN(), nullable=False), - sa.Column('create_at', sa.DATETIME(), nullable=False), - sa.Column('update_at', sa.DATETIME(), nullable=False), - sa.PrimaryKeyConstraint('id') + op.create_table( + "component", + sa.Column("id", sa.CHAR(length=32), nullable=False), + sa.Column("frontend_node_id", sa.CHAR(length=32), nullable=False), + sa.Column("name", sa.VARCHAR(), nullable=False), + sa.Column("description", sa.VARCHAR(), nullable=True), + sa.Column("python_code", sa.VARCHAR(), nullable=True), + sa.Column("return_type", sa.VARCHAR(), nullable=True), + sa.Column("is_disabled", sa.BOOLEAN(), nullable=False), + sa.Column("is_read_only", sa.BOOLEAN(), nullable=False), + sa.Column("create_at", sa.DATETIME(), nullable=False), + sa.Column("update_at", sa.DATETIME(), nullable=False), + sa.PrimaryKeyConstraint("id"), ) - op.create_index('ix_component_name', 'component', ['name'], unique=False) - op.create_index('ix_component_frontend_node_id', 'component', ['frontend_node_id'], unique=False) - op.create_table('flowstyle', - sa.Column('color', sa.VARCHAR(), nullable=False), - sa.Column('emoji', sa.VARCHAR(), nullable=False), - sa.Column('flow_id', sa.CHAR(length=32), nullable=True), - sa.Column('id', sa.CHAR(length=32), nullable=False), - sa.ForeignKeyConstraint(['flow_id'], ['flow.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('id') + op.create_index("ix_component_name", "component", ["name"], unique=False) + op.create_index( + "ix_component_frontend_node_id", "component", ["frontend_node_id"], unique=False ) - op.drop_index(op.f('ix_user_username'), table_name='user') - op.drop_table('user') - op.drop_index(op.f('ix_apikey_api_key'), table_name='apikey') - op.drop_table('apikey') + op.create_table( + "flowstyle", + sa.Column("color", sa.VARCHAR(), nullable=False), + sa.Column("emoji", sa.VARCHAR(), nullable=False), + sa.Column("flow_id", sa.CHAR(length=32), nullable=True), + sa.Column("id", sa.CHAR(length=32), nullable=False), + sa.ForeignKeyConstraint( + ["flow_id"], + ["flow.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("id"), + ) + op.drop_index(op.f("ix_user_username"), table_name="user") + op.drop_table("user") + op.drop_index(op.f("ix_apikey_api_key"), table_name="apikey") + op.drop_table("apikey") # ### end Alembic commands ### From 6b82b730cd3d90eda40a773bf3f5d0efc524b47e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 25 Aug 2023 10:09:37 -0300 Subject: [PATCH 20/38] =?UTF-8?q?=F0=9F=94=A5=20refactor(auth):=20remove?= =?UTF-8?q?=20unused=20code=20and=20dependencies=20in=20auth=20module=20?= =?UTF-8?q?=F0=9F=94=A5=20refactor(routers):=20remove=20unused=20code=20an?= =?UTF-8?q?d=20dependencies=20in=20routers=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/auth/__init__.py | 0 src/backend/langflow/auth/auth.py | 219 ----------------------- src/backend/langflow/routers/__init__.py | 0 3 files changed, 219 deletions(-) delete mode 100644 src/backend/langflow/auth/__init__.py delete mode 100644 src/backend/langflow/auth/auth.py delete mode 100644 src/backend/langflow/routers/__init__.py diff --git a/src/backend/langflow/auth/__init__.py b/src/backend/langflow/auth/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/backend/langflow/auth/auth.py b/src/backend/langflow/auth/auth.py deleted file mode 100644 index f274a9523..000000000 --- a/src/backend/langflow/auth/auth.py +++ /dev/null @@ -1,219 +0,0 @@ -from uuid import UUID -from typing import Annotated -from jose import JWTError, jwt -from langflow.services.database.models.user import ( - User, - get_user_by_id, - get_user_by_username, -) -from sqlalchemy.orm import Session -from passlib.context import CryptContext -from fastapi.security import OAuth2PasswordBearer -from fastapi import Depends, HTTPException, status -from datetime import datetime, timedelta, timezone - -from langflow.services.utils import get_settings_manager, get_session - -from langflow.services.database.models.user import ( - update_user_last_login_at, -) - - -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") - - -async def get_current_user( - token: Annotated[str, Depends(oauth2_scheme)], db: Session = Depends(get_session) -) -> User: - settings_manager = get_settings_manager() - - credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) - - try: - payload = jwt.decode( - token, - settings_manager.auth_settings.SECRET_KEY, - algorithms=[settings_manager.auth_settings.ALGORITHM], - ) - user_id: UUID = payload.get("sub") # type: ignore - token_type: str = payload.get("type") # type: ignore - - if user_id is None or token_type: - raise credentials_exception - except JWTError as e: - raise credentials_exception from e - - user = get_user_by_id(db, user_id) # type: ignore - if user is None: - raise credentials_exception - return user - - -async def get_current_active_user( - current_user: Annotated[User, Depends(get_current_user)] -): - if not current_user.is_active: - raise HTTPException(status_code=400, detail="Inactive user") - return current_user - - -def verify_password(plain_password, hashed_password): - return pwd_context.verify(plain_password, hashed_password) - - -def get_password_hash(password): - return pwd_context.hash(password) - - -def create_token(data: dict, expires_delta: timedelta): - settings_manager = get_settings_manager() - - to_encode = data.copy() - expire = datetime.now(timezone.utc) + expires_delta - to_encode["exp"] = expire - - return jwt.encode( - to_encode, - settings_manager.auth_settings.SECRET_KEY, - algorithm=settings_manager.auth_settings.ALGORITHM, - ) - - -def create_super_user(db: Session = Depends(get_session)) -> User: - settings_manager = get_settings_manager() - - super_user = get_user_by_username( - db, settings_manager.auth_settings.FIRST_SUPERUSER - ) - - if not super_user: - super_user = User( - username=settings_manager.auth_settings.FIRST_SUPERUSER, - password=get_password_hash( - settings_manager.auth_settings.FIRST_SUPERUSER_PASSWORD - ), - is_superuser=True, - is_active=True, - last_login_at=None, - ) - - db.add(super_user) - db.commit() - db.refresh(super_user) - - return super_user - - -def create_user_longterm_token(db: Session = Depends(get_session)) -> dict: - super_user = create_super_user(db) - - access_token_expires_longterm = timedelta(days=365) - access_token = create_token( - data={"sub": str(super_user.id)}, - expires_delta=access_token_expires_longterm, - ) - - # Update: last_login_at - update_user_last_login_at(super_user.id, db) - - return { - "access_token": access_token, - "refresh_token": None, - "token_type": "bearer", - } - - -def create_user_api_key(user_id: UUID) -> dict: - access_token = create_token( - data={"sub": str(user_id), "role": "api_key"}, - expires_delta=timedelta(days=365 * 2), - ) - - return {"api_key": access_token} - - -def get_user_id_from_token(token: str) -> UUID: - try: - user_id = jwt.get_unverified_claims(token)["sub"] - return UUID(user_id) - except (KeyError, JWTError, ValueError): - return UUID(int=0) - - -def create_user_tokens( - user_id: UUID, db: Session = Depends(get_session), update_last_login: bool = False -) -> dict: - settings_manager = get_settings_manager() - - access_token_expires = timedelta( - minutes=settings_manager.auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES - ) - access_token = create_token( - data={"sub": str(user_id)}, - expires_delta=access_token_expires, - ) - - refresh_token_expires = timedelta( - minutes=settings_manager.auth_settings.REFRESH_TOKEN_EXPIRE_MINUTES - ) - refresh_token = create_token( - data={"sub": str(user_id), "type": "rf"}, - expires_delta=refresh_token_expires, - ) - - # Update: last_login_at - if update_last_login: - update_user_last_login_at(user_id, db) - - return { - "access_token": access_token, - "refresh_token": refresh_token, - "token_type": "bearer", - } - - -def create_refresh_token(refresh_token: str, db: Session = Depends(get_session)): - settings_manager = get_settings_manager() - - try: - payload = jwt.decode( - refresh_token, - settings_manager.auth_settings.SECRET_KEY, - algorithms=[settings_manager.auth_settings.ALGORITHM], - ) - user_id: UUID = payload.get("sub") # type: ignore - token_type: str = payload.get("type") # type: ignore - - if user_id is None or token_type is None: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token" - ) - - return create_user_tokens(user_id, db) - - except JWTError as e: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid refresh token", - ) from e - - -def authenticate_user( - username: str, password: str, db: Session = Depends(get_session) -) -> User | None: - user = get_user_by_username(db, username) - - if not user: - return None - - if not user.is_active: - if not user.last_login_at: - raise HTTPException(status_code=400, detail="Waiting for approval") - raise HTTPException(status_code=400, detail="Inactive user") - - return user if verify_password(password, user.password) else None diff --git a/src/backend/langflow/routers/__init__.py b/src/backend/langflow/routers/__init__.py deleted file mode 100644 index e69de29bb..000000000 From 43b2d62661204f385248f8faeaf0eb85a453f7f7 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 25 Aug 2023 10:10:19 -0300 Subject: [PATCH 21/38] =?UTF-8?q?=F0=9F=94=80=20chore(router.py):=20add=20?= =?UTF-8?q?login=5Frouter=20to=20the=20APIRouter=20to=20include=20login=20?= =?UTF-8?q?functionality=20=F0=9F=94=80=20chore(=5F=5Finit=5F=5F.py):=20im?= =?UTF-8?q?port=20and=20include=20login=5Frouter=20in=20the=20APIRouter=20?= =?UTF-8?q?to=20enable=20login=20functionality=20=F0=9F=94=80=20chore(logi?= =?UTF-8?q?n.py):=20add=20tags=20to=20the=20login=20router=20to=20categori?= =?UTF-8?q?ze=20it=20as=20"Login"=20in=20the=20API=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/api/router.py | 2 ++ src/backend/langflow/api/v1/__init__.py | 2 ++ src/backend/langflow/api/v1/login.py | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/api/router.py b/src/backend/langflow/api/router.py index 70cce437d..dbaf20e75 100644 --- a/src/backend/langflow/api/router.py +++ b/src/backend/langflow/api/router.py @@ -8,6 +8,7 @@ from langflow.api.v1 import ( component_router, users_router, api_key_router, + login_router, ) router = APIRouter( @@ -20,3 +21,4 @@ router.include_router(component_router) router.include_router(flows_router) router.include_router(users_router) router.include_router(api_key_router) +router.include_router(login_router) diff --git a/src/backend/langflow/api/v1/__init__.py b/src/backend/langflow/api/v1/__init__.py index 38f1c9148..9335a4607 100644 --- a/src/backend/langflow/api/v1/__init__.py +++ b/src/backend/langflow/api/v1/__init__.py @@ -5,6 +5,7 @@ from langflow.api.v1.flows import router as flows_router from langflow.api.v1.components import router as component_router from langflow.api.v1.users import router as users_router from langflow.api.v1.api_key import router as api_key_router +from langflow.api.v1.login import router as login_router __all__ = [ "chat_router", @@ -14,4 +15,5 @@ __all__ = [ "flows_router", "users_router", "api_key_router", + "login_router", ] diff --git a/src/backend/langflow/api/v1/login.py b/src/backend/langflow/api/v1/login.py index b559b0a23..600e373df 100644 --- a/src/backend/langflow/api/v1/login.py +++ b/src/backend/langflow/api/v1/login.py @@ -13,7 +13,7 @@ from langflow.services.auth.utils import ( from langflow.services.utils import get_settings_manager -router = APIRouter() +router = APIRouter(tags=["Login"]) @router.post("/login", response_model=Token) From e4cbc0a07ffdc5613dc33b11b571c003a54823bb Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 25 Aug 2023 10:10:57 -0300 Subject: [PATCH 22/38] =?UTF-8?q?=F0=9F=90=9B=20fix(users.py):=20change=20?= =?UTF-8?q?router=20tag=20from=20"Login"=20to=20"Users"=20for=20better=20c?= =?UTF-8?q?ategorization=20=E2=9C=A8=20feat(users.py):=20add=20status=20co?= =?UTF-8?q?de=20201=20to=20the=20response=20of=20the=20add=5Fuser=20endpoi?= =?UTF-8?q?nt=20to=20indicate=20successful=20creation=20of=20a=20new=20use?= =?UTF-8?q?r=20=F0=9F=90=9B=20fix(users.py):=20update=20the=20usage=20of?= =?UTF-8?q?=20UserCreate=20model=20to=20create=20a=20new=20User=20instance?= =?UTF-8?q?=20using=20User.from=5Form(user)=20for=20better=20compatibility?= =?UTF-8?q?=20=E2=9C=A8=20feat(users.py):=20add=20current=5Fuser=20paramet?= =?UTF-8?q?er=20to=20the=20read=5Fcurrent=5Fuser=20endpoint=20to=20enforce?= =?UTF-8?q?=20authentication=20and=20authorization=20=E2=9C=A8=20feat(user?= =?UTF-8?q?s.py):=20add=20current=5Fuser=20parameter=20to=20the=20read=5Fa?= =?UTF-8?q?ll=5Fusers=20endpoint=20to=20enforce=20authentication=20and=20a?= =?UTF-8?q?uthorization=20=E2=9C=A8=20feat(users.py):=20add=20current=5Fus?= =?UTF-8?q?er=20parameter=20to=20the=20delete=5Fuser=20endpoint=20to=20enf?= =?UTF-8?q?orce=20authentication=20and=20authorization.=20Also,=20add=20va?= =?UTF-8?q?lidation=20checks=20to=20prevent=20deleting=20own=20user=20acco?= =?UTF-8?q?unt=20and=20unauthorized=20deletion=20of=20users.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/api/v1/users.py | 29 +++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/backend/langflow/api/v1/users.py b/src/backend/langflow/api/v1/users.py index 33ddc9763..5a464b5f2 100644 --- a/src/backend/langflow/api/v1/users.py +++ b/src/backend/langflow/api/v1/users.py @@ -14,15 +14,19 @@ from sqlmodel import Session, select from fastapi import APIRouter, Depends, HTTPException from langflow.services.utils import get_session -from langflow.services.auth.utils import get_current_active_user, get_password_hash +from langflow.services.auth.utils import ( + get_current_active_superuser, + get_current_active_user, + get_password_hash, +) from langflow.services.database.models.user.utils import ( update_user, ) -router = APIRouter(tags=["Login"]) +router = APIRouter(tags=["Users"]) -@router.post("/user", response_model=UserRead) +@router.post("/user", response_model=UserRead, status_code=201) def add_user( user: UserCreate, db: Session = Depends(get_session), @@ -30,7 +34,7 @@ def add_user( """ Add a new user to the database. """ - new_user = User(**user.dict()) + new_user = User.from_orm(user) try: new_user.password = get_password_hash(user.password) @@ -45,7 +49,9 @@ def add_user( @router.get("/user", response_model=UserRead) -def read_current_user(current_user: User = Depends(get_current_active_user)) -> User: +def read_current_user( + current_user: User = Depends(get_current_active_user), +) -> User: """ Retrieve the current user's data. """ @@ -56,7 +62,7 @@ def read_current_user(current_user: User = Depends(get_current_active_user)) -> def read_all_users( skip: int = 0, limit: int = 10, - _: Session = Depends(get_current_active_user), + current_user: Session = Depends(get_current_active_superuser), db: Session = Depends(get_session), ) -> UsersResponse: """ @@ -90,12 +96,21 @@ def patch_user( @router.delete("/user/{user_id}") def delete_user( user_id: UUID, - _: Session = Depends(get_current_active_user), + current_user: Session = Depends(get_current_active_superuser), db: Session = Depends(get_session), ) -> dict: """ Delete a user from the database. """ + if current_user.id == user_id: + raise HTTPException( + status_code=400, detail="You can't delete your own user account" + ) + elif not current_user.is_superuser: + raise HTTPException( + status_code=403, detail="You don't have the permission to delete this user" + ) + user_db = db.query(User).filter(User.id == user_id).first() if not user_db: raise HTTPException(status_code=404, detail="User not found") From 4517f8ad5c3b5f5cbcee99bdc4ee19b68f9e5f23 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 25 Aug 2023 10:11:24 -0300 Subject: [PATCH 23/38] =?UTF-8?q?=F0=9F=94=A7=20fix(service.py):=20add=20r?= =?UTF-8?q?equest=20parameter=20to=20run=5Foauth2=5Fscheme=20method=20in?= =?UTF-8?q?=20AuthManager=20class=20to=20handle=20FastAPI=20request=20obje?= =?UTF-8?q?ct=20=F0=9F=94=A7=20fix(utils.py):=20add=20request=20parameter?= =?UTF-8?q?=20to=20auth=5Fscheme=5Fdependency=20function=20to=20handle=20F?= =?UTF-8?q?astAPI=20request=20object=20=F0=9F=94=A7=20fix(utils.py):=20cha?= =?UTF-8?q?nge=20get=5Fcurrent=5Factive=5Fuser=20function=20to=20synchrono?= =?UTF-8?q?us=20and=20remove=20async=20keyword=20=E2=9C=A8=20feat(utils.py?= =?UTF-8?q?):=20add=20get=5Fcurrent=5Factive=5Fsuperuser=20function=20to?= =?UTF-8?q?=20check=20if=20the=20current=20user=20is=20an=20active=20super?= =?UTF-8?q?user?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/services/auth/service.py | 5 ++-- src/backend/langflow/services/auth/utils.py | 26 +++++++++++++------ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/backend/langflow/services/auth/service.py b/src/backend/langflow/services/auth/service.py index 57c586c94..c80b984bb 100644 --- a/src/backend/langflow/services/auth/service.py +++ b/src/backend/langflow/services/auth/service.py @@ -1,3 +1,4 @@ +from fastapi import Request from langflow.services.base import Service from typing import TYPE_CHECKING @@ -13,5 +14,5 @@ class AuthManager(Service): # We need to define a function that can be passed to the Depends() function. # This function will be called by FastAPI to run oauth2_scheme - def run_oauth2_scheme(self, *args, **kwargs): - return self.settings_manager.auth_settings.oauth2_scheme(*args, **kwargs) + def run_oauth2_scheme(self, request: Request): + return self.settings_manager.auth_settings.oauth2_scheme(request=request) diff --git a/src/backend/langflow/services/auth/utils.py b/src/backend/langflow/services/auth/utils.py index aa4bec669..ae6065beb 100644 --- a/src/backend/langflow/services/auth/utils.py +++ b/src/backend/langflow/services/auth/utils.py @@ -1,5 +1,5 @@ from datetime import datetime, timedelta, timezone -from fastapi import Depends, HTTPException, status +from fastapi import Depends, HTTPException, Request, status from jose import JWTError, jwt from typing import Annotated from uuid import UUID @@ -14,12 +14,12 @@ from langflow.services.utils import get_session, get_settings_manager from sqlalchemy.orm import Session -def auth_scheme_dependency(*args, **kwargs): +def auth_scheme_dependency(request: Request): settings_manager = ( get_settings_manager() ) # Assuming get_settings_manager is defined - return AuthManager(settings_manager).run_oauth2_scheme(*args, **kwargs) + return AuthManager(settings_manager).run_oauth2_scheme(request) async def get_current_user( @@ -35,7 +35,7 @@ async def get_current_user( ) try: payload = jwt.decode( - token, + await token, settings_manager.auth_settings.SECRET_KEY, algorithms=[settings_manager.auth_settings.ALGORITHM], ) @@ -48,19 +48,29 @@ async def get_current_user( raise credentials_exception from e user = get_user_by_id(db, user_id) # type: ignore - if user is None: + if user is None or not user.is_active: raise credentials_exception return user -async def get_current_active_user( - current_user: Annotated[User, Depends(get_current_user)] -): +def get_current_active_user(current_user: Annotated[User, Depends(get_current_user)]): if not current_user.is_active: raise HTTPException(status_code=400, detail="Inactive user") return current_user +def get_current_active_superuser( + current_user: Annotated[User, Depends(get_current_user)] +) -> User: + if not current_user.is_active: + raise HTTPException(status_code=401, detail="Inactive user") + if not current_user.is_superuser: + raise HTTPException( + status_code=400, detail="The user doesn't have enough privileges" + ) + return current_user + + def verify_password(plain_password, hashed_password): settings_manager = get_settings_manager() return settings_manager.auth_settings.pwd_context.verify( From 92a7ae6be71c7cf6718db20232f75a55e0083a2e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 25 Aug 2023 10:16:04 -0300 Subject: [PATCH 24/38] =?UTF-8?q?=F0=9F=90=9B=20fix(utils.py):=20remove=20?= =?UTF-8?q?unnecessary=20type=20casting=20in=20get=5Fuser=5Fby=5Fusername?= =?UTF-8?q?=20and=20get=5Fuser=5Fby=5Fid=20functions=20=F0=9F=90=9B=20fix(?= =?UTF-8?q?utils.py):=20fix=20update=5Fuser=20function=20to=20correctly=20?= =?UTF-8?q?update=20user=20attributes=20and=20handle=20username=20conflict?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/database/models/user/utils.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/backend/langflow/services/database/models/user/utils.py b/src/backend/langflow/services/database/models/user/utils.py index 3b600bd9a..514ca4e82 100644 --- a/src/backend/langflow/services/database/models/user/utils.py +++ b/src/backend/langflow/services/database/models/user/utils.py @@ -4,41 +4,41 @@ from fastapi import Depends, HTTPException from langflow.services.database.models.user.user import User, UserUpdate from langflow.services.utils import get_session from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import Session +from sqlmodel import Session + + +from sqlalchemy.orm.attributes import flag_modified def get_user_by_username(db: Session, username: str) -> User: - db_user = db.query(User).filter(User.username == username).first() - return User.from_orm(db_user) if db_user else None # type: ignore + return db.query(User).filter(User.username == username).first() def get_user_by_id(db: Session, id: UUID) -> User: - db_user = db.query(User).filter(User.id == id).first() - return User.from_orm(db_user) if db_user else None # type: ignore + return db.query(User).filter(User.id == id).first() def update_user( user_id: UUID, user: UserUpdate, db: Session = Depends(get_session) ) -> User: - user_db = get_user_by_username(db, user.username) # type: ignore - if user_db and user_db.id != user_id: - raise HTTPException(status_code=409, detail="Username already exists") - user_db = get_user_by_id(db, user_id) if not user_db: raise HTTPException(status_code=404, detail="User not found") + user_db_by_username = get_user_by_username(db, user.username) # type: ignore + if user_db_by_username and user_db_by_username.id != user_id: + raise HTTPException(status_code=409, detail="Username already exists") + + user_data = user.dict(exclude_unset=True) + for attr, value in user_data.items(): + if hasattr(user_db, attr) and value is not None: + setattr(user_db, attr, value) + + user_db.updated_at = datetime.now(timezone.utc) + flag_modified(user_db, "updated_at") + try: - user_data = user.dict(exclude_unset=True) - for key, value in user_data.items(): - setattr(user_db, key, value) - - user_db.updated_at = datetime.now(timezone.utc) - user_db = db.merge(user_db) db.commit() - if db.identity_key(instance=user_db) is not None: - db.refresh(user_db) - except IntegrityError as e: db.rollback() raise HTTPException(status_code=400, detail=str(e)) from e From 7e3d329df9f0ed8b71794b6b3cccd1bf92f8ec27 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 25 Aug 2023 10:17:25 -0300 Subject: [PATCH 25/38] =?UTF-8?q?=F0=9F=94=A7=20chore(auth.py):=20add=20AP?= =?UTF-8?q?I=5FV1=5FSTR=20constant=20to=20improve=20code=20readability=20a?= =?UTF-8?q?nd=20maintainability=20=F0=9F=90=9B=20fix(auth.py):=20update=20?= =?UTF-8?q?tokenUrl=20in=20oauth2=5Fscheme=20to=20use=20API=5FV1=5FSTR=20c?= =?UTF-8?q?onstant=20for=20consistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/services/settings/auth.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backend/langflow/services/settings/auth.py b/src/backend/langflow/services/settings/auth.py index a00ebf31e..2aa4e17bc 100644 --- a/src/backend/langflow/services/settings/auth.py +++ b/src/backend/langflow/services/settings/auth.py @@ -18,6 +18,7 @@ class AuthSettings(BaseSettings): str ] = "b82818e0ad4ff76615c5721ee21004b07d84cd9b87ba4d9cb42374da134b841a" API_KEY_ALGORITHM: str = "HS256" + API_V1_STR: str = "/api/v1" # If AUTO_LOGIN = True # > The application does not request login and logs in automatically as a super user. @@ -26,7 +27,7 @@ class AuthSettings(BaseSettings): FIRST_SUPERUSER_PASSWORD: str = "langflow" pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") - oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") + oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{API_V1_STR}/login") class Config: validate_assignment = True From 7b897906b9dcf660dc0ccbb7677d9d76b4639d73 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 25 Aug 2023 10:17:36 -0300 Subject: [PATCH 26/38] =?UTF-8?q?=E2=9C=A8=20feat(test=5Flogin.py):=20add?= =?UTF-8?q?=20tests=20for=20login=20functionality?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🐛 fix(test_login.py): fix typo in test_login_unsuccessful_wrong_username test 🐛 fix(test_login.py): fix typo in test_login_unsuccessful_wrong_password test --- tests/test_login.py | 47 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 tests/test_login.py diff --git a/tests/test_login.py b/tests/test_login.py new file mode 100644 index 000000000..07abb35ab --- /dev/null +++ b/tests/test_login.py @@ -0,0 +1,47 @@ +import pytest +from langflow.services.database.models.user import User +from langflow.services.auth.utils import get_password_hash + + +@pytest.fixture +def test_user(): + return User( + username="testuser", + password=get_password_hash( + "testpassword" + ), # Assuming password needs to be hashed + is_active=True, + is_superuser=False, + ) + + +def test_login_successful(client, test_user, session): + # Adding the test user to the database + session.add(test_user) + session.commit() + + response = client.post( + "api/v1/login", data={"username": "testuser", "password": "testpassword"} + ) + assert response.status_code == 200 + assert "access_token" in response.json() + + +def test_login_unsuccessful_wrong_username(client): + response = client.post( + "api/v1/login", data={"username": "wrongusername", "password": "testpassword"} + ) + assert response.status_code == 401 + assert response.json()["detail"] == "Incorrect username or password" + + +def test_login_unsuccessful_wrong_password(client, test_user, session): + # Adding the test user to the database + session.add(test_user) + session.commit() + + response = client.post( + "api/v1/login", data={"username": "testuser", "password": "wrongpassword"} + ) + assert response.status_code == 401 + assert response.json()["detail"] == "Incorrect username or password" From 8411922a07d8376c704062e03ed93a04f006d860 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 25 Aug 2023 10:17:46 -0300 Subject: [PATCH 27/38] =?UTF-8?q?=E2=9C=A8=20feat(test=5Fuser.py):=20add?= =?UTF-8?q?=20tests=20for=20user-related=20functionalities=20=F0=9F=90=9B?= =?UTF-8?q?=20fix(test=5Fuser.py):=20fix=20typo=20in=20test=5Fuser=5Fwaiti?= =?UTF-8?q?ng=5Ffor=5Fapproval=20function=20name=20=F0=9F=90=9B=20fix(test?= =?UTF-8?q?=5Fuser.py):=20fix=20typo=20in=20test=5Fdeactivated=5Fuser=5Fca?= =?UTF-8?q?nnot=5Faccess=20function=20name=20=F0=9F=90=9B=20fix(test=5Fuse?= =?UTF-8?q?r.py):=20fix=20typo=20in=20test=5Fdata=5Fconsistency=5Fafter=5F?= =?UTF-8?q?update=20function=20name=20=F0=9F=90=9B=20fix(test=5Fuser.py):?= =?UTF-8?q?=20fix=20typo=20in=20test=5Fdata=5Fconsistency=5Fafter=5Fdelete?= =?UTF-8?q?=20function=20name=20=F0=9F=90=9B=20fix(test=5Fuser.py):=20fix?= =?UTF-8?q?=20typo=20in=20test=5Finactive=5Fuser=20function=20name=20?= =?UTF-8?q?=F0=9F=90=9B=20fix(test=5Fuser.py):=20fix=20typo=20in=20test=5F?= =?UTF-8?q?normal=5Fuser=5Fcant=5Fread=5Fall=5Fusers=20function=20name=20?= =?UTF-8?q?=F0=9F=90=9B=20fix(test=5Fuser.py):=20fix=20typo=20in=20test=5F?= =?UTF-8?q?patch=5Fuser=5Fwrong=5Fid=20function=20name=20=F0=9F=90=9B=20fi?= =?UTF-8?q?x(test=5Fuser.py):=20fix=20typo=20in=20test=5Fdelete=5Fuser=5Fw?= =?UTF-8?q?rong=5Fid=20function=20name=20=F0=9F=90=9B=20fix(test=5Fuser.py?= =?UTF-8?q?):=20fix=20typo=20in=20test=5Fnormal=5Fuser=5Fcant=5Fdelete=5Fu?= =?UTF-8?q?ser=20function=20name=20=E2=9C=A8=20feat(test=5Fuser.py):=20add?= =?UTF-8?q?=20test=5Fadd=5Fsuper=5Fuser=5Ffor=5Ftesting=5Fpurposes=5Fdelet?= =?UTF-8?q?e=5Fme=5Fbefore=5Fmerge=5Finto=5Fdev=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_user.py | 248 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 tests/test_user.py diff --git a/tests/test_user.py b/tests/test_user.py new file mode 100644 index 000000000..f8d4ff788 --- /dev/null +++ b/tests/test_user.py @@ -0,0 +1,248 @@ +from datetime import datetime +from langflow.services.auth.utils import create_super_user, get_password_hash + +from langflow.services.database.models.user.user import User +from langflow.services.utils import get_settings_manager +import pytest +from langflow.services.database.models.user import UserCreate, UserUpdate + + +@pytest.fixture +def test_user(client): + user_data = UserCreate( + username="testuser", + password="testpassword", + ) + response = client.post("/api/v1/user", json=user_data.dict()) + return response.json() + + +@pytest.fixture(scope="function") +def active_user(session): + user = User( + username="activeuser", + password=get_password_hash( + "testpassword" + ), # Assuming password needs to be hashed + is_active=True, + is_superuser=False, + ) + session.add(user) + session.commit() + return user + + +@pytest.fixture +def logged_in_headers(client, active_user): + login_data = {"username": active_user.username, "password": "testpassword"} + response = client.post("/api/v1/login", data=login_data) + assert response.status_code == 200 + tokens = response.json() + a_token = tokens["access_token"] + return {"Authorization": f"Bearer {a_token}"} + + +@pytest.fixture +def super_user(client, session): + return create_super_user(session) + + +@pytest.fixture +def super_user_headers(client, super_user): + settings_manager = get_settings_manager() + auth_settings = settings_manager.auth_settings + login_data = { + "username": auth_settings.FIRST_SUPERUSER, + "password": auth_settings.FIRST_SUPERUSER_PASSWORD, + } + response = client.post("/api/v1/login", data=login_data) + assert response.status_code == 200 + tokens = response.json() + a_token = tokens["access_token"] + return {"Authorization": f"Bearer {a_token}"} + + +@pytest.fixture +def deactivated_user(session): + user = User( + username="deactivateduser", + password=get_password_hash("testpassword"), + is_active=False, + is_superuser=False, + last_login_at=datetime.now(), + ) + session.add(user) + session.commit() + return user + + +def test_user_waiting_for_approval(client, session): + # Create a user that is not active and has never logged in + user = User( + username="waitingforapproval", + password=get_password_hash("testpassword"), + is_active=False, + last_login_at=None, + ) + session.add(user) + session.commit() + + login_data = {"username": "waitingforapproval", "password": "testpassword"} + response = client.post("/api/v1/login", data=login_data) + assert response.status_code == 400 + assert response.json()["detail"] == "Waiting for approval" + + +def test_deactivated_user_cannot_login(client, deactivated_user): + login_data = {"username": deactivated_user.username, "password": "testpassword"} + response = client.post("/api/v1/login", data=login_data) + assert response.status_code == 400, response.json() + assert response.json()["detail"] == "Inactive user" + + +def test_deactivated_user_cannot_access(client, deactivated_user, logged_in_headers): + # Assuming the headers for deactivated_user + response = client.get("/api/v1/users", headers=logged_in_headers) + assert response.status_code == 400, response.json() + assert response.json()["detail"] == "The user doesn't have enough privileges" + + +def test_data_consistency_after_update(client, active_user, logged_in_headers): + user_id = active_user.id + update_data = UserUpdate(username="newname") + + response = client.patch( + f"/api/v1/user/{user_id}", json=update_data.dict(), headers=logged_in_headers + ) + assert response.status_code == 200 + + # Fetch the updated user from the database + response = client.get("/api/v1/user", headers=logged_in_headers) + assert response.json()["username"] == "newname", response.json() + + +def test_data_consistency_after_delete(client, test_user, super_user_headers): + user_id = test_user["id"] + response = client.delete(f"/api/v1/user/{user_id}", headers=super_user_headers) + assert response.status_code == 200 + + # Attempt to fetch the deleted user from the database + response = client.get("/api/v1/users", headers=super_user_headers) + assert response.status_code == 200 + assert all(user["id"] != user_id for user in response.json()["users"]) + + +def test_inactive_user(client, session): + # Create a user that is not active and has a last_login_at value + user = User( + username="inactiveuser", + password=get_password_hash("testpassword"), + is_active=False, + last_login_at="2023-01-01T00:00:00", # Set to a valid datetime string + ) + session.add(user) + session.commit() + + login_data = {"username": "inactiveuser", "password": "testpassword"} + response = client.post("/api/v1/login", data=login_data) + assert response.status_code == 400 + assert response.json()["detail"] == "Inactive user" + + +def test_add_user(client, test_user): + assert test_user["username"] == "testuser" + + +# This is not used in the Frontend at the moment +# def test_read_current_user(client: TestClient, active_user): +# # First we need to login to get the access token +# login_data = {"username": "testuser", "password": "testpassword"} +# response = client.post("/api/v1/login", data=login_data) +# assert response.status_code == 200 + +# headers = {"Authorization": f"Bearer {response.json()['access_token']}"} + +# response = client.get("/api/v1/user", headers=headers) +# assert response.status_code == 200, response.json() +# assert response.json()["username"] == "testuser" + + +def test_read_all_users(client, super_user_headers): + response = client.get("/api/v1/users", headers=super_user_headers) + assert response.status_code == 200, response.json() + assert isinstance(response.json()["users"], list) + + +def test_normal_user_cant_read_all_users(client, logged_in_headers): + response = client.get("/api/v1/users", headers=logged_in_headers) + assert response.status_code == 400, response.json() + assert response.json() == {"detail": "The user doesn't have enough privileges"} + + +def test_patch_user(client, active_user, logged_in_headers): + user_id = active_user.id + update_data = UserUpdate( + username="newname", + ) + + response = client.patch( + f"/api/v1/user/{user_id}", json=update_data.dict(), headers=logged_in_headers + ) + assert response.status_code == 200, response.json() + + +def test_patch_user_wrong_id(client, active_user, logged_in_headers): + user_id = "wrong_id" + update_data = UserUpdate( + username="newname", + ) + + response = client.patch( + f"/api/v1/user/{user_id}", json=update_data.dict(), headers=logged_in_headers + ) + assert response.status_code == 422, response.json() + assert response.json() == { + "detail": [ + { + "loc": ["path", "user_id"], + "msg": "value is not a valid uuid", + "type": "type_error.uuid", + } + ] + } + + +def test_delete_user(client, test_user, super_user_headers): + user_id = test_user["id"] + response = client.delete(f"/api/v1/user/{user_id}", headers=super_user_headers) + assert response.status_code == 200 + assert response.json() == {"detail": "User deleted"} + + +def test_delete_user_wrong_id(client, test_user, super_user_headers): + user_id = "wrong_id" + response = client.delete(f"/api/v1/user/{user_id}", headers=super_user_headers) + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "loc": ["path", "user_id"], + "msg": "value is not a valid uuid", + "type": "type_error.uuid", + } + ] + } + + +def test_normal_user_cant_delete_user(client, test_user, logged_in_headers): + user_id = test_user["id"] + response = client.delete(f"/api/v1/user/{user_id}", headers=logged_in_headers) + assert response.status_code == 400 + assert response.json() == {"detail": "The user doesn't have enough privileges"} + + +# If you still want to test the superuser endpoint +def test_add_super_user_for_testing_purposes_delete_me_before_merge_into_dev(client): + response = client.post("/api/v1/super_user") + assert response.status_code == 200 + assert response.json()["username"] == "superuser" From 81816baadae57a70c10772397ec9848a6fd2d277 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 25 Aug 2023 10:24:01 -0300 Subject: [PATCH 28/38] =?UTF-8?q?=F0=9F=94=A7=20fix(login.py):=20update=20?= =?UTF-8?q?import=20statement=20from=20sqlalchemy.orm=20to=20sqlmodel.Sess?= =?UTF-8?q?ion=20for=20Session=20class=20=F0=9F=94=A7=20fix(users.py):=20u?= =?UTF-8?q?pdate=20type=20annotation=20for=20current=5Fuser=20parameter=20?= =?UTF-8?q?from=20Session=20to=20User=20=F0=9F=94=A7=20fix(utils.py):=20up?= =?UTF-8?q?date=20import=20statement=20from=20sqlalchemy.orm=20to=20sqlmod?= =?UTF-8?q?el.Session=20=F0=9F=94=A7=20fix(utils.py):=20update=20type=20an?= =?UTF-8?q?notation=20for=20token=20parameter=20from=20Annotated=20to=20Un?= =?UTF-8?q?ion[Coroutine,=20str]=20=F0=9F=94=A7=20fix(utils.py):=20update?= =?UTF-8?q?=20type=20annotation=20for=20get=5Fuser=5Fby=5Fusername=20funct?= =?UTF-8?q?ion=20return=20type=20from=20User=20to=20Union[User,=20None]=20?= =?UTF-8?q?=F0=9F=94=A7=20fix(utils.py):=20update=20type=20annotation=20fo?= =?UTF-8?q?r=20get=5Fuser=5Fby=5Fid=20function=20return=20type=20from=20Us?= =?UTF-8?q?er=20to=20Union[User,=20None]=20=F0=9F=94=A7=20fix(manager.py):?= =?UTF-8?q?=20update=20type=20annotation=20for=20dependencies=20parameter?= =?UTF-8?q?=20in=20register=5Ffactory=20method=20from=20List=20to=20Option?= =?UTF-8?q?al[List]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/api/v1/login.py | 2 +- src/backend/langflow/api/v1/users.py | 2 +- src/backend/langflow/services/auth/utils.py | 10 +++++++--- .../langflow/services/database/models/user/utils.py | 5 +++-- src/backend/langflow/services/manager.py | 6 ++++-- 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/backend/langflow/api/v1/login.py b/src/backend/langflow/api/v1/login.py index 600e373df..a11167a40 100644 --- a/src/backend/langflow/api/v1/login.py +++ b/src/backend/langflow/api/v1/login.py @@ -1,4 +1,4 @@ -from sqlalchemy.orm import Session +from sqlmodel import Session from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm diff --git a/src/backend/langflow/api/v1/users.py b/src/backend/langflow/api/v1/users.py index 5a464b5f2..e41551e8c 100644 --- a/src/backend/langflow/api/v1/users.py +++ b/src/backend/langflow/api/v1/users.py @@ -96,7 +96,7 @@ def patch_user( @router.delete("/user/{user_id}") def delete_user( user_id: UUID, - current_user: Session = Depends(get_current_active_superuser), + current_user: User = Depends(get_current_active_superuser), db: Session = Depends(get_session), ) -> dict: """ diff --git a/src/backend/langflow/services/auth/utils.py b/src/backend/langflow/services/auth/utils.py index ae6065beb..540b012b1 100644 --- a/src/backend/langflow/services/auth/utils.py +++ b/src/backend/langflow/services/auth/utils.py @@ -1,7 +1,7 @@ from datetime import datetime, timedelta, timezone from fastapi import Depends, HTTPException, Request, status from jose import JWTError, jwt -from typing import Annotated +from typing import Annotated, Coroutine from uuid import UUID from langflow.services.auth.service import AuthManager from langflow.services.database.models.user.user import User @@ -11,7 +11,7 @@ from langflow.services.database.models.user.utils import ( update_user_last_login_at, ) from langflow.services.utils import get_session, get_settings_manager -from sqlalchemy.orm import Session +from sqlmodel import Session def auth_scheme_dependency(request: Request): @@ -33,9 +33,13 @@ async def get_current_user( detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) + + if isinstance(token, Coroutine): + token = await token + try: payload = jwt.decode( - await token, + token, settings_manager.auth_settings.SECRET_KEY, algorithms=[settings_manager.auth_settings.ALGORITHM], ) diff --git a/src/backend/langflow/services/database/models/user/utils.py b/src/backend/langflow/services/database/models/user/utils.py index 514ca4e82..3dc02a499 100644 --- a/src/backend/langflow/services/database/models/user/utils.py +++ b/src/backend/langflow/services/database/models/user/utils.py @@ -1,4 +1,5 @@ from datetime import datetime, timezone +from typing import Union from uuid import UUID from fastapi import Depends, HTTPException from langflow.services.database.models.user.user import User, UserUpdate @@ -10,11 +11,11 @@ from sqlmodel import Session from sqlalchemy.orm.attributes import flag_modified -def get_user_by_username(db: Session, username: str) -> User: +def get_user_by_username(db: Session, username: str) -> Union[User, None]: return db.query(User).filter(User.username == username).first() -def get_user_by_id(db: Session, id: UUID) -> User: +def get_user_by_id(db: Session, id: UUID) -> Union[User, None]: return db.query(User).filter(User.id == id).first() diff --git a/src/backend/langflow/services/manager.py b/src/backend/langflow/services/manager.py index 1592e7612..e9895adab 100644 --- a/src/backend/langflow/services/manager.py +++ b/src/backend/langflow/services/manager.py @@ -1,5 +1,5 @@ from langflow.services.schema import ServiceType -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, List, Optional if TYPE_CHECKING: from langflow.services.factory import ServiceFactory @@ -16,7 +16,9 @@ class ServiceManager: self.dependencies = {} def register_factory( - self, service_factory: "ServiceFactory", dependencies: List[ServiceType] = None + self, + service_factory: "ServiceFactory", + dependencies: Optional[List[ServiceType]] = None, ): """ Registers a new factory with dependencies. From fc93e4ced0235b9506837d0922590fe2cb057a85 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 25 Aug 2023 10:24:33 -0300 Subject: [PATCH 29/38] =?UTF-8?q?=F0=9F=94=80=20chore(test=5Fdatabase.py):?= =?UTF-8?q?=20update=20import=20statement=20for=20Session=20from=20sqlalch?= =?UTF-8?q?emy.orm=20to=20sqlmodel=20to=20match=20the=20library=20being=20?= =?UTF-8?q?used?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_database.py b/tests/test_database.py index 52a5daa4c..996d4faa5 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -2,7 +2,7 @@ import json import pytest from uuid import UUID, uuid4 -from sqlalchemy.orm import Session +from sqlmodel import Session from fastapi.testclient import TestClient From 8d1e1b871044260c8b734a144888d210458afb06 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 25 Aug 2023 10:25:28 -0300 Subject: [PATCH 30/38] =?UTF-8?q?=F0=9F=94=92=20chore(api=5Fkey.py):=20upd?= =?UTF-8?q?ate=20hardcoded=20API=20key=20value=20to=20improve=20security?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🔒 chore(api_key.py): update hardcoded API key value to improve security by removing the actual key from the code and adding a placeholder comment --- src/backend/langflow/api/v1/api_key.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/langflow/api/v1/api_key.py b/src/backend/langflow/api/v1/api_key.py index 9fa6acec5..56deb3059 100644 --- a/src/backend/langflow/api/v1/api_key.py +++ b/src/backend/langflow/api/v1/api_key.py @@ -40,7 +40,7 @@ def create_api_key(user_id: str): return { "user_id": user_id, "name": "my api-key 01", - "api_key": "lf-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1YTBmODM1ZS0yMTQxLTQ2YWItYmQ4NS0yMWEzMjQ1MTE2ZDAiLCJleHAiOjE2OTIyMTUwMTN9.c_s0ZPRtjSI9yUrhi8ACIwyXf0feRLYfaeIZEbRVKQg", + "api_key": "lf-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1YTBmODM1ZS0yMTQxLTQ2YWItYmQ4NS0yMWEzMjQ1MTE2ZDAiLCJleHAiOjE2OTIyMTUwMTN9.c_s0ZPRtjSI9yUrhi8ACIwyXf0feRLYfaeIZEbRVKQg", # noqa } From eb02220aadc6e4c0ab20923c206a8c42395cb205 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 25 Aug 2023 12:02:32 -0300 Subject: [PATCH 31/38] =?UTF-8?q?=E2=9C=A8=20feat(api=5Fkey.py):=20add=20G?= =?UTF-8?q?ET=20and=20POST=20routes=20for=20retrieving=20and=20creating=20?= =?UTF-8?q?API=20keys=20=F0=9F=90=9B=20fix(api=5Fkey.py):=20fix=20delete?= =?UTF-8?q?=5Fapi=5Fkey=20route=20to=20use=20correct=20dependencies=20and?= =?UTF-8?q?=20handle=20exceptions=20properly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/api/v1/api_key.py | 87 ++++++++++++++------------ 1 file changed, 48 insertions(+), 39 deletions(-) diff --git a/src/backend/langflow/api/v1/api_key.py b/src/backend/langflow/api/v1/api_key.py index 56deb3059..e7b2dd163 100644 --- a/src/backend/langflow/api/v1/api_key.py +++ b/src/backend/langflow/api/v1/api_key.py @@ -1,49 +1,58 @@ -from fastapi import APIRouter +from fastapi import APIRouter, HTTPException, Depends +from langflow.api.v1.schemas import ApiKeysResponse, CreateApiKeyRequest +from langflow.services.auth.utils import get_current_active_user +from langflow.services.database.models.api_key.api_key import UnmaskedApiKeyRead + +# Assuming you have these methods in your service layer +from langflow.services.database.models.api_key.crud import ( + get_api_keys, + create_api_key, + delete_api_key, +) +from langflow.services.database.models.user.user import User +from langflow.services.utils import get_session +from sqlmodel import Session router = APIRouter(tags=["APIKey"]) -@router.get("/api_key/{user_id}") -def get_api_key(user_id: str): - return { - "total_count": 3, - "user_id": user_id, - "api_keys": [ - { - "id": "4425707e-cce4-4d1b-a54e-bd2632064657", - "api_key": "lf-...abcd", - "name": "my api_key name - 01", - "created_at": "2023-08-15T19:28:40.019613", - "last_used_at": "2023-08-16T18:38:20.875210", - }, - { - "id": "6fb7282b-9f2e-4efe-9bda-0c3d8f899473", - "api_key": "lf-...abcd", - "name": "my api_key name - 02", - "created_at": "2023-08-15T19:41:30.077942", - "last_used_at": "2023-08-15T19:45:32.067899", - }, - { - "id": "c55f3b32-4920-42b6-a5cd-698b4251806e", - "api_key": "lf-...abcd", - "name": "my api_key name - 03", - "created_at": "2023-08-15T20:29:40.577808", - "last_used_at": "2023-08-15T20:29:40.577816", - }, - ], - } +@router.get("/api_key", response_model=ApiKeysResponse) +def get_api_keys_route( + db: Session = Depends(get_session), + current_user: User = Depends(get_current_active_user), +): + try: + user_id = current_user.id + keys = get_api_keys(db, user_id) + + result = {"total_count": len(keys), "user_id": user_id, "api_keys": keys} + return ApiKeysResponse(**result) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) -@router.post("/api_key/{user_id}") -def create_api_key(user_id: str): - return { - "user_id": user_id, - "name": "my api-key 01", - "api_key": "lf-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1YTBmODM1ZS0yMTQxLTQ2YWItYmQ4NS0yMWEzMjQ1MTE2ZDAiLCJleHAiOjE2OTIyMTUwMTN9.c_s0ZPRtjSI9yUrhi8ACIwyXf0feRLYfaeIZEbRVKQg", # noqa - } +@router.post("/api_key", response_model=UnmaskedApiKeyRead) +def create_api_key_route( + req: CreateApiKeyRequest, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_session), +): + try: + user_id = current_user.id + return create_api_key(db, req, user_id=user_id) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) from e @router.delete("/api_key/{api_key_id}") -def delete_api_key(api_key_id: str): - return {"detail": "API Key deleted"} +def delete_api_key_route( + api_key_id: str, + current_user=Depends(get_current_active_user), + db: Session = Depends(get_session), +): + try: + delete_api_key(db, api_key_id) + return {"detail": "API Key deleted"} + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) From edd58705e4c4962c9d5cc200a99cd771827cb79e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 25 Aug 2023 12:03:03 -0300 Subject: [PATCH 32/38] =?UTF-8?q?=F0=9F=94=A8=20refactor(schemas.py):=20up?= =?UTF-8?q?date=20import=20statement=20for=20ApiKeyRead=20to=20reflect=20n?= =?UTF-8?q?ew=20file=20structure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🔨 refactor(schemas.py): update import statement for ApiKeyRead to reflect --- src/backend/langflow/api/v1/schemas.py | 27 +++++++++++++++------ src/backend/langflow/api/v1/users.py | 2 +- src/backend/langflow/services/auth/utils.py | 2 +- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/backend/langflow/api/v1/schemas.py b/src/backend/langflow/api/v1/schemas.py index d788469fa..3c138610c 100644 --- a/src/backend/langflow/api/v1/schemas.py +++ b/src/backend/langflow/api/v1/schemas.py @@ -1,7 +1,8 @@ from enum import Enum from pathlib import Path from typing import Any, Dict, List, Optional, Union -from langflow.services.database.models.api_key import ApiKeyRead +from uuid import UUID +from langflow.services.database.models.api_key.api_key import ApiKeyRead from langflow.services.database.models.flow import FlowCreate, FlowRead from langflow.services.database.models.user import UserRead from pydantic import BaseModel, Field, validator @@ -138,12 +139,24 @@ class ComponentListRead(BaseModel): flows: List[FlowRead] -class ApiKeyResponse(BaseModel): - total_count: int - user_id: str - api_keys: List[ApiKeyRead] - - class UsersResponse(BaseModel): total_count: int users: List[UserRead] + + +class ApiKeyResponse(BaseModel): + id: str + api_key: str + name: str + created_at: str + last_used_at: str + + +class ApiKeysResponse(BaseModel): + total_count: int + user_id: UUID + api_keys: List[ApiKeyRead] + + +class CreateApiKeyRequest(BaseModel): + name: str diff --git a/src/backend/langflow/api/v1/users.py b/src/backend/langflow/api/v1/users.py index e41551e8c..140ee773f 100644 --- a/src/backend/langflow/api/v1/users.py +++ b/src/backend/langflow/api/v1/users.py @@ -19,7 +19,7 @@ from langflow.services.auth.utils import ( get_current_active_user, get_password_hash, ) -from langflow.services.database.models.user.utils import ( +from langflow.services.database.models.user.crud import ( update_user, ) diff --git a/src/backend/langflow/services/auth/utils.py b/src/backend/langflow/services/auth/utils.py index 540b012b1..8cc67d216 100644 --- a/src/backend/langflow/services/auth/utils.py +++ b/src/backend/langflow/services/auth/utils.py @@ -5,7 +5,7 @@ from typing import Annotated, Coroutine from uuid import UUID from langflow.services.auth.service import AuthManager from langflow.services.database.models.user.user import User -from langflow.services.database.models.user.utils import ( +from langflow.services.database.models.user.crud import ( get_user_by_id, get_user_by_username, update_user_last_login_at, From 82fbeace068a09a83e1c8d02f958a1e40643c39b Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 25 Aug 2023 12:03:51 -0300 Subject: [PATCH 33/38] =?UTF-8?q?=F0=9F=94=80=20chore(api=5Fkey):=20update?= =?UTF-8?q?=20import=20and=20export=20names=20in=20=5F=5Finit=5F=5F.py=20f?= =?UTF-8?q?or=20better=20clarity=20and=20consistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🔀 chore(api_key): add UnmaskedApiKeyRead model to represent an unmasked API key 🔀 chore(api_key): add user relationship to ApiKey model for easier access to associated user 🔀 chore(api_key): add user_id field to ApiKeyBase and ApiKeyCreate models for easier user association 🔀 chore(api_key): add mask_api_key validator to ApiKeyRead model to mask the API key for security reasons --- .../database/models/api_key/__init__.py | 4 +-- .../database/models/api_key/api_key.py | 28 ++++++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/backend/langflow/services/database/models/api_key/__init__.py b/src/backend/langflow/services/database/models/api_key/__init__.py index c97425ee8..fbb8265b9 100644 --- a/src/backend/langflow/services/database/models/api_key/__init__.py +++ b/src/backend/langflow/services/database/models/api_key/__init__.py @@ -1,3 +1,3 @@ -from .api_key import ApiKey, ApiKeyCreate, ApiKeyRead +from .api_key import ApiKey, ApiKeyCreate, UnmaskedApiKeyRead, ApiKeyRead -__all__ = ["ApiKey", "ApiKeyCreate", "ApiKeyRead"] +__all__ = ["ApiKey", "ApiKeyCreate", "UnmaskedApiKeyRead", "ApiKeyRead"] diff --git a/src/backend/langflow/services/database/models/api_key/api_key.py b/src/backend/langflow/services/database/models/api_key/api_key.py index 784b25229..1006b1c0f 100644 --- a/src/backend/langflow/services/database/models/api_key/api_key.py +++ b/src/backend/langflow/services/database/models/api_key/api_key.py @@ -1,24 +1,44 @@ -from sqlmodel import Field +from pydantic import validator +from sqlmodel import Field, Relationship from uuid import UUID, uuid4 -from typing import Optional +from typing import Optional, TYPE_CHECKING from datetime import datetime from langflow.services.database.models.base import SQLModelSerializable +if TYPE_CHECKING: + from langflow.services.database.models.user import User + class ApiKeyBase(SQLModelSerializable): api_key: str = Field(index=True, unique=True) name: Optional[str] = Field(index=True) - create_at: datetime = Field(default_factory=datetime.utcnow) + created_at: datetime = Field(default_factory=datetime.utcnow) last_used_at: Optional[datetime] = Field(default=None) + user_id: UUID = Field() class ApiKey(ApiKeyBase, table=True): id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True) + # User relationship + user_id: UUID = Field(index=True, foreign_key="user.id") + user: "User" = Relationship(back_populates="api_keys") class ApiKeyCreate(ApiKeyBase): - pass + api_key: Optional[str] = None + user_id: Optional[UUID] = None + + +class UnmaskedApiKeyRead(ApiKeyBase): + id: UUID class ApiKeyRead(ApiKeyBase): id: UUID + api_key: Optional[str] = None + user_id: Optional[UUID] = None + + @validator("api_key", always=True) + def mask_api_key(cls, v): + # This validator will always run, and will mask the API key + return f"{'*' * 8}{v[-4:]}" From e42686738ce21d5ad850196aa34a78dee0841f1d Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 25 Aug 2023 12:04:20 -0300 Subject: [PATCH 34/38] =?UTF-8?q?=F0=9F=93=A6=20chore(api=5Fkey/crud.py):?= =?UTF-8?q?=20add=20CRUD=20functions=20for=20managing=20API=20keys=20in=20?= =?UTF-8?q?the=20database?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds the following functions to the `api_key/crud.py` file: - `get_api_keys`: Retrieves a list of API keys associated with a specific user ID from the database. - `create_api_key`: Generates a random API key, hashes it, and creates a new `ApiKey` object in the database. - `delete_api_key`: Deletes an API key from the database based on its ID. These functions provide the necessary functionality for managing API keys in the application's database. --- .../services/database/models/api_key/crud.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/backend/langflow/services/database/models/api_key/crud.py diff --git a/src/backend/langflow/services/database/models/api_key/crud.py b/src/backend/langflow/services/database/models/api_key/crud.py new file mode 100644 index 000000000..933e7c2b3 --- /dev/null +++ b/src/backend/langflow/services/database/models/api_key/crud.py @@ -0,0 +1,44 @@ +import secrets +from uuid import UUID +from typing import List +from langflow.services.auth.utils import get_password_hash +from sqlmodel import Session, select +from langflow.services.database.models.api_key import ( + ApiKey, + ApiKeyCreate, + UnmaskedApiKeyRead, + ApiKeyRead, +) + + +def get_api_keys(session: Session, user_id: str) -> List[UnmaskedApiKeyRead]: + query = select(ApiKey).where(ApiKey.user_id == user_id) + api_keys = session.exec(query).all() + return [ApiKeyRead.from_orm(api_key) for api_key in api_keys] + + +def create_api_key( + session: Session, api_key_create: ApiKeyCreate, user_id: str +) -> UnmaskedApiKeyRead: + # Generate a random API key with 32 bytes of randomness + generated_api_key = secrets.token_urlsafe(32) + + # hash the API key + hashed_api_key = get_password_hash(generated_api_key) + # Use the generated key to create the ApiKey object + api_key = ApiKey(api_key=hashed_api_key, **api_key_create.dict(), user_id=user_id) + + session.add(api_key) + session.commit() + session.refresh(api_key) + unmasked = UnmaskedApiKeyRead.from_orm(api_key) + unmasked.api_key = generated_api_key + return unmasked + + +def delete_api_key(session: Session, api_key_id: UUID) -> None: + api_key = session.get(ApiKey, api_key_id) + if api_key is None: + raise ValueError("API Key not found") + session.delete(api_key) + session.commit() From 7783f4532ea6efb75fdd082aa6455ecf3633e23a Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 25 Aug 2023 12:04:51 -0300 Subject: [PATCH 35/38] =?UTF-8?q?=F0=9F=93=A6=20chore(user/crud.py):=20add?= =?UTF-8?q?=20CRUD=20operations=20for=20User=20model=20in=20database=20?= =?UTF-8?q?=F0=9F=94=92=20fix(user/crud.py):=20add=20validation=20to=20pre?= =?UTF-8?q?vent=20duplicate=20usernames=20during=20user=20update=20?= =?UTF-8?q?=F0=9F=94=84=20refactor(user/crud.py):=20update=20user=20attrib?= =?UTF-8?q?utes=20and=20last=5Flogin=5Fat=20timestamp=20during=20user=20up?= =?UTF-8?q?date=20=F0=9F=90=9B=20fix(user/crud.py):=20handle=20IntegrityEr?= =?UTF-8?q?ror=20during=20user=20update=20and=20rollback=20transaction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/database/models/user/crud.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/backend/langflow/services/database/models/user/crud.py diff --git a/src/backend/langflow/services/database/models/user/crud.py b/src/backend/langflow/services/database/models/user/crud.py new file mode 100644 index 000000000..3dc02a499 --- /dev/null +++ b/src/backend/langflow/services/database/models/user/crud.py @@ -0,0 +1,53 @@ +from datetime import datetime, timezone +from typing import Union +from uuid import UUID +from fastapi import Depends, HTTPException +from langflow.services.database.models.user.user import User, UserUpdate +from langflow.services.utils import get_session +from sqlalchemy.exc import IntegrityError +from sqlmodel import Session + + +from sqlalchemy.orm.attributes import flag_modified + + +def get_user_by_username(db: Session, username: str) -> Union[User, None]: + return db.query(User).filter(User.username == username).first() + + +def get_user_by_id(db: Session, id: UUID) -> Union[User, None]: + return db.query(User).filter(User.id == id).first() + + +def update_user( + user_id: UUID, user: UserUpdate, db: Session = Depends(get_session) +) -> User: + user_db = get_user_by_id(db, user_id) + if not user_db: + raise HTTPException(status_code=404, detail="User not found") + + user_db_by_username = get_user_by_username(db, user.username) # type: ignore + if user_db_by_username and user_db_by_username.id != user_id: + raise HTTPException(status_code=409, detail="Username already exists") + + user_data = user.dict(exclude_unset=True) + for attr, value in user_data.items(): + if hasattr(user_db, attr) and value is not None: + setattr(user_db, attr, value) + + user_db.updated_at = datetime.now(timezone.utc) + flag_modified(user_db, "updated_at") + + try: + db.commit() + except IntegrityError as e: + db.rollback() + raise HTTPException(status_code=400, detail=str(e)) from e + + return user_db + + +def update_user_last_login_at(user_id: UUID, db: Session = Depends(get_session)): + user_data = UserUpdate(last_login_at=datetime.now(timezone.utc)) # type: ignore + + return update_user(user_id, user_data, db) From ba081e81b68f7a1f76a8664c96a21fa0836e25e2 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 25 Aug 2023 12:05:20 -0300 Subject: [PATCH 36/38] =?UTF-8?q?=F0=9F=94=80=20chore(user.py):=20add=20ty?= =?UTF-8?q?pe=20hinting=20for=20ApiKey=20import=20to=20improve=20code=20re?= =?UTF-8?q?adability=20=F0=9F=94=80=20chore(user.py):=20add=20relationship?= =?UTF-8?q?=20between=20User=20and=20ApiKey=20models=20to=20establish=20a?= =?UTF-8?q?=20one-to-many=20relationship?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../langflow/services/database/models/user/user.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/backend/langflow/services/database/models/user/user.py b/src/backend/langflow/services/database/models/user/user.py index 3a4308b42..b6c27c2dc 100644 --- a/src/backend/langflow/services/database/models/user/user.py +++ b/src/backend/langflow/services/database/models/user/user.py @@ -1,11 +1,14 @@ from langflow.services.database.models.base import SQLModel, SQLModelSerializable -from sqlmodel import Field +from sqlmodel import Field, Relationship from datetime import datetime -from typing import Optional +from typing import Optional, TYPE_CHECKING from uuid import UUID, uuid4 +if TYPE_CHECKING: + from langflow.services.database.models.api_key import ApiKey + class User(SQLModelSerializable, table=True): id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True) @@ -16,6 +19,7 @@ class User(SQLModelSerializable, table=True): create_at: datetime = Field(default_factory=datetime.utcnow) updated_at: datetime = Field(default_factory=datetime.utcnow) last_login_at: Optional[datetime] = Field() + api_keys: list["ApiKey"] = Relationship(back_populates="user") class UserCreate(SQLModel): From 950b9d179bb6994f9df951ed2070795a033e2bf5 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 25 Aug 2023 12:05:54 -0300 Subject: [PATCH 37/38] =?UTF-8?q?=F0=9F=94=A5=20refactor(user/utils.py):?= =?UTF-8?q?=20remove=20unused=20code=20and=20imports=20from=20user/utils.p?= =?UTF-8?q?y=20module=20=E2=9C=A8=20feat(test=5Fapi=5Fkey.py):=20add=20tes?= =?UTF-8?q?ts=20for=20API=20key=20creation,=20retrieval,=20and=20deletion?= =?UTF-8?q?=20=E2=9C=A8=20feat(test=5Fuser.py):=20remove=20unused=20fixtur?= =?UTF-8?q?es=20and=20imports=20from=20test=5Fuser.py=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/database/models/user/utils.py | 53 ------------------- tests/conftest.py | 37 +++++++++++++ tests/test_api_key.py | 50 +++++++++++++++++ tests/test_user.py | 37 +------------ 4 files changed, 88 insertions(+), 89 deletions(-) delete mode 100644 src/backend/langflow/services/database/models/user/utils.py create mode 100644 tests/test_api_key.py diff --git a/src/backend/langflow/services/database/models/user/utils.py b/src/backend/langflow/services/database/models/user/utils.py deleted file mode 100644 index 3dc02a499..000000000 --- a/src/backend/langflow/services/database/models/user/utils.py +++ /dev/null @@ -1,53 +0,0 @@ -from datetime import datetime, timezone -from typing import Union -from uuid import UUID -from fastapi import Depends, HTTPException -from langflow.services.database.models.user.user import User, UserUpdate -from langflow.services.utils import get_session -from sqlalchemy.exc import IntegrityError -from sqlmodel import Session - - -from sqlalchemy.orm.attributes import flag_modified - - -def get_user_by_username(db: Session, username: str) -> Union[User, None]: - return db.query(User).filter(User.username == username).first() - - -def get_user_by_id(db: Session, id: UUID) -> Union[User, None]: - return db.query(User).filter(User.id == id).first() - - -def update_user( - user_id: UUID, user: UserUpdate, db: Session = Depends(get_session) -) -> User: - user_db = get_user_by_id(db, user_id) - if not user_db: - raise HTTPException(status_code=404, detail="User not found") - - user_db_by_username = get_user_by_username(db, user.username) # type: ignore - if user_db_by_username and user_db_by_username.id != user_id: - raise HTTPException(status_code=409, detail="Username already exists") - - user_data = user.dict(exclude_unset=True) - for attr, value in user_data.items(): - if hasattr(user_db, attr) and value is not None: - setattr(user_db, attr, value) - - user_db.updated_at = datetime.now(timezone.utc) - flag_modified(user_db, "updated_at") - - try: - db.commit() - except IntegrityError as e: - db.rollback() - raise HTTPException(status_code=400, detail=str(e)) from e - - return user_db - - -def update_user_last_login_at(user_id: UUID, db: Session = Depends(get_session)): - user_data = UserUpdate(last_login_at=datetime.now(timezone.utc)) # type: ignore - - return update_user(user_id, user_data, db) diff --git a/tests/conftest.py b/tests/conftest.py index e90d03d0a..9abe89d49 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,8 @@ from typing import AsyncGenerator, TYPE_CHECKING from langflow.api.v1.flows import get_session from langflow.graph.graph.base import Graph +from langflow.services.auth.utils import get_password_hash +from langflow.services.database.models.user.user import User, UserCreate import pytest from fastapi.testclient import TestClient from httpx import AsyncClient @@ -155,3 +157,38 @@ def session_getter_fixture(client): @pytest.fixture def runner(): return CliRunner() + + +@pytest.fixture +def test_user(client): + user_data = UserCreate( + username="testuser", + password="testpassword", + ) + response = client.post("/api/v1/user", json=user_data.dict()) + return response.json() + + +@pytest.fixture(scope="function") +def active_user(session): + user = User( + username="activeuser", + password=get_password_hash( + "testpassword" + ), # Assuming password needs to be hashed + is_active=True, + is_superuser=False, + ) + session.add(user) + session.commit() + return user + + +@pytest.fixture +def logged_in_headers(client, active_user): + login_data = {"username": active_user.username, "password": "testpassword"} + response = client.post("/api/v1/login", data=login_data) + assert response.status_code == 200 + tokens = response.json() + a_token = tokens["access_token"] + return {"Authorization": f"Bearer {a_token}"} diff --git a/tests/test_api_key.py b/tests/test_api_key.py new file mode 100644 index 000000000..43b91fa43 --- /dev/null +++ b/tests/test_api_key.py @@ -0,0 +1,50 @@ +import pytest +from langflow.services.database.models.api_key import ApiKeyCreate + + +@pytest.fixture +def api_key(client, logged_in_headers, active_user): + api_key = ApiKeyCreate(name="test-api-key") + + response = client.post( + "api/v1/api_key", data=api_key.json(), headers=logged_in_headers + ) + assert response.status_code == 200, response.text + return response.json() + + +def test_get_api_keys(client, logged_in_headers, api_key): + response = client.get("api/v1/api_key", headers=logged_in_headers) + assert response.status_code == 200, response.text + data = response.json() + assert "total_count" in data + assert "user_id" in data + assert "api_keys" in data + assert any("test-api-key" in api_key["name"] for api_key in data["api_keys"]) + # assert all api keys in data["api_keys"] are masked + assert all("**" in api_key["api_key"] for api_key in data["api_keys"]) + # Add more assertions as needed based on the expected data structure and content + + +def test_create_api_key(client, logged_in_headers): + api_key_name = "test-api-key" + response = client.post( + "api/v1/api_key", json={"name": api_key_name}, headers=logged_in_headers + ) + assert response.status_code == 200 + data = response.json() + assert "name" in data and data["name"] == api_key_name + assert "api_key" in data + # When creating the API key is returned which is + # the only time the API key is unmasked + assert "**" not in data["api_key"] + + +def test_delete_api_key(client, logged_in_headers, active_user, api_key): + # Assuming a function to create a test API key, returning the key ID + api_key_id = api_key["id"] + response = client.delete(f"api/v1/api_key/{api_key_id}", headers=logged_in_headers) + assert response.status_code == 200 + data = response.json() + assert data["detail"] == "API Key deleted" + # Optionally, add a follow-up check to ensure that the key is actually removed from the database diff --git a/tests/test_user.py b/tests/test_user.py index f8d4ff788..d734e4d61 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -4,42 +4,7 @@ from langflow.services.auth.utils import create_super_user, get_password_hash from langflow.services.database.models.user.user import User from langflow.services.utils import get_settings_manager import pytest -from langflow.services.database.models.user import UserCreate, UserUpdate - - -@pytest.fixture -def test_user(client): - user_data = UserCreate( - username="testuser", - password="testpassword", - ) - response = client.post("/api/v1/user", json=user_data.dict()) - return response.json() - - -@pytest.fixture(scope="function") -def active_user(session): - user = User( - username="activeuser", - password=get_password_hash( - "testpassword" - ), # Assuming password needs to be hashed - is_active=True, - is_superuser=False, - ) - session.add(user) - session.commit() - return user - - -@pytest.fixture -def logged_in_headers(client, active_user): - login_data = {"username": active_user.username, "password": "testpassword"} - response = client.post("/api/v1/login", data=login_data) - assert response.status_code == 200 - tokens = response.json() - a_token = tokens["access_token"] - return {"Authorization": f"Bearer {a_token}"} +from langflow.services.database.models.user import UserUpdate @pytest.fixture From a4aeff6d7966d07f7cca11bcc55b03090da51d38 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 25 Aug 2023 12:21:14 -0300 Subject: [PATCH 38/38] =?UTF-8?q?=F0=9F=94=A7=20fix(api=5Fkey.py):=20impor?= =?UTF-8?q?t=20missing=20dependencies=20and=20fix=20type=20annotations=20i?= =?UTF-8?q?n=20api=5Fkey.py=20=F0=9F=94=A7=20fix(api=5Fkey.py):=20fix=20ty?= =?UTF-8?q?pe=20annotations=20and=20import=20in=20api=5Fkey.py=20?= =?UTF-8?q?=F0=9F=94=A7=20fix(api=5Fkey.py):=20fix=20type=20annotations=20?= =?UTF-8?q?and?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/langflow/api/v1/api_key.py | 15 +++++++++------ .../services/database/models/api_key/api_key.py | 9 +++++---- .../services/database/models/api_key/crud.py | 7 ++++--- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/backend/langflow/api/v1/api_key.py b/src/backend/langflow/api/v1/api_key.py index e7b2dd163..df2d3e420 100644 --- a/src/backend/langflow/api/v1/api_key.py +++ b/src/backend/langflow/api/v1/api_key.py @@ -1,7 +1,11 @@ +from uuid import UUID from fastapi import APIRouter, HTTPException, Depends -from langflow.api.v1.schemas import ApiKeysResponse, CreateApiKeyRequest +from langflow.api.v1.schemas import ApiKeysResponse from langflow.services.auth.utils import get_current_active_user -from langflow.services.database.models.api_key.api_key import UnmaskedApiKeyRead +from langflow.services.database.models.api_key.api_key import ( + ApiKeyCreate, + UnmaskedApiKeyRead, +) # Assuming you have these methods in your service layer from langflow.services.database.models.api_key.crud import ( @@ -26,15 +30,14 @@ def get_api_keys_route( user_id = current_user.id keys = get_api_keys(db, user_id) - result = {"total_count": len(keys), "user_id": user_id, "api_keys": keys} - return ApiKeysResponse(**result) + return ApiKeysResponse(total_count=len(keys), user_id=user_id, api_keys=keys) except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/api_key", response_model=UnmaskedApiKeyRead) def create_api_key_route( - req: CreateApiKeyRequest, + req: ApiKeyCreate, current_user: User = Depends(get_current_active_user), db: Session = Depends(get_session), ): @@ -47,7 +50,7 @@ def create_api_key_route( @router.delete("/api_key/{api_key_id}") def delete_api_key_route( - api_key_id: str, + api_key_id: UUID, current_user=Depends(get_current_active_user), db: Session = Depends(get_session), ): diff --git a/src/backend/langflow/services/database/models/api_key/api_key.py b/src/backend/langflow/services/database/models/api_key/api_key.py index 1006b1c0f..601d060b5 100644 --- a/src/backend/langflow/services/database/models/api_key/api_key.py +++ b/src/backend/langflow/services/database/models/api_key/api_key.py @@ -10,15 +10,14 @@ if TYPE_CHECKING: class ApiKeyBase(SQLModelSerializable): - api_key: str = Field(index=True, unique=True) name: Optional[str] = Field(index=True) created_at: datetime = Field(default_factory=datetime.utcnow) last_used_at: Optional[datetime] = Field(default=None) - user_id: UUID = Field() class ApiKey(ApiKeyBase, table=True): id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True) + api_key: str = Field(index=True, unique=True) # User relationship user_id: UUID = Field(index=True, foreign_key="user.id") user: "User" = Relationship(back_populates="api_keys") @@ -31,12 +30,14 @@ class ApiKeyCreate(ApiKeyBase): class UnmaskedApiKeyRead(ApiKeyBase): id: UUID + api_key: str = Field(index=True, unique=True) + user_id: UUID = Field() class ApiKeyRead(ApiKeyBase): id: UUID - api_key: Optional[str] = None - user_id: Optional[UUID] = None + api_key: str = Field(index=True, unique=True) + user_id: UUID = Field() @validator("api_key", always=True) def mask_api_key(cls, v): diff --git a/src/backend/langflow/services/database/models/api_key/crud.py b/src/backend/langflow/services/database/models/api_key/crud.py index 933e7c2b3..af697b6d5 100644 --- a/src/backend/langflow/services/database/models/api_key/crud.py +++ b/src/backend/langflow/services/database/models/api_key/crud.py @@ -11,14 +11,14 @@ from langflow.services.database.models.api_key import ( ) -def get_api_keys(session: Session, user_id: str) -> List[UnmaskedApiKeyRead]: +def get_api_keys(session: Session, user_id: UUID) -> List[ApiKeyRead]: query = select(ApiKey).where(ApiKey.user_id == user_id) api_keys = session.exec(query).all() return [ApiKeyRead.from_orm(api_key) for api_key in api_keys] def create_api_key( - session: Session, api_key_create: ApiKeyCreate, user_id: str + session: Session, api_key_create: ApiKeyCreate, user_id: UUID ) -> UnmaskedApiKeyRead: # Generate a random API key with 32 bytes of randomness generated_api_key = secrets.token_urlsafe(32) @@ -26,7 +26,8 @@ def create_api_key( # hash the API key hashed_api_key = get_password_hash(generated_api_key) # Use the generated key to create the ApiKey object - api_key = ApiKey(api_key=hashed_api_key, **api_key_create.dict(), user_id=user_id) + + api_key = ApiKey(api_key=hashed_api_key, name=api_key_create.name, user_id=user_id) session.add(api_key) session.commit()