Spaces:
Running
Running
Delete app
Browse files- app/.env.example +0 -6
- app/Dockerfile +0 -22
- app/Procfile +0 -1
- app/app.py +0 -28
- app/backend/__init__.py +0 -0
- app/backend/app.py +0 -36
- app/backend/core/config.py +0 -14
- app/backend/core/security.py +0 -40
- app/backend/database.py +0 -16
- app/backend/migrations/alembic.ini +0 -3
- app/backend/migrations/env.py +0 -23
- app/backend/migrations/versions/0001_init.py +0 -37
- app/backend/models.py +0 -34
- app/backend/routes/about.py +0 -17
- app/backend/routes/apikey.py +0 -32
- app/backend/routes/auth.py +0 -27
- app/backend/routes/chat.py +0 -28
- app/backend/schemas.py +0 -45
- app/frontend/index.html +0 -1
- app/frontend/package.json +0 -24
- app/frontend/postcss.config.js +0 -1
- app/frontend/src/App.tsx +0 -23
- app/frontend/src/components/ChatBox.tsx +0 -8
- app/frontend/src/components/Loader.tsx +0 -1
- app/frontend/src/components/Onboarding.tsx +0 -2
- app/frontend/src/components/Sidebar.tsx +0 -2
- app/frontend/src/main.tsx +0 -12
- app/frontend/src/pages/About.tsx +0 -2
- app/frontend/src/pages/ApiKeys.tsx +0 -3
- app/frontend/src/pages/Chat.tsx +0 -4
- app/frontend/src/pages/HowToUse.tsx +0 -2
- app/frontend/src/pages/Login.tsx +0 -3
- app/frontend/src/pages/NotFound.tsx +0 -1
- app/frontend/src/pages/Signup.tsx +0 -3
- app/frontend/src/styles.css +0 -1
- app/frontend/tailwind.config.js +0 -1
- app/frontend/vite.config.js +0 -3
- app/requirements.txt +0 -10
app/.env.example
DELETED
@@ -1,6 +0,0 @@
|
|
1 |
-
# Backend config
|
2 |
-
DATABASE_URL=sqlite:///./app.db
|
3 |
-
JWT_SECRET=your_super_secret_key_here
|
4 |
-
JWT_EXPIRES_MINUTES=60
|
5 |
-
APP_NAME=CHB App
|
6 |
-
CORS_ORIGINS=*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/Dockerfile
DELETED
@@ -1,22 +0,0 @@
|
|
1 |
-
# Build frontend (React + Vite + Tailwind)
|
2 |
-
FROM node:20-alpine AS frontend
|
3 |
-
WORKDIR /ui
|
4 |
-
COPY frontend/package.json frontend/package-lock.json ./
|
5 |
-
RUN npm ci --silent
|
6 |
-
COPY frontend ./
|
7 |
-
RUN npm run build
|
8 |
-
|
9 |
-
# Backend image
|
10 |
-
FROM python:3.11-slim
|
11 |
-
ENV PYTHONDONTWRITEBYTECODE=1
|
12 |
-
ENV PYTHONUNBUFFERED=1
|
13 |
-
WORKDIR /app
|
14 |
-
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates git && rm -rf /var/lib/apt/lists/*
|
15 |
-
COPY requirements.txt ./
|
16 |
-
RUN pip install --no-cache-dir -r requirements.txt
|
17 |
-
COPY backend ./backend
|
18 |
-
COPY app.py Procfile .env.example ./
|
19 |
-
# Bring built frontend
|
20 |
-
COPY --from=frontend /ui/dist ./frontend_dist
|
21 |
-
EXPOSE 7860
|
22 |
-
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/Procfile
DELETED
@@ -1 +0,0 @@
|
|
1 |
-
web: uvicorn app:app --host 0.0.0.0 --port 7860
|
|
|
|
app/app.py
DELETED
@@ -1,28 +0,0 @@
|
|
1 |
-
import os
|
2 |
-
from fastapi import FastAPI
|
3 |
-
from fastapi.staticfiles import StaticFiles
|
4 |
-
from fastapi.responses import FileResponse, JSONResponse
|
5 |
-
from backend.app import create_app, run_migrations
|
6 |
-
|
7 |
-
app: FastAPI = create_app()
|
8 |
-
|
9 |
-
@app.on_event("startup")
|
10 |
-
async def startup_event():
|
11 |
-
try:
|
12 |
-
run_migrations()
|
13 |
-
except Exception as e:
|
14 |
-
print("Migration error:", e)
|
15 |
-
|
16 |
-
FRONTEND_DIR = os.path.join(os.getcwd(), "frontend_dist")
|
17 |
-
if os.path.isdir(FRONTEND_DIR):
|
18 |
-
app.mount("/assets", StaticFiles(directory=os.path.join(FRONTEND_DIR, "assets")), name="assets")
|
19 |
-
@app.get("/{full_path:path}")
|
20 |
-
async def serve_spa(full_path: str):
|
21 |
-
index_path = os.path.join(FRONTEND_DIR, "index.html")
|
22 |
-
if os.path.exists(index_path):
|
23 |
-
return FileResponse(index_path)
|
24 |
-
return JSONResponse({"detail": "Frontend not built."}, status_code=501)
|
25 |
-
else:
|
26 |
-
@app.get("/")
|
27 |
-
async def root_info():
|
28 |
-
return {"message": "Backend running. Frontend not built."}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/backend/__init__.py
DELETED
File without changes
|
app/backend/app.py
DELETED
@@ -1,36 +0,0 @@
|
|
1 |
-
from fastapi import FastAPI
|
2 |
-
from fastapi.middleware.cors import CORSMiddleware
|
3 |
-
from .core.config import settings
|
4 |
-
from .database import Base, engine
|
5 |
-
from .routes import auth, apikey, chat, about
|
6 |
-
|
7 |
-
def run_migrations():
|
8 |
-
try:
|
9 |
-
from alembic import command
|
10 |
-
from alembic.config import Config
|
11 |
-
import os
|
12 |
-
cfg = Config(os.path.join(os.path.dirname(__file__), "migrations", "alembic.ini"))
|
13 |
-
cfg.set_main_option("script_location", os.path.join(os.path.dirname(__file__), "migrations"))
|
14 |
-
cfg.set_main_option("sqlalchemy.url", settings.DATABASE_URL)
|
15 |
-
command.upgrade(cfg, "head")
|
16 |
-
except Exception as e:
|
17 |
-
print("Alembic migration error:", e)
|
18 |
-
|
19 |
-
def create_app() -> FastAPI:
|
20 |
-
app = FastAPI(title=settings.APP_NAME)
|
21 |
-
|
22 |
-
app.add_middleware(
|
23 |
-
CORSMiddleware,
|
24 |
-
allow_origins=[settings.CORS_ORIGINS] if settings.CORS_ORIGINS != "*" else ["*"],
|
25 |
-
allow_credentials=True,
|
26 |
-
allow_methods=["*"],
|
27 |
-
allow_headers=["*"],
|
28 |
-
)
|
29 |
-
|
30 |
-
Base.metadata.bind = engine
|
31 |
-
|
32 |
-
app.include_router(auth.router)
|
33 |
-
app.include_router(apikey.router)
|
34 |
-
app.include_router(chat.router)
|
35 |
-
app.include_router(about.router)
|
36 |
-
return app
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/backend/core/config.py
DELETED
@@ -1,14 +0,0 @@
|
|
1 |
-
import os
|
2 |
-
from pydantic import BaseModel
|
3 |
-
from dotenv import load_dotenv
|
4 |
-
|
5 |
-
load_dotenv()
|
6 |
-
|
7 |
-
class Settings(BaseModel):
|
8 |
-
APP_NAME: str = os.getenv("APP_NAME", "CHB App")
|
9 |
-
DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///./app.db")
|
10 |
-
JWT_SECRET: str = os.getenv("JWT_SECRET", "dev-secret")
|
11 |
-
JWT_EXPIRES_MINUTES: int = int(os.getenv("JWT_EXPIRES_MINUTES", "60"))
|
12 |
-
CORS_ORIGINS: str = os.getenv("CORS_ORIGINS", "*")
|
13 |
-
|
14 |
-
settings = Settings()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/backend/core/security.py
DELETED
@@ -1,40 +0,0 @@
|
|
1 |
-
from datetime import datetime, timedelta
|
2 |
-
from typing import Optional
|
3 |
-
from jose import jwt, JWTError
|
4 |
-
from passlib.context import CryptContext
|
5 |
-
from fastapi import Depends, HTTPException, status
|
6 |
-
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
7 |
-
from sqlalchemy.orm import Session
|
8 |
-
from .config import settings
|
9 |
-
from ..database import get_db
|
10 |
-
from ..models import User
|
11 |
-
|
12 |
-
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
13 |
-
auth_scheme = HTTPBearer()
|
14 |
-
|
15 |
-
def hash_password(p: str) -> str:
|
16 |
-
return pwd_context.hash(p)
|
17 |
-
|
18 |
-
def verify_password(p: str, hashed: str) -> bool:
|
19 |
-
return pwd_context.verify(p, hashed)
|
20 |
-
|
21 |
-
def create_access_token(sub: str, expires_minutes: int = None) -> str:
|
22 |
-
expire = datetime.utcnow() + timedelta(minutes=expires_minutes or settings.JWT_EXPIRES_MINUTES)
|
23 |
-
to_encode = {"sub": sub, "exp": expire}
|
24 |
-
return jwt.encode(to_encode, settings.JWT_SECRET, algorithm="HS256")
|
25 |
-
|
26 |
-
def decode_token(token: str) -> Optional[str]:
|
27 |
-
try:
|
28 |
-
payload = jwt.decode(token, settings.JWT_SECRET, algorithms=["HS256"])
|
29 |
-
return payload.get("sub")
|
30 |
-
except JWTError:
|
31 |
-
return None
|
32 |
-
|
33 |
-
def get_current_user(creds: HTTPAuthorizationCredentials = Depends(auth_scheme), db: Session = Depends(get_db)) -> User:
|
34 |
-
email = decode_token(creds.credentials)
|
35 |
-
if not email:
|
36 |
-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
37 |
-
user = db.query(User).filter(User.email == email).first()
|
38 |
-
if not user:
|
39 |
-
raise HTTPException(status_code=401, detail="User not found")
|
40 |
-
return user
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/backend/database.py
DELETED
@@ -1,16 +0,0 @@
|
|
1 |
-
from sqlalchemy import create_engine
|
2 |
-
from sqlalchemy.orm import sessionmaker, DeclarativeBase
|
3 |
-
from .core.config import settings
|
4 |
-
|
5 |
-
class Base(DeclarativeBase):
|
6 |
-
pass
|
7 |
-
|
8 |
-
engine = create_engine(settings.DATABASE_URL, connect_args={"check_same_thread": False} if settings.DATABASE_URL.startswith("sqlite") else {})
|
9 |
-
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
10 |
-
|
11 |
-
def get_db():
|
12 |
-
db = SessionLocal()
|
13 |
-
try:
|
14 |
-
yield db
|
15 |
-
finally:
|
16 |
-
db.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/backend/migrations/alembic.ini
DELETED
@@ -1,3 +0,0 @@
|
|
1 |
-
[alembic]
|
2 |
-
script_location = migrations
|
3 |
-
sqlalchemy.url = sqlite:///./app.db
|
|
|
|
|
|
|
|
app/backend/migrations/env.py
DELETED
@@ -1,23 +0,0 @@
|
|
1 |
-
from logging.config import fileConfig
|
2 |
-
from sqlalchemy import engine_from_config, pool
|
3 |
-
from alembic import context
|
4 |
-
import os, sys
|
5 |
-
config = context.config
|
6 |
-
if config.config_file_name is not None:
|
7 |
-
fileConfig(config.config_file_name)
|
8 |
-
target_metadata = None
|
9 |
-
def run_migrations_offline():
|
10 |
-
url = config.get_main_option("sqlalchemy.url")
|
11 |
-
context.configure(url=url, literal_binds=True)
|
12 |
-
with context.begin_transaction():
|
13 |
-
context.run_migrations()
|
14 |
-
def run_migrations_online():
|
15 |
-
connectable = engine_from_config(config.get_section(config.config_ini_section), prefix="sqlalchemy.", poolclass=pool.NullPool)
|
16 |
-
with connectable.connect() as connection:
|
17 |
-
context.configure(connection=connection)
|
18 |
-
with context.begin_transaction():
|
19 |
-
context.run_migrations()
|
20 |
-
if context.is_offline_mode():
|
21 |
-
run_migrations_offline()
|
22 |
-
else:
|
23 |
-
run_migrations_online()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/backend/migrations/versions/0001_init.py
DELETED
@@ -1,37 +0,0 @@
|
|
1 |
-
from alembic import op
|
2 |
-
import sqlalchemy as sa
|
3 |
-
|
4 |
-
revision = '0001_init'
|
5 |
-
down_revision = None
|
6 |
-
branch_labels = None
|
7 |
-
depends_on = None
|
8 |
-
|
9 |
-
def upgrade():
|
10 |
-
op.create_table(
|
11 |
-
'users',
|
12 |
-
sa.Column('id', sa.Integer(), primary_key=True),
|
13 |
-
sa.Column('email', sa.String(255), nullable=False, unique=True),
|
14 |
-
sa.Column('password_hash', sa.String(255), nullable=False),
|
15 |
-
sa.Column('created_at', sa.DateTime(), nullable=True),
|
16 |
-
)
|
17 |
-
op.create_table(
|
18 |
-
'api_keys',
|
19 |
-
sa.Column('id', sa.Integer(), primary_key=True),
|
20 |
-
sa.Column('key', sa.String(255), nullable=False, unique=True),
|
21 |
-
sa.Column('revoked', sa.Boolean(), default=False),
|
22 |
-
sa.Column('created_at', sa.DateTime(), nullable=True),
|
23 |
-
sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id', ondelete='CASCADE'))
|
24 |
-
)
|
25 |
-
op.create_table(
|
26 |
-
'chat_history',
|
27 |
-
sa.Column('id', sa.Integer(), primary_key=True),
|
28 |
-
sa.Column('role', sa.String(20)),
|
29 |
-
sa.Column('content', sa.Text(), nullable=False),
|
30 |
-
sa.Column('created_at', sa.DateTime(), nullable=True),
|
31 |
-
sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id', ondelete='CASCADE'))
|
32 |
-
)
|
33 |
-
|
34 |
-
def downgrade():
|
35 |
-
op.drop_table('chat_history')
|
36 |
-
op.drop_table('api_keys')
|
37 |
-
op.drop_table('users')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/backend/models.py
DELETED
@@ -1,34 +0,0 @@
|
|
1 |
-
from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, Text
|
2 |
-
from sqlalchemy.orm import relationship, Mapped, mapped_column
|
3 |
-
from datetime import datetime
|
4 |
-
from .database import Base
|
5 |
-
|
6 |
-
class User(Base):
|
7 |
-
__tablename__ = "users"
|
8 |
-
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
9 |
-
email: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False)
|
10 |
-
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
11 |
-
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
12 |
-
|
13 |
-
api_keys = relationship("APIKey", back_populates="owner", cascade="all, delete-orphan")
|
14 |
-
messages = relationship("ChatMessage", back_populates="user", cascade="all, delete-orphan")
|
15 |
-
|
16 |
-
class APIKey(Base):
|
17 |
-
__tablename__ = "api_keys"
|
18 |
-
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
19 |
-
key: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
|
20 |
-
revoked: Mapped[bool] = mapped_column(Boolean, default=False)
|
21 |
-
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
22 |
-
|
23 |
-
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"))
|
24 |
-
owner = relationship("User", back_populates="api_keys")
|
25 |
-
|
26 |
-
class ChatMessage(Base):
|
27 |
-
__tablename__ = "chat_history"
|
28 |
-
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
29 |
-
role: Mapped[str] = mapped_column(String(20), default="user") # 'user' or 'assistant'
|
30 |
-
content: Mapped[str] = mapped_column(Text, nullable=False)
|
31 |
-
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
32 |
-
|
33 |
-
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"))
|
34 |
-
user = relationship("User", back_populates="messages")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/backend/routes/about.py
DELETED
@@ -1,17 +0,0 @@
|
|
1 |
-
from fastapi import APIRouter
|
2 |
-
|
3 |
-
router = APIRouter(prefix="/api/info", tags=["Info"])
|
4 |
-
|
5 |
-
@router.get("/about")
|
6 |
-
def about():
|
7 |
-
return {"name": "CHB", "tagline": "Multimodal Assistant", "version": "1.0.0"}
|
8 |
-
|
9 |
-
@router.get("/howto")
|
10 |
-
def howto():
|
11 |
-
return {
|
12 |
-
"steps": [
|
13 |
-
"Sign up or log in",
|
14 |
-
"Use Chat to send messages, long-press the paperclip to switch modes",
|
15 |
-
"Go to API Keys to generate or revoke keys"
|
16 |
-
]
|
17 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/backend/routes/apikey.py
DELETED
@@ -1,32 +0,0 @@
|
|
1 |
-
import secrets
|
2 |
-
from fastapi import APIRouter, Depends, HTTPException
|
3 |
-
from sqlalchemy.orm import Session
|
4 |
-
from ..database import get_db
|
5 |
-
from ..models import APIKey
|
6 |
-
from ..schemas import APIKeyOut, KeyRevokeIn
|
7 |
-
from ..core.security import get_current_user
|
8 |
-
|
9 |
-
router = APIRouter(prefix="/api/keys", tags=["API Keys"])
|
10 |
-
|
11 |
-
@router.get("", response_model=list[APIKeyOut])
|
12 |
-
def list_keys(db: Session = Depends(get_db), user = Depends(get_current_user)):
|
13 |
-
rows = db.query(APIKey).filter(APIKey.user_id == user.id).order_by(APIKey.created_at.desc()).all()
|
14 |
-
return rows
|
15 |
-
|
16 |
-
@router.post("/generate", response_model=APIKeyOut)
|
17 |
-
def generate_key(db: Session = Depends(get_db), user = Depends(get_current_user)):
|
18 |
-
key = "chb_" + secrets.token_urlsafe(32)
|
19 |
-
row = APIKey(key=key, user_id=user.id)
|
20 |
-
db.add(row)
|
21 |
-
db.commit()
|
22 |
-
db.refresh(row)
|
23 |
-
return row
|
24 |
-
|
25 |
-
@router.post("/revoke")
|
26 |
-
def revoke_key(payload: KeyRevokeIn, db: Session = Depends(get_db), user = Depends(get_current_user)):
|
27 |
-
row = db.query(APIKey).filter(APIKey.user_id == user.id, APIKey.key == payload.key).first()
|
28 |
-
if not row:
|
29 |
-
raise HTTPException(status_code=404, detail="Key not found")
|
30 |
-
row.revoked = True
|
31 |
-
db.commit()
|
32 |
-
return {"revoked": payload.key}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/backend/routes/auth.py
DELETED
@@ -1,27 +0,0 @@
|
|
1 |
-
from fastapi import APIRouter, HTTPException, Depends
|
2 |
-
from sqlalchemy.orm import Session
|
3 |
-
from ..schemas import UserCreate, UserOut, LoginIn, Token
|
4 |
-
from ..models import User
|
5 |
-
from ..database import get_db
|
6 |
-
from ..core.security import hash_password, verify_password, create_access_token
|
7 |
-
|
8 |
-
router = APIRouter(prefix="/api/auth", tags=["Auth"])
|
9 |
-
|
10 |
-
@router.post("/signup", response_model=Token)
|
11 |
-
def signup(payload: UserCreate, db: Session = Depends(get_db)):
|
12 |
-
if db.query(User).filter(User.email == payload.email).first():
|
13 |
-
raise HTTPException(status_code=400, detail="User already exists")
|
14 |
-
user = User(email=payload.email, password_hash=hash_password(payload.password))
|
15 |
-
db.add(user)
|
16 |
-
db.commit()
|
17 |
-
db.refresh(user)
|
18 |
-
token = create_access_token(sub=user.email)
|
19 |
-
return {"access_token": token}
|
20 |
-
|
21 |
-
@router.post("/login", response_model=Token)
|
22 |
-
def login(payload: LoginIn, db: Session = Depends(get_db)):
|
23 |
-
user = db.query(User).filter(User.email == payload.email).first()
|
24 |
-
if not user or not verify_password(payload.password, user.password_hash):
|
25 |
-
raise HTTPException(status_code=401, detail="Invalid credentials")
|
26 |
-
token = create_access_token(sub=user.email)
|
27 |
-
return {"access_token": token}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/backend/routes/chat.py
DELETED
@@ -1,28 +0,0 @@
|
|
1 |
-
from fastapi import APIRouter, Depends, UploadFile, File
|
2 |
-
from sqlalchemy.orm import Session
|
3 |
-
from ..database import get_db
|
4 |
-
from ..schemas import ChatIn, ChatOut, ChatMessageOut
|
5 |
-
from ..models import ChatMessage
|
6 |
-
from ..core.security import get_current_user
|
7 |
-
|
8 |
-
router = APIRouter(prefix="/api/chat", tags=["Chat"])
|
9 |
-
|
10 |
-
@router.get("/history", response_model=list[ChatMessageOut])
|
11 |
-
def history(db: Session = Depends(get_db), user = Depends(get_current_user)):
|
12 |
-
rows = db.query(ChatMessage).filter(ChatMessage.user_id == user.id).order_by(ChatMessage.created_at.asc()).all()
|
13 |
-
return rows
|
14 |
-
|
15 |
-
@router.post("", response_model=ChatOut)
|
16 |
-
def send(payload: ChatIn, db: Session = Depends(get_db), user = Depends(get_current_user)):
|
17 |
-
um = ChatMessage(user_id=user.id, role="user", content=payload.message)
|
18 |
-
db.add(um)
|
19 |
-
reply_text = f"CHB: You said '{payload.message}'"
|
20 |
-
bm = ChatMessage(user_id=user.id, role="assistant", content=reply_text)
|
21 |
-
db.add(bm)
|
22 |
-
db.commit()
|
23 |
-
return {"reply": reply_text}
|
24 |
-
|
25 |
-
@router.post("/upload")
|
26 |
-
async def upload(file: UploadFile = File(...), user = Depends(get_current_user)):
|
27 |
-
# stub: accept file and return name; real storage to be implemented
|
28 |
-
return {"filename": file.filename, "detail":"file received"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/backend/schemas.py
DELETED
@@ -1,45 +0,0 @@
|
|
1 |
-
from pydantic import BaseModel, EmailStr, Field
|
2 |
-
from typing import List, Optional
|
3 |
-
from datetime import datetime
|
4 |
-
|
5 |
-
class UserCreate(BaseModel):
|
6 |
-
email: EmailStr
|
7 |
-
password: str = Field(min_length=6)
|
8 |
-
|
9 |
-
class UserOut(BaseModel):
|
10 |
-
id: int
|
11 |
-
email: EmailStr
|
12 |
-
created_at: datetime
|
13 |
-
class Config:
|
14 |
-
from_attributes = True
|
15 |
-
|
16 |
-
class Token(BaseModel):
|
17 |
-
access_token: str
|
18 |
-
token_type: str = "bearer"
|
19 |
-
|
20 |
-
class LoginIn(BaseModel):
|
21 |
-
email: EmailStr
|
22 |
-
password: str
|
23 |
-
|
24 |
-
class APIKeyOut(BaseModel):
|
25 |
-
key: str
|
26 |
-
revoked: bool
|
27 |
-
created_at: datetime
|
28 |
-
class Config:
|
29 |
-
from_attributes = True
|
30 |
-
|
31 |
-
class KeyRevokeIn(BaseModel):
|
32 |
-
key: str
|
33 |
-
|
34 |
-
class ChatIn(BaseModel):
|
35 |
-
message: str
|
36 |
-
|
37 |
-
class ChatOut(BaseModel):
|
38 |
-
reply: str
|
39 |
-
|
40 |
-
class ChatMessageOut(BaseModel):
|
41 |
-
role: str
|
42 |
-
content: str
|
43 |
-
created_at: datetime
|
44 |
-
class Config:
|
45 |
-
from_attributes = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/frontend/index.html
DELETED
@@ -1 +0,0 @@
|
|
1 |
-
<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><title>CHB</title></head><body class="bg-slate-900 text-slate-100"><div id="root"></div><script type="module" src="/src/main.tsx"></script></body></html>
|
|
|
|
app/frontend/package.json
DELETED
@@ -1,24 +0,0 @@
|
|
1 |
-
{
|
2 |
-
"name": "chb-frontend",
|
3 |
-
"version": "1.0.0",
|
4 |
-
"private": true,
|
5 |
-
"type": "module",
|
6 |
-
"scripts": {
|
7 |
-
"dev": "vite",
|
8 |
-
"build": "vite build",
|
9 |
-
"preview": "vite preview"
|
10 |
-
},
|
11 |
-
"dependencies": {
|
12 |
-
"react": "^18.2.0",
|
13 |
-
"react-dom": "^18.2.0",
|
14 |
-
"react-router-dom": "^6.23.1",
|
15 |
-
"framer-motion": "^11.2.10"
|
16 |
-
},
|
17 |
-
"devDependencies": {
|
18 |
-
"typescript": "^5.5.4",
|
19 |
-
"vite": "^5.3.3",
|
20 |
-
"tailwindcss": "^3.4.7",
|
21 |
-
"postcss": "^8.4.41",
|
22 |
-
"autoprefixer": "^10.4.20"
|
23 |
-
}
|
24 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/frontend/postcss.config.js
DELETED
@@ -1 +0,0 @@
|
|
1 |
-
export default { plugins: { tailwindcss: {}, autoprefixer: {} } }
|
|
|
|
app/frontend/src/App.tsx
DELETED
@@ -1,23 +0,0 @@
|
|
1 |
-
import { Routes, Route, Navigate } from 'react-router-dom'
|
2 |
-
import Login from './pages/Login'
|
3 |
-
import Signup from './pages/Signup'
|
4 |
-
import Chat from './pages/Chat'
|
5 |
-
import ApiKeys from './pages/ApiKeys'
|
6 |
-
import About from './pages/About'
|
7 |
-
import HowToUse from './pages/HowToUse'
|
8 |
-
import NotFound from './pages/NotFound'
|
9 |
-
const isAuthed = () => !!localStorage.getItem('token')
|
10 |
-
export default function App(){
|
11 |
-
return (
|
12 |
-
<Routes>
|
13 |
-
<Route path="/" element={isAuthed()?<Navigate to="/chat"/>:<Navigate to="/login"/>} />
|
14 |
-
<Route path="/login" element={<Login/>} />
|
15 |
-
<Route path="/signup" element={<Signup/>} />
|
16 |
-
<Route path="/chat" element={isAuthed()?<Chat/>:<Navigate to="/login"/>} />
|
17 |
-
<Route path="/apikeys" element={isAuthed()?<ApiKeys/>:<Navigate to="/login"/>} />
|
18 |
-
<Route path="/about" element={<About/>} />
|
19 |
-
<Route path="/howto" element={<HowToUse/>} />
|
20 |
-
<Route path="*" element={<NotFound/>} />
|
21 |
-
</Routes>
|
22 |
-
)
|
23 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/frontend/src/components/ChatBox.tsx
DELETED
@@ -1,8 +0,0 @@
|
|
1 |
-
import { useEffect, useRef, useState } from 'react'
|
2 |
-
const API=(p:string)=>'/api'+p
|
3 |
-
type Msg={role:'user'|'assistant',content:string}
|
4 |
-
export default function ChatBox(){const [messages,setMessages]=useState<Msg[]>([]);const [text,setText]=useState('');const [mode,setMode]=useState<'file'|'mic'|'video'>('file');const pressTimer=useRef<number|undefined>(undefined);const feedRef=useRef<HTMLDivElement>(null);const token=localStorage.getItem('token')||'';const headers={'Content-Type':'application/json','Authorization':'Bearer '+token};useEffect(()=>{fetch(API('/chat/history'),{headers}).then(r=>r.json()).then((data)=>{setMessages(data.map((m:any)=>({role:m.role,content:m.content})))})},[]);useEffect(()=>{feedRef.current?.scrollTo({top:feedRef.current.scrollHeight})},[messages]);const send=async()=>{if(!text.trim()) return; setMessages(m=>[...m,{role:'user',content:text}]); setText(''); const r=await fetch(API('/chat'),{method:'POST',headers,body:JSON.stringify({message:text})}); const data=await r.json(); setMessages(m=>[...m,{role:'assistant',content:data.reply}])}
|
5 |
-
const cycle=()=>setMode(m=> m==='file'?'mic': m==='mic'?'video':'file')
|
6 |
-
const onMouseDown=()=>{pressTimer.current=window.setTimeout(cycle,450)}
|
7 |
-
const onMouseUp=()=>{if(pressTimer.current){clearTimeout(pressTimer.current); pressTimer.current=undefined}}
|
8 |
-
return (<div className="flex flex-col h-full"><div ref={feedRef} className="flex-1 overflow-y-auto p-3 border border-slate-800 rounded-xl bg-slate-900/40 space-y-2">{messages.map((m,i)=>(<div key={i} className={`max-w-[70%] px-3 py-2 rounded-2xl ${m.role==='user'?'self-end bg-blue-600 text-white rounded-br-sm':'self-start bg-slate-800 border border-slate-700 rounded-bl-sm'}`}>{m.content}</div>))}</div><div className="grid grid-cols-[44px_1fr_60px] gap-2 mt-2 items-center"><button onMouseDown={onMouseDown} onMouseUp={onMouseUp} onTouchStart={onMouseDown} onTouchEnd={onMouseUp} onClick={()=>{if(mode==='file'){alert('Upload placeholder')}else if(mode==='mic'){alert('Record audio placeholder')}else{alert('Record video placeholder')}}} className="w-11 h-11 rounded-full border border-slate-700 bg-slate-800">{mode==='file'?'📎':mode==='mic'?'🎤':'📹'}</button><input value={text} onChange={e=>setText((e.target as any).value)} placeholder="Type a message" className="px-3 py-2 rounded-xl border border-slate-700 bg-slate-800 outline-none"/><button onClick={send} className="px-3 py-2 rounded-xl bg-sky-400 text-slate-900 font-semibold">Send</button></div></div>)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/frontend/src/components/Loader.tsx
DELETED
@@ -1 +0,0 @@
|
|
1 |
-
export default function Loader(){return (<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50"><div className="w-14 h-14 rounded-full border-4 border-white/30 border-t-[color:var(--primary)] animate-spin"/></div>)}
|
|
|
|
app/frontend/src/components/Onboarding.tsx
DELETED
@@ -1,2 +0,0 @@
|
|
1 |
-
import { useEffect, useState } from 'react'
|
2 |
-
export default function Onboarding(){const [show,setShow]=useState(false);useEffect(()=>{if(localStorage.getItem('first_time')==='1'){setShow(true)}},[]); if(!show) return null; return (<div className="fixed inset-0 z-40"><div className="absolute inset-0 bg-black/40"/><div className="absolute left-64 top-24 p-3 rounded-xl border border-slate-600 bg-slate-800">Tap here to open Chat</div><div className="absolute left-56 top-20 w-12 h-12 rounded-full bg-amber-200 shadow animate-pulse"/><button onClick={()=>{localStorage.removeItem('first_time'); setShow(false)}} className="absolute bottom-6 right-6 px-3 py-2 rounded-xl border border-slate-600 bg-slate-800">Skip</button></div>)}
|
|
|
|
|
|
app/frontend/src/components/Sidebar.tsx
DELETED
@@ -1,2 +0,0 @@
|
|
1 |
-
import { Link, useLocation } from 'react-router-dom'
|
2 |
-
export default function Sidebar(){const { pathname } = useLocation(); const item=(to:string,label:string)=> (<Link to={to} className={`block px-3 py-2 rounded-xl border mb-2 ${pathname===to?'border-sky-400 bg-sky-400/10':'border-slate-700 bg-slate-800/40'}`}>{label}</Link>); return (<aside className="w-64 p-4 border-r border-slate-800 bg-slate-900/60"><div className="font-extrabold tracking-widest text-xl mb-3">CHB</div>{item('/chat','💬 Chat')}{item('/apikeys','🔑 API Keys')}{item('/about','ℹ️ About Us')}{item('/howto','❓ How to Use')}<button onClick={()=>{localStorage.removeItem('token'); location.href='/login'}} className="mt-2 w-full px-3 py-2 rounded-xl border border-red-700 bg-red-900/40">⎋ Logout</button></aside>)}
|
|
|
|
|
|
app/frontend/src/main.tsx
DELETED
@@ -1,12 +0,0 @@
|
|
1 |
-
import React from 'react'
|
2 |
-
import ReactDOM from 'react-dom/client'
|
3 |
-
import { BrowserRouter } from 'react-router-dom'
|
4 |
-
import App from './App'
|
5 |
-
import './styles.css'
|
6 |
-
ReactDOM.createRoot(document.getElementById('root')!).render(
|
7 |
-
<React.StrictMode>
|
8 |
-
<BrowserRouter>
|
9 |
-
<App />
|
10 |
-
</BrowserRouter>
|
11 |
-
</React.StrictMode>
|
12 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/frontend/src/pages/About.tsx
DELETED
@@ -1,2 +0,0 @@
|
|
1 |
-
import Sidebar from '../components/Sidebar'
|
2 |
-
export default function About(){return(<div className="h-screen grid grid-cols-[260px_1fr]"><Sidebar /><main className="p-4"><h2 className="text-xl font-semibold mb-2">About Us</h2><div className="border p-3 rounded-xl border-slate-800 bg-slate-900/40"><p>CHB Multimodal Assistant. Built for chat, uploads, audio/video, and developer APIs.</p></div></main></div>)}
|
|
|
|
|
|
app/frontend/src/pages/ApiKeys.tsx
DELETED
@@ -1,3 +0,0 @@
|
|
1 |
-
import Sidebar from '../components/Sidebar'
|
2 |
-
import { useState } from 'react'
|
3 |
-
export default function ApiKeys(){const [gen,setGen]=useState('');const [revokeResult,setRevoke]=useState('');const [list,setList]=useState<any[]>([]);const token=localStorage.getItem('token')||'';const headers={'Authorization':'Bearer '+token,'Content-Type':'application/json'};const API=(p:string)=>'/api'+p;const doGen=async()=>{const r=await fetch(API('/keys/generate'),{method:'POST',headers});setGen(JSON.stringify(await r.json(),null,2))};const doRevoke=async()=>{const key=prompt('Enter key to revoke');if(!key) return;const r=await fetch(API('/keys/revoke'),{method:'POST',headers,body:JSON.stringify({key})});setRevoke(JSON.stringify(await r.json(),null,2))};const doList=async()=>{const r=await fetch(API('/keys'),{headers:{'Authorization':'Bearer '+token}});setList(await r.json())};return(<div className="h-screen grid grid-cols-[260px_1fr]"><Sidebar /><main className="p-4 space-y-3"><header className="flex items-center justify-between"><h2 className="text-xl font-semibold">API Keys</h2></header><div className="border p-3 rounded-xl border-slate-800 bg-slate-900/40"><h3 className="font-semibold mb-1">Guide</h3><ol className="list-decimal pl-5 text-slate-300"><li><b>Generate new key:</b> Creates a fresh key for API access.</li><li><b>Revoke key:</b> Immediately disables a key.</li><li><b>My key details:</b> View your active keys and status.</li></ol></div><div className="grid md:grid-cols-3 gap-3"><div className="border p-3 rounded-xl border-slate-800 bg-slate-900/40"><h3 className="font-semibold mb-2">Generate new key</h3><button onClick={doGen} className="px-3 py-2 rounded-xl bg-sky-400 text-slate-900 font-semibold">Generate</button><pre className="mt-2 text-xs bg-slate-800 p-2 rounded">{gen}</pre></div><div className="border p-3 rounded-xl border-slate-800 bg-slate-900/40"><h3 className="font-semibold mb-2">Revoke key</h3><button onClick={doRevoke} className="px-3 py-2 rounded-xl bg-red-400 text-slate-900 font-semibold">Revoke</button><pre className="mt-2 text-xs bg-slate-800 p-2 rounded">{revokeResult}</pre></div><div className="border p-3 rounded-xl border-slate-800 bg-slate-900/40"><h3 className="font-semibold mb-2">My key details</h3><button onClick={doList} className="px-3 py-2 rounded-xl bg-slate-200/80 text-slate-900 font-semibold">Refresh</button><pre className="mt-2 text-xs bg-slate-800 p-2 rounded">{JSON.stringify(list,null,2)}</pre></div></div></main></div>)}
|
|
|
|
|
|
|
|
app/frontend/src/pages/Chat.tsx
DELETED
@@ -1,4 +0,0 @@
|
|
1 |
-
import Sidebar from '../components/Sidebar'
|
2 |
-
import Onboarding from '../components/Onboarding'
|
3 |
-
import ChatBox from '../components/ChatBox'
|
4 |
-
export default function Chat(){return(<div className="h-screen grid grid-cols-[260px_1fr]"><Sidebar /><main className="p-4"><header className="flex items-center justify-between mb-2"><h2 className="text-xl font-semibold">Chat</h2></header><section className="h-[calc(100vh-80px)]"><ChatBox /></section></main><Onboarding /></div>)}
|
|
|
|
|
|
|
|
|
|
app/frontend/src/pages/HowToUse.tsx
DELETED
@@ -1,2 +0,0 @@
|
|
1 |
-
import Sidebar from '../components/Sidebar'
|
2 |
-
export default function HowToUse(){return(<div className="h-screen grid grid-cols-[260px_1fr]"><Sidebar /><main className="p-4"><h2 className="text-xl font-semibold mb-2">How to Use</h2><div className="border p-3 rounded-xl border-slate-800 bg-slate-900/40"><ol className="list-decimal pl-5 space-y-1"><li>Sign up or log in (email + password).</li><li>Use Chat to talk to CHB. Long-press the paperclip to switch between upload / voice / video.</li><li>Manage API keys in <b>API Keys</b>.</li></ol></div></main></div>)}
|
|
|
|
|
|
app/frontend/src/pages/Login.tsx
DELETED
@@ -1,3 +0,0 @@
|
|
1 |
-
import { useState } from 'react'
|
2 |
-
import Loader from '../components/Loader'
|
3 |
-
export default function Login(){const [email,setEmail]=useState('');const [password,setPassword]=useState('');const [loading,setLoading]=useState(false);const submit=async(e:any)=>{e.preventDefault();setLoading(true);const r=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email,password})});const data=await r.json();setLoading(false);if(r.ok){localStorage.setItem('token',data.access_token);location.href='/chat'}else{alert(data.detail||'Login failed')}};return(<div className="min-h-screen flex items-center justify-center p-4 bg-slate-900">{loading&&<Loader/>}<div className="w-full max-w-md border border-slate-800 rounded-2xl p-6 bg-slate-900/60"><h1 className="text-2xl font-bold mb-1 tracking-widest">CHB</h1><p className="text-slate-400 mb-4">Sign in to continue</p><form onSubmit={submit} className="space-y-3"><input value={email} onChange={e=>setEmail((e.target as any).value)} type="email" placeholder="[email protected]" className="w-full px-3 py-2 rounded-xl border border-slate-700 bg-slate-800" required/><input value={password} onChange={e=>setPassword((e.target as any).value)} type="password" placeholder="••••••••" className="w-full px-3 py-2 rounded-xl border border-slate-700 bg-slate-800" required/><button className="w-full px-3 py-2 rounded-xl bg-sky-400 text-slate-900 font-semibold">Log in</button></form><div className="mt-3 text-sm text-slate-400">No account? <a href="/signup" className="text-sky-300">Sign up</a></div></div></div>)}
|
|
|
|
|
|
|
|
app/frontend/src/pages/NotFound.tsx
DELETED
@@ -1 +0,0 @@
|
|
1 |
-
export default function NotFound(){return <div className='min-h-screen grid place-items-center text-slate-200'>Not Found</div>}
|
|
|
|
app/frontend/src/pages/Signup.tsx
DELETED
@@ -1,3 +0,0 @@
|
|
1 |
-
import { useState } from 'react'
|
2 |
-
import Loader from '../components/Loader'
|
3 |
-
export default function Signup(){const [email,setEmail]=useState('');const [password,setPassword]=useState('');const [loading,setLoading]=useState(false);const submit=async(e:any)=>{e.preventDefault();setLoading(true);const r=await fetch('/api/auth/signup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email,password})});const data=await r.json();setLoading(false);if(r.ok){localStorage.setItem('token',data.access_token);localStorage.setItem('first_time','1');location.href='/chat'}else{alert(data.detail||'Signup failed')}};return(<div className="min-h-screen flex items-center justify-center p-4 bg-slate-900">{loading&&<Loader/>}<div className="w-full max-w-md border border-slate-800 rounded-2xl p-6 bg-slate-900/60"><h1 className="text-2xl font-bold mb-1 tracking-widest">CHB</h1><p className="text-slate-400 mb-4">Create an account</p><form onSubmit={submit} className="space-y-3"><input value={email} onChange={e=>setEmail((e.target as any).value)} type="email" placeholder="[email protected]" className="w-full px-3 py-2 rounded-xl border border-slate-700 bg-slate-800" required/><input value={password} onChange={e=>setPassword((e.target as any).value)} type="password" placeholder="Create a password" className="w-full px-3 py-2 rounded-xl border border-slate-700 bg-slate-800" required/><button className="w-full px-3 py-2 rounded-xl bg-sky-400 text-slate-900 font-semibold">Create account</button></form></div></div>)}
|
|
|
|
|
|
|
|
app/frontend/src/styles.css
DELETED
@@ -1 +0,0 @@
|
|
1 |
-
@tailwind base;@tailwind components;@tailwind utilities;:root{--primary:#3ab4f2}::-webkit-scrollbar{width:8px}::-webkit-scrollbar-thumb{background:#1f3047;border-radius:8px}
|
|
|
|
app/frontend/tailwind.config.js
DELETED
@@ -1 +0,0 @@
|
|
1 |
-
export default {content:['./index.html','./src/**/*.{ts,tsx}'], theme:{extend:{}}, plugins:[]}
|
|
|
|
app/frontend/vite.config.js
DELETED
@@ -1,3 +0,0 @@
|
|
1 |
-
import { defineConfig } from 'vite'
|
2 |
-
import react from '@vitejs/plugin-react'
|
3 |
-
export default defineConfig({plugins:[react()], server:{port:5173, proxy:{'/api':'http://localhost:7860'}}, build:{outDir:'dist'}})
|
|
|
|
|
|
|
|
app/requirements.txt
DELETED
@@ -1,10 +0,0 @@
|
|
1 |
-
fastapi==0.111.0
|
2 |
-
uvicorn[standard]==0.30.1
|
3 |
-
SQLAlchemy==2.0.31
|
4 |
-
alembic==1.13.2
|
5 |
-
pydantic[email]==2.8.2
|
6 |
-
passlib[bcrypt]==1.7.4
|
7 |
-
python-jose[cryptography]==3.3.0
|
8 |
-
python-dotenv==1.0.1
|
9 |
-
jinja2==3.1.4
|
10 |
-
python-multipart==0.0.9
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|