Compare commits

...

6 Commits

Author SHA1 Message Date
shokollm
429d46c6d0 feat: implement bot CRUD with 3-bot limit per user 2026-04-08 06:05:43 +00:00
a2f0c9a0e9 Merge pull request '[Backend] Auth System - JWT Authentication' (#15) from fix/issue-4 into main 2026-04-08 08:01:24 +02:00
shokollm
42640679c7 feat: implement JWT authentication system
- Add register endpoint with bcrypt password hashing
- Add login endpoint returning JWT tokens
- Add logout endpoint with token blacklisting
- Add /me endpoint for current user info
- Add rate limiting (5/minute) for login attempts using slowapi
- Add user settings GET and PATCH endpoints
- Create auth middleware via get_current_user dependency
- Add UserSettings and UserSettingsUpdate schemas
2026-04-08 05:48:38 +00:00
f59e595ffd Merge pull request '[Backend] Database Models - Add missing Pydantic schemas' (#14) from fix/issue-3 into main 2026-04-08 06:57:46 +02:00
shokollm
a5e41ab449 Add missing Pydantic schemas for BotConversation and Signal
Based on IMPLEMENTATION_PLAN.md Section 4 schema, the existing schemas.py
was missing schemas for:
- BotConversationCreate/Response (for bot_conversations table)
- SignalResponse (for signals table)

These were identified as gaps during issue #3 review.
2026-04-08 04:41:31 +00:00
6977203748 Merge pull request '[Backend] Project Setup - FastAPI Structure and Dependencies' (#13) from fix/issue-2 into main 2026-04-08 06:35:53 +02:00
6 changed files with 350 additions and 54 deletions

View File

@@ -1,7 +1,6 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from datetime import timedelta
from typing import Annotated
from ..core.database import get_db
@@ -12,41 +11,133 @@ from ..core.security import (
verify_token,
)
from ..core.config import get_settings
from ..db.schemas import UserCreate, UserResponse, Token
from ..core.limiter import limiter
from ..db.schemas import (
UserCreate,
UserResponse,
Token,
UserSettings,
UserSettingsUpdate,
)
from ..db.models import User
router = APIRouter()
settings = get_settings()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
TOKEN_BLACKLIST = set()
@router.post("/register", response_model=UserResponse)
def register(user: UserCreate, db: Session = Depends(get_db)):
def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)], db: Session = Depends(get_db)
) -> User:
if token in TOKEN_BLACKLIST:
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has been revoked",
)
payload = verify_token(token)
if payload is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
)
user_id = payload.get("sub")
if user_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token payload",
)
user = db.query(User).filter(User.id == user_id).first()
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found",
)
return user
@router.post("/login")
@router.post(
"/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED
)
def register(user: UserCreate, db: Session = Depends(get_db)):
existing_user = db.query(User).filter(User.email == user.email).first()
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered",
)
hashed_password = get_password_hash(user.password)
db_user = User(
email=user.email,
password_hash=hashed_password,
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
@router.post("/login", response_model=Token)
@limiter.limit("5/minute")
def login(
request: Request,
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
db: Session = Depends(get_db),
):
user = db.query(User).filter(User.email == form_data.username).first()
if not user or not verify_password(form_data.password, user.password_hash):
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
)
access_token = create_access_token(data={"sub": user.id})
return Token(access_token=access_token, token_type="bearer")
@router.post("/logout")
def logout(token: Annotated[str, Depends(oauth2_scheme)]):
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
)
def logout(
current_user: Annotated[User, Depends(get_current_user)],
token: Annotated[str, Depends(oauth2_scheme)],
):
TOKEN_BLACKLIST.add(token)
return {"message": "Successfully logged out"}
@router.get("/me", response_model=UserResponse)
def get_me(
token: Annotated[str, Depends(oauth2_scheme)], db: Session = Depends(get_db)
current_user: Annotated[User, Depends(get_current_user)],
):
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
return current_user
@router.get("/settings", response_model=UserSettings)
def get_settings_endpoint(
current_user: Annotated[User, Depends(get_current_user)],
):
return UserSettings(email=current_user.email)
@router.patch("/settings", response_model=UserSettings)
def update_settings(
current_user: Annotated[User, Depends(get_current_user)],
settings_update: UserSettingsUpdate,
db: Session = Depends(get_db),
):
if settings_update.email:
existing = (
db.query(User)
.filter(User.email == settings_update.email, User.id != current_user.id)
.first()
)
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already in use",
)
current_user.email = settings_update.email
if settings_update.password:
current_user.password_hash = get_password_hash(settings_update.password)
db.commit()
db.refresh(current_user)
return UserSettings(email=current_user.email)

View File

@@ -1,57 +1,211 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from typing import List, Annotated
from .auth import get_current_user
from ..core.database import get_db
from ..db.schemas import BotCreate, BotUpdate, BotResponse
from ..db.schemas import (
BotCreate,
BotUpdate,
BotResponse,
BotConversationCreate,
BotConversationResponse,
)
from ..db.models import Bot, BotConversation, User
router = APIRouter()
MAX_BOTS_PER_USER = 3
@router.get("", response_model=List[BotResponse])
def list_bots(db: Session = Depends(get_db)):
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
)
def list_bots(
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
):
bots = db.query(Bot).filter(Bot.user_id == current_user.id).all()
return bots
@router.post("", response_model=BotResponse)
def create_bot(bot: BotCreate, db: Session = Depends(get_db)):
@router.post("", response_model=BotResponse, status_code=status.HTTP_201_CREATED)
def create_bot(
bot_data: BotCreate,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
):
user_bot_count = db.query(Bot).filter(Bot.user_id == current_user.id).count()
if user_bot_count >= MAX_BOTS_PER_USER:
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Maximum of {MAX_BOTS_PER_USER} bots per user exceeded",
)
existing_bot = (
db.query(Bot)
.filter(Bot.user_id == current_user.id, Bot.name == bot_data.name)
.first()
)
if existing_bot:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Bot name must be unique per user",
)
db_bot = Bot(
user_id=current_user.id,
name=bot_data.name,
description=bot_data.description,
strategy_config=bot_data.strategy_config,
llm_config=bot_data.llm_config,
)
db.add(db_bot)
db.commit()
db.refresh(db_bot)
return db_bot
@router.get("/{bot_id}", response_model=BotResponse)
def get_bot(bot_id: str, db: Session = Depends(get_db)):
def get_bot(
bot_id: str,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
):
bot = db.query(Bot).filter(Bot.id == bot_id).first()
if not bot:
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
status_code=status.HTTP_404_NOT_FOUND,
detail="Bot not found",
)
if bot.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to access this bot",
)
return bot
@router.put("/{bot_id}", response_model=BotResponse)
def update_bot(bot_id: str, bot: BotUpdate, db: Session = Depends(get_db)):
def update_bot(
bot_id: str,
bot_data: BotUpdate,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
):
bot = db.query(Bot).filter(Bot.id == bot_id).first()
if not bot:
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
status_code=status.HTTP_404_NOT_FOUND,
detail="Bot not found",
)
if bot.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to update this bot",
)
@router.delete("/{bot_id}")
def delete_bot(bot_id: str, db: Session = Depends(get_db)):
if bot_data.name is not None:
existing_bot = (
db.query(Bot)
.filter(
Bot.user_id == current_user.id,
Bot.name == bot_data.name,
Bot.id != bot_id,
)
.first()
)
if existing_bot:
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
status_code=status.HTTP_400_BAD_REQUEST,
detail="Bot name must be unique per user",
)
bot.name = bot_data.name
if bot_data.description is not None:
bot.description = bot_data.description
if bot_data.strategy_config is not None:
bot.strategy_config = bot_data.strategy_config
if bot_data.llm_config is not None:
bot.llm_config = bot_data.llm_config
if bot_data.status is not None:
bot.status = bot_data.status
db.commit()
db.refresh(bot)
return bot
@router.delete("/{bot_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_bot(
bot_id: str,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
):
bot = db.query(Bot).filter(Bot.id == bot_id).first()
if not bot:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Bot not found",
)
if bot.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to delete this bot",
)
db.delete(bot)
db.commit()
@router.post("/{bot_id}/chat", response_model=BotConversationResponse)
def chat(
bot_id: str,
message: BotConversationCreate,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
):
bot = db.query(Bot).filter(Bot.id == bot_id).first()
if not bot:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Bot not found",
)
if bot.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to chat with this bot",
)
db_conversation = BotConversation(
bot_id=bot_id,
role=message.role,
content=message.content,
)
db.add(db_conversation)
db.commit()
db.refresh(db_conversation)
return db_conversation
@router.post("/{bot_id}/chat")
def chat(bot_id: str, message: dict, db: Session = Depends(get_db)):
@router.get("/{bot_id}/history", response_model=List[BotConversationResponse])
def get_history(
bot_id: str,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
):
bot = db.query(Bot).filter(Bot.id == bot_id).first()
if not bot:
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
status_code=status.HTTP_404_NOT_FOUND,
detail="Bot not found",
)
if bot.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to access this bot's history",
)
@router.get("/{bot_id}/history")
def get_history(bot_id: str, db: Session = Depends(get_db)):
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
conversations = (
db.query(BotConversation)
.filter(BotConversation.bot_id == bot_id)
.order_by(BotConversation.created_at)
.all()
)
return conversations

View File

@@ -0,0 +1,4 @@
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)

View File

@@ -23,6 +23,15 @@ class Token(BaseModel):
token_type: str
class UserSettings(BaseModel):
email: EmailStr
class UserSettingsUpdate(BaseModel):
email: Optional[EmailStr] = None
password: Optional[str] = None
class BotCreate(BaseModel):
name: str
description: Optional[str] = None
@@ -91,3 +100,35 @@ class SimulationResponse(BaseModel):
class Config:
from_attributes = True
class BotConversationCreate(BaseModel):
role: str
content: str
class BotConversationResponse(BaseModel):
id: str
bot_id: str
role: str
content: str
created_at: datetime
class Config:
from_attributes = True
class SignalResponse(BaseModel):
id: str
bot_id: str
run_id: str
signal_type: str
token: str
price: float
confidence: Optional[float]
reasoning: Optional[str]
executed: bool
created_at: datetime
class Config:
from_attributes = True

View File

@@ -1,6 +1,9 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from slowapi import Limiter
from slowapi.util import get_remote_address
from .api import auth, bots, backtest, simulate, config
from .core.limiter import limiter
app = FastAPI(
title="Randebu Trading Bot API",
@@ -8,6 +11,8 @@ app = FastAPI(
version="0.1.0",
)
app.state.limiter = limiter
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],

View File

@@ -10,3 +10,4 @@ crewai>=0.1.0
anthropic>=0.18.0
httpx>=0.26.0
python-multipart>=0.0.6
slowapi>=0.1.9