From 98d99ded372ac956361457a0acf4a1c03dd12e59 Mon Sep 17 00:00:00 2001 From: qixinbo Date: Sat, 14 Mar 2026 19:20:37 +0800 Subject: [PATCH] add user management system --- backend/app/api/users.py | 133 +++++++++++ backend/app/core/security.py | 26 +++ backend/app/database.py | 19 ++ backend/app/models/user.py | 14 ++ backend/app/schemas/user.py | 25 ++ backend/dataclaw.db | Bin 0 -> 20480 bytes backend/main.py | 3 +- backend/pyproject.toml | 3 + backend/uv.lock | 137 +++++++++++ frontend/src/App.tsx | 86 +++++-- frontend/src/components/ChatInterface.tsx | 2 +- frontend/src/components/Sidebar.tsx | 89 +++++++- frontend/src/pages/Login.tsx | 164 +++++++++++++ frontend/src/pages/Users.tsx | 266 ++++++++++++++++++++++ frontend/src/store/authStore.ts | 32 +++ 15 files changed, 976 insertions(+), 23 deletions(-) create mode 100644 backend/app/api/users.py create mode 100644 backend/app/core/security.py create mode 100644 backend/app/database.py create mode 100644 backend/app/models/user.py create mode 100644 backend/app/schemas/user.py create mode 100644 backend/dataclaw.db create mode 100644 frontend/src/pages/Login.tsx create mode 100644 frontend/src/pages/Users.tsx create mode 100644 frontend/src/store/authStore.ts diff --git a/backend/app/api/users.py b/backend/app/api/users.py new file mode 100644 index 0000000..fa6d711 --- /dev/null +++ b/backend/app/api/users.py @@ -0,0 +1,133 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordRequestForm +from sqlalchemy.orm import Session +from typing import List + +from app.database import get_db, engine, Base +from app.models.user import User +from app.schemas.user import UserCreate, UserUpdate, UserResponse +from app.core.security import get_password_hash, verify_password, create_access_token, ACCESS_TOKEN_EXPIRE_MINUTES +from datetime import timedelta + +# Create tables +Base.metadata.create_all(bind=engine) + +router = APIRouter() + +@router.post("/auth/login") +def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): + user = db.query(User).filter(User.username == form_data.username).first() + if not user or not verify_password(form_data.password, user.hashed_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + if not user.is_active: + raise HTTPException(status_code=400, detail="Inactive user") + + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username, "is_admin": user.is_admin, "id": user.id}, + expires_delta=access_token_expires + ) + + return { + "access_token": access_token, + "token_type": "bearer", + "user": { + "id": user.id, + "username": user.username, + "email": user.email, + "is_admin": user.is_admin + } + } + +@router.post("/auth/register", response_model=UserResponse) +def register_user(user: UserCreate, db: Session = Depends(get_db)): + db_user = db.query(User).filter(User.username == user.username).first() + if db_user: + raise HTTPException(status_code=400, detail="Username already registered") + + db_user_email = db.query(User).filter(User.email == user.email).first() + if db_user_email: + raise HTTPException(status_code=400, detail="Email already registered") + + hashed_password = get_password_hash(user.password) + + # If this is the first user, make them an admin + is_first_user = db.query(User).count() == 0 + + db_user = User( + username=user.username, + email=user.email, + hashed_password=hashed_password, + is_active=True, + is_admin=is_first_user or user.is_admin + ) + db.add(db_user) + db.commit() + db.refresh(db_user) + return db_user + +@router.get("/users", response_model=List[UserResponse]) +def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + users = db.query(User).offset(skip).limit(limit).all() + return users + +@router.get("/users/{user_id}", response_model=UserResponse) +def read_user(user_id: int, db: Session = Depends(get_db)): + db_user = db.query(User).filter(User.id == user_id).first() + if db_user is None: + raise HTTPException(status_code=404, detail="User not found") + return db_user + +@router.post("/users", response_model=UserResponse) +def create_user(user: UserCreate, db: Session = Depends(get_db)): + db_user = db.query(User).filter(User.username == user.username).first() + if db_user: + raise HTTPException(status_code=400, detail="Username already registered") + + db_user_email = db.query(User).filter(User.email == user.email).first() + if db_user_email: + raise HTTPException(status_code=400, detail="Email already registered") + + db_user = User( + username=user.username, + email=user.email, + hashed_password=get_password_hash(user.password), + is_active=user.is_active, + is_admin=user.is_admin + ) + db.add(db_user) + db.commit() + db.refresh(db_user) + return db_user + +@router.put("/users/{user_id}", response_model=UserResponse) +def update_user(user_id: int, user: UserUpdate, db: Session = Depends(get_db)): + db_user = db.query(User).filter(User.id == user_id).first() + if not db_user: + raise HTTPException(status_code=404, detail="User not found") + + update_data = user.model_dump(exclude_unset=True) + for key, value in update_data.items(): + if key == "password" and value: + db_user.hashed_password = get_password_hash(value) + elif key != "password": + setattr(db_user, key, value) + + db.commit() + db.refresh(db_user) + return db_user + +@router.delete("/users/{user_id}") +def delete_user(user_id: int, db: Session = Depends(get_db)): + db_user = db.query(User).filter(User.id == user_id).first() + if not db_user: + raise HTTPException(status_code=404, detail="User not found") + + db.delete(db_user) + db.commit() + return {"ok": True} diff --git a/backend/app/core/security.py b/backend/app/core/security.py new file mode 100644 index 0000000..4e075c2 --- /dev/null +++ b/backend/app/core/security.py @@ -0,0 +1,26 @@ +from datetime import datetime, timedelta +from typing import Optional +from jose import jwt +from passlib.context import CryptContext + +SECRET_KEY = "your-super-secret-key-for-dataclaw" # In production, use env variable +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 * 24 * 60 # 30 days + +pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto") + +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_access_token(data: dict, expires_delta: Optional[timedelta] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt diff --git a/backend/app/database.py b/backend/app/database.py new file mode 100644 index 0000000..9154f4a --- /dev/null +++ b/backend/app/database.py @@ -0,0 +1,19 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +SQLALCHEMY_DATABASE_URL = "sqlite:///./dataclaw.db" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/app/models/user.py b/backend/app/models/user.py new file mode 100644 index 0000000..b49cee6 --- /dev/null +++ b/backend/app/models/user.py @@ -0,0 +1,14 @@ +from sqlalchemy import Column, Integer, String, Boolean, DateTime +from sqlalchemy.sql import func +from app.database import Base + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + username = Column(String, unique=True, index=True, nullable=False) + email = Column(String, unique=True, index=True, nullable=False) + hashed_password = Column(String, nullable=False) + is_active = Column(Boolean, default=True) + is_admin = Column(Boolean, default=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py new file mode 100644 index 0000000..15587b8 --- /dev/null +++ b/backend/app/schemas/user.py @@ -0,0 +1,25 @@ +from pydantic import BaseModel, ConfigDict +from typing import Optional +from datetime import datetime + +class UserBase(BaseModel): + username: str + email: str + is_active: Optional[bool] = True + is_admin: Optional[bool] = False + +class UserCreate(UserBase): + password: str + +class UserUpdate(BaseModel): + username: Optional[str] = None + email: Optional[str] = None + is_active: Optional[bool] = None + is_admin: Optional[bool] = None + password: Optional[str] = None + +class UserResponse(UserBase): + id: int + created_at: datetime + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/dataclaw.db b/backend/dataclaw.db new file mode 100644 index 0000000000000000000000000000000000000000..a421a5e705dc756ecd4d4bcf5ea5f3e0739e7067 GIT binary patch literal 20480 zcmeI(&u`jR8~|_|lPpjL?^1bE4}DTO6lnn)1A*)k17_xUw=5gXZg6>#J-E3Pv#@b zMqkiJFwxCMYp*WM|*hexJ$h3U3%`^>$xKf55sV)qREmjBSTfXhKv-o zDxad;>rh)o;ac=G{9dZeY;#XI+Ps|;JK>_}O~N~wu(Oc{JE zZkUPrWNGWA8)?^5sh=LuUvV5q|9t6N{o&u)!jzGll)6-I%Ilko$4GICs>|QY8dBRj zQjKQwTNID2^Pv+-s~kzUFNPHNUOBhCbJyujEzes_=FYuqu-CI}AHQ@_x!rEck{bM8 zFzJl&cyrF4yOvL~k)3;${7<^ll2KKzNk&sg6+_cxRqq9p$GX(&1W7uY(vmb2HDnV# zCD8HIE_*>m5~oz+ed3f1&_DnLKmY_l00ck)1V8`;KmY_l;9m(`$C()QP`v&w78C8E zwFvs5WBFDPq=ysR8YZXx3+H)u&pWrW`9d;V;(0#lw$k|;Usq-)M#DT_)E4DIbs;7F zMdRG%>+`CW(d_v|@)XrGDrc)w(aQ}pZok)HlCo0hb~3%_H!X6|a)86@l0m58;afAVa2b(E>6yN+}wsg-6nZ(b;w zBdO@~`n-11D0=3dqJ)E7A>Cn70|5{K0T2KI5C8!X009sH0T2KI j5cn(wI7SSg^51#8g4tyr(;we*2=@BlY&`b=_@=>M@e@r- literal 0 HcmV?d00001 diff --git a/backend/main.py b/backend/main.py index f8b40c2..ce45cb8 100644 --- a/backend/main.py +++ b/backend/main.py @@ -4,7 +4,7 @@ from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel import asyncio -from app.api import upload, llm, skills +from app.api import upload, llm, skills, users from app.connectors.postgres import postgres_connector from app.connectors.clickhouse import clickhouse_connector from app.connectors.minio import minio_connector @@ -24,6 +24,7 @@ app.add_middleware( app.include_router(upload.router, prefix="/api/v1") app.include_router(llm.router, prefix="/api/v1") app.include_router(skills.router, prefix="/api/v1") +app.include_router(users.router, prefix="/api/v1") @app.on_event("startup") async def startup_event(): diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 271da2d..d468603 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -5,6 +5,7 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.11" dependencies = [ + "bcrypt>=5.0.0", "chardet>=3.0.2,<6.0.0", "clickhouse-driver>=0.2.10", "croniter>=6.0.0,<7.0.0", @@ -23,10 +24,12 @@ dependencies = [ "oauth-cli-kit>=0.1.3,<1.0.0", "openai>=2.8.0", "pandas>=3.0.1", + "passlib>=1.7.4", "prompt-toolkit>=3.0.50,<4.0.0", "psycopg2-binary>=2.9.11", "pydantic>=2.12.0,<3.0.0", "pydantic-settings>=2.12.0,<3.0.0", + "python-jose[cryptography]>=3.5.0", "python-multipart>=0.0.22", "python-socketio>=5.16.0,<6.0.0", "python-socks[asyncio]>=2.8.0,<3.0.0", diff --git a/backend/uv.lock b/backend/uv.lock index ab07c99..63c33d3 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -234,6 +234,7 @@ name = "backend" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "bcrypt" }, { name = "chardet" }, { name = "clickhouse-driver" }, { name = "croniter" }, @@ -252,10 +253,12 @@ dependencies = [ { name = "oauth-cli-kit" }, { name = "openai" }, { name = "pandas" }, + { name = "passlib" }, { name = "prompt-toolkit" }, { name = "psycopg2-binary" }, { name = "pydantic" }, { name = "pydantic-settings" }, + { name = "python-jose", extra = ["cryptography"] }, { name = "python-multipart" }, { name = "python-socketio" }, { name = "python-socks" }, @@ -275,6 +278,7 @@ dependencies = [ [package.metadata] requires-dist = [ + { name = "bcrypt", specifier = ">=5.0.0" }, { name = "chardet", specifier = ">=3.0.2,<6.0.0" }, { name = "clickhouse-driver", specifier = ">=0.2.10" }, { name = "croniter", specifier = ">=6.0.0,<7.0.0" }, @@ -293,10 +297,12 @@ requires-dist = [ { name = "oauth-cli-kit", specifier = ">=0.1.3,<1.0.0" }, { name = "openai", specifier = ">=2.8.0" }, { name = "pandas", specifier = ">=3.0.1" }, + { name = "passlib", specifier = ">=1.7.4" }, { name = "prompt-toolkit", specifier = ">=3.0.50,<4.0.0" }, { name = "psycopg2-binary", specifier = ">=2.9.11" }, { name = "pydantic", specifier = ">=2.12.0,<3.0.0" }, { name = "pydantic-settings", specifier = ">=2.12.0,<3.0.0" }, + { name = "python-jose", extras = ["cryptography"], specifier = ">=3.5.0" }, { name = "python-multipart", specifier = ">=0.0.22" }, { name = "python-socketio", specifier = ">=5.16.0,<6.0.0" }, { name = "python-socks", extras = ["asyncio"], specifier = ">=2.8.0,<3.0.0" }, @@ -314,6 +320,76 @@ requires-dist = [ { name = "websockets", specifier = ">=16.0,<17.0" }, ] +[[package]] +name = "bcrypt" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386, upload-time = "2025-09-25T19:50:47.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/85/3e65e01985fddf25b64ca67275bb5bdb4040bd1a53b66d355c6c37c8a680/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", size = 481806, upload-time = "2025-09-25T19:49:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", size = 268626, upload-time = "2025-09-25T19:49:06.723Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", size = 271853, upload-time = "2025-09-25T19:49:08.028Z" }, + { url = "https://files.pythonhosted.org/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", size = 269793, upload-time = "2025-09-25T19:49:09.727Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", size = 289930, upload-time = "2025-09-25T19:49:11.204Z" }, + { url = "https://files.pythonhosted.org/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", size = 272194, upload-time = "2025-09-25T19:49:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", size = 269381, upload-time = "2025-09-25T19:49:14.308Z" }, + { url = "https://files.pythonhosted.org/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", size = 271750, upload-time = "2025-09-25T19:49:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", size = 303757, upload-time = "2025-09-25T19:49:17.244Z" }, + { url = "https://files.pythonhosted.org/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", size = 306740, upload-time = "2025-09-25T19:49:18.693Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", size = 334197, upload-time = "2025-09-25T19:49:20.523Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", size = 352974, upload-time = "2025-09-25T19:49:22.254Z" }, + { url = "https://files.pythonhosted.org/packages/10/a6/ffb49d4254ed085e62e3e5dd05982b4393e32fe1e49bb1130186617c29cd/bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", size = 148498, upload-time = "2025-09-25T19:49:24.134Z" }, + { url = "https://files.pythonhosted.org/packages/48/a9/259559edc85258b6d5fc5471a62a3299a6aa37a6611a169756bf4689323c/bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", size = 145853, upload-time = "2025-09-25T19:49:25.702Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/9714173403c7e8b245acf8e4be8876aac64a209d1b392af457c79e60492e/bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", size = 139626, upload-time = "2025-09-25T19:49:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862, upload-time = "2025-09-25T19:49:28.365Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544, upload-time = "2025-09-25T19:49:30.39Z" }, + { url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787, upload-time = "2025-09-25T19:49:32.144Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753, upload-time = "2025-09-25T19:49:33.885Z" }, + { url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587, upload-time = "2025-09-25T19:49:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178, upload-time = "2025-09-25T19:49:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295, upload-time = "2025-09-25T19:49:38.164Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700, upload-time = "2025-09-25T19:49:39.917Z" }, + { url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034, upload-time = "2025-09-25T19:49:41.227Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766, upload-time = "2025-09-25T19:49:43.08Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d4/1a388d21ee66876f27d1a1f41287897d0c0f1712ef97d395d708ba93004c/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", size = 152449, upload-time = "2025-09-25T19:49:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/3f/61/3291c2243ae0229e5bca5d19f4032cecad5dfb05a2557169d3a69dc0ba91/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", size = 149310, upload-time = "2025-09-25T19:49:46.162Z" }, + { url = "https://files.pythonhosted.org/packages/3e/89/4b01c52ae0c1a681d4021e5dd3e45b111a8fb47254a274fa9a378d8d834b/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", size = 143761, upload-time = "2025-09-25T19:49:47.345Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553, upload-time = "2025-09-25T19:49:49.006Z" }, + { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009, upload-time = "2025-09-25T19:49:50.581Z" }, + { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029, upload-time = "2025-09-25T19:49:52.533Z" }, + { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907, upload-time = "2025-09-25T19:49:54.709Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500, upload-time = "2025-09-25T19:49:56.013Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412, upload-time = "2025-09-25T19:49:57.356Z" }, + { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486, upload-time = "2025-09-25T19:49:59.116Z" }, + { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940, upload-time = "2025-09-25T19:50:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776, upload-time = "2025-09-25T19:50:02.393Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922, upload-time = "2025-09-25T19:50:04.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367, upload-time = "2025-09-25T19:50:05.559Z" }, + { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187, upload-time = "2025-09-25T19:50:06.916Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752, upload-time = "2025-09-25T19:50:08.515Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881, upload-time = "2025-09-25T19:50:09.742Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931, upload-time = "2025-09-25T19:50:11.016Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313, upload-time = "2025-09-25T19:50:12.309Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290, upload-time = "2025-09-25T19:50:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253, upload-time = "2025-09-25T19:50:15.089Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084, upload-time = "2025-09-25T19:50:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185, upload-time = "2025-09-25T19:50:18.525Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656, upload-time = "2025-09-25T19:50:19.809Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662, upload-time = "2025-09-25T19:50:21.567Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240, upload-time = "2025-09-25T19:50:23.305Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152, upload-time = "2025-09-25T19:50:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284, upload-time = "2025-09-25T19:50:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643, upload-time = "2025-09-25T19:50:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698, upload-time = "2025-09-25T19:50:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725, upload-time = "2025-09-25T19:50:34.384Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912, upload-time = "2025-09-25T19:50:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/75/4aa9f5a4d40d762892066ba1046000b329c7cd58e888a6db878019b282dc/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534", size = 271180, upload-time = "2025-09-25T19:50:38.575Z" }, + { url = "https://files.pythonhosted.org/packages/54/79/875f9558179573d40a9cc743038ac2bf67dfb79cecb1e8b5d70e88c94c3d/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4", size = 273791, upload-time = "2025-09-25T19:50:39.913Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fe/975adb8c216174bf70fc17535f75e85ac06ed5252ea077be10d9cff5ce24/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911", size = 270746, upload-time = "2025-09-25T19:50:43.306Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f8/972c96f5a2b6c4b3deca57009d93e946bbdbe2241dca9806d502f29dd3ee/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4", size = 273375, upload-time = "2025-09-25T19:50:45.43Z" }, +] + [[package]] name = "bidict" version = "0.23.1" @@ -708,6 +784,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/aa/f14dd5e241ec80d9f9d82196ca65e0c53badfc8a7a619d5497c5626657ad/duckdb-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:d6d2858c734d1a7e7a1b6e9b8403b3fce26dfefb4e0a2479c420fba6cd36db36", size = 14341879, upload-time = "2026-03-09T12:50:22.347Z" }, ] +[[package]] +name = "ecdsa" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/1f/924e3caae75f471eae4b26bd13b698f6af2c44279f67af317439c2f4c46a/ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61", size = 201793, upload-time = "2025-03-13T11:52:43.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3", size = 150607, upload-time = "2025-03-13T11:52:41.757Z" }, +] + [[package]] name = "fastapi" version = "0.135.1" @@ -1939,6 +2027,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/b0/34937815889fa982613775e4b97fddd13250f11012d769949c5465af2150/pandas-3.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:108dd1790337a494aa80e38def654ca3f0968cf4f362c85f44c15e471667102d", size = 9452085, upload-time = "2026-02-17T22:20:14.331Z" }, ] +[[package]] +name = "passlib" +version = "1.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" }, +] + [[package]] name = "platformdirs" version = "4.9.4" @@ -2111,6 +2208,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, ] +[[package]] +name = "pyasn1" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -2332,6 +2438,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl", hash = "sha256:f32ad10589859c11053ad7d9bb3c9695cdf862113bfb0d20bc4d890198287399", size = 59847, upload-time = "2026-02-06T23:38:04.861Z" }, ] +[[package]] +name = "python-jose" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ecdsa" }, + { name = "pyasn1" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/77/3a1c9039db7124eb039772b935f2244fbb73fc8ee65b9acf2375da1c07bf/python_jose-3.5.0.tar.gz", hash = "sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b", size = 92726, upload-time = "2025-05-28T17:31:54.288Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771", size = 34624, upload-time = "2025-05-28T17:31:52.802Z" }, +] + +[package.optional-dependencies] +cryptography = [ + { name = "cryptography" }, +] + [[package]] name = "python-multipart" version = "0.0.22" @@ -2758,6 +2883,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, ] +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + [[package]] name = "shellingham" version = "1.5.4" diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index acf8ea6..92660c0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,28 +1,88 @@ -import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import { Sidebar } from "./components/Sidebar"; import { ChatInterface } from "./components/ChatInterface"; import { Dashboard } from "./pages/Dashboard"; import { Skills } from "./pages/Skills"; import { Settings } from "./pages/Settings"; +import { Users } from "./pages/Users"; +import { Login } from "./pages/Login"; +import { useAuthStore } from "./store/authStore"; + +// Protected Route Component +function ProtectedRoute({ children, requireAdmin = false }: { children: React.ReactNode, requireAdmin?: boolean }) { + const { isAuthenticated, user } = useAuthStore(); + + if (!isAuthenticated) { + return ; + } + + if (requireAdmin && !user?.is_admin) { + return ; + } + + return <>{children}; +} + +function MainLayout({ children }: { children: React.ReactNode }) { + return ( +
+ +
+ {children} +
+
+ ); +} function App() { return ( -
- -
- - + } /> + + {/* Protected Routes */} + +
- } /> - } /> - } /> - } /> -
-
-
+ + + } /> + + + + + + + } /> + + + + + + + } /> + + + + + + + } /> + + + + + + + } /> +
); } diff --git a/frontend/src/components/ChatInterface.tsx b/frontend/src/components/ChatInterface.tsx index 3196a9e..d6e25c0 100644 --- a/frontend/src/components/ChatInterface.tsx +++ b/frontend/src/components/ChatInterface.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { User, Loader2, Sparkles, Search, ArrowUp, ChevronDown, Database, Table, Paperclip, Bot } from "lucide-react"; +import { User, Loader2, Sparkles, Search, ArrowUp, ChevronDown, Table, Paperclip } from "lucide-react"; import { api } from "@/lib/api"; import { useVisualizationStore } from "@/store/visualizationStore"; diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index c3c028f..e4b4b69 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1,8 +1,10 @@ import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { Menu, LayoutDashboard, Plus, MoreVertical, User, MessageSquare, Search, PanelLeftClose, Wrench, Bot } from "lucide-react"; -import { useState } from "react"; +import { Menu, LayoutDashboard, Plus, MoreVertical, User, Search, Wrench, Settings } from "lucide-react"; +import { useState, useRef, useEffect } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { useAuthStore } from "@/store/authStore"; const threadItems = ["我叫", "年龄最大的是谁", "有哪些字段", "文件中有些什么字段"]; @@ -39,16 +41,36 @@ function Section({ } function SidebarBody() { + const navigate = useNavigate(); + const { user, logout } = useAuthStore(); + const [showUserMenu, setShowUserMenu] = useState(false); + const menuRef = useRef(null); + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setShowUserMenu(false); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const handleLogout = () => { + logout(); + navigate("/login"); + }; + return ( -
+
{/* Header */}
-
+ 🦞 龙虾问数 -
+
+ + {/* User Settings Popover Menu */} + {showUserMenu && ( +
+
+

{user?.username}

+

{user?.email}

+
+ + + + {user?.is_admin && ( + + )} + +
+ + +
+ )}
); diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx new file mode 100644 index 0000000..0d822a7 --- /dev/null +++ b/frontend/src/pages/Login.tsx @@ -0,0 +1,164 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Loader2 } from "lucide-react"; +import { api } from "@/lib/api"; +import { useAuthStore } from "@/store/authStore"; + +export function Login() { + const [isLogin, setIsLogin] = useState(true); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(""); + const navigate = useNavigate(); + const login = useAuthStore((state) => state.login); + + const [formData, setFormData] = useState({ + username: "", + email: "", + password: "", + }); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + setIsLoading(true); + + try { + if (isLogin) { + // Handle Login using application/x-www-form-urlencoded as required by OAuth2PasswordRequestForm + const params = new URLSearchParams(); + params.append("username", formData.username); + params.append("password", formData.password); + + const response = await fetch("/api/v1/auth/login", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: params, + }); + + if (!response.ok) { + const errData = await response.json(); + throw new Error(errData.detail || "Login failed"); + } + + const data = await response.json(); + login(data.user, data.access_token); + navigate("/"); + } else { + // Handle Registration + await api.post("/api/v1/auth/register", { + username: formData.username, + email: formData.email, + password: formData.password, + }); + + // Auto login after successful registration + setIsLogin(true); + setError("Registration successful! Please login."); + } + } catch (err: any) { + setError(err.message || "An error occurred"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+
+ 🦞 +
+

+ DataClaw +

+
+ +
+

+ {isLogin ? "Welcome Back" : "Create Account"} +

+ + {error && ( +
+ {error} +
+ )} + +
+
+ + setFormData({ ...formData, username: e.target.value })} + required + className="h-11" + /> +
+ + {!isLogin && ( +
+ + setFormData({ ...formData, email: e.target.value })} + required + className="h-11" + /> +
+ )} + +
+ + setFormData({ ...formData, password: e.target.value })} + required + className="h-11" + /> +
+ + +
+ +
+ {isLogin ? "Don't have an account?" : "Already have an account?"} + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/Users.tsx b/frontend/src/pages/Users.tsx new file mode 100644 index 0000000..a20e3e3 --- /dev/null +++ b/frontend/src/pages/Users.tsx @@ -0,0 +1,266 @@ +import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from "@/components/ui/dialog"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Pencil, Trash2, Plus, Users as UsersIcon, Loader2 } from "lucide-react"; +import { api } from "@/lib/api"; + +interface User { + id: number; + username: string; + email: string; + is_active: boolean; + is_admin: boolean; + created_at: string; +} + +export function Users() { + const [users, setUsers] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [editingUser, setEditingUser] = useState(null); + const [formData, setFormData] = useState({ + username: "", + email: "", + password: "", + is_active: true, + is_admin: false, + }); + const [error, setError] = useState(""); + + const fetchUsers = async () => { + try { + setIsLoading(true); + const data = await api.get("/api/v1/users"); + setUsers(data); + } catch (err) { + console.error("Failed to fetch users", err); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchUsers(); + }, []); + + const handleOpenDialog = (user?: User) => { + setError(""); + if (user) { + setEditingUser(user); + setFormData({ + username: user.username, + email: user.email, + password: "", // Don't show password + is_active: user.is_active, + is_admin: user.is_admin, + }); + } else { + setEditingUser(null); + setFormData({ + username: "", + email: "", + password: "", + is_active: true, + is_admin: false, + }); + } + setIsDialogOpen(true); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + + try { + if (editingUser) { + // Update + await api.put(`/api/v1/users/${editingUser.id}`, { + username: formData.username, + email: formData.email, + is_active: formData.is_active, + is_admin: formData.is_admin, + }); + } else { + // Create + if (!formData.password) { + setError("Password is required for new users"); + return; + } + await api.post("/api/v1/users", formData); + } + setIsDialogOpen(false); + fetchUsers(); + } catch (err: any) { + setError(err.message || "An error occurred"); + } + }; + + const handleDelete = async (id: number) => { + if (window.confirm("Are you sure you want to delete this user?")) { + try { + await api.delete(`/api/v1/users/${id}`); + fetchUsers(); + } catch (err) { + console.error("Failed to delete user", err); + } + } + }; + + return ( +
+
+
+ + User Management +
+ + handleOpenDialog()}> + + Add User + + +
+ + {editingUser ? "Edit User" : "Add New User"} + +
+ {error &&
{error}
} +
+ + setFormData({ ...formData, username: e.target.value })} + required + /> +
+
+ + setFormData({ ...formData, email: e.target.value })} + required + /> +
+ {!editingUser && ( +
+ + setFormData({ ...formData, password: e.target.value })} + required + /> +
+ )} +
+ + setFormData({ ...formData, is_active: checked })} + /> +
+
+ + setFormData({ ...formData, is_admin: checked })} + /> +
+
+ + + + +
+
+
+
+ +
+
+ {isLoading ? ( +
+ +
+ ) : ( + + + + ID + Username + Email + Status + Role + Created At + Actions + + + + {users.length === 0 ? ( + + + No users found. + + + ) : ( + users.map((user) => ( + + {user.id} + {user.username} + {user.email} + + + {user.is_active ? 'Active' : 'Inactive'} + + + + + {user.is_admin ? 'Admin' : 'User'} + + + + {new Date(user.created_at).toLocaleDateString()} + + + + + + + )) + )} + +
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/store/authStore.ts b/frontend/src/store/authStore.ts new file mode 100644 index 0000000..d2508f1 --- /dev/null +++ b/frontend/src/store/authStore.ts @@ -0,0 +1,32 @@ +import { create } from 'zustand'; + +export interface User { + id: number; + username: string; + email: string; + is_admin: boolean; +} + +interface AuthState { + user: User | null; + token: string | null; + isAuthenticated: boolean; + login: (user: User, token: string) => void; + logout: () => void; +} + +export const useAuthStore = create((set) => ({ + user: JSON.parse(localStorage.getItem('user') || 'null'), + token: localStorage.getItem('token'), + isAuthenticated: !!localStorage.getItem('token'), + login: (user, token) => { + localStorage.setItem('user', JSON.stringify(user)); + localStorage.setItem('token', token); + set({ user, token, isAuthenticated: true }); + }, + logout: () => { + localStorage.removeItem('user'); + localStorage.removeItem('token'); + set({ user: null, token: null, isAuthenticated: false }); + }, +})); \ No newline at end of file