264 lines
8.0 KiB
Python
264 lines
8.0 KiB
Python
"""Application FastAPI principale."""
|
|
import json
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from datetime import datetime
|
|
|
|
# #region agent log
|
|
def write_debug_log(hypothesisId: str, location: str, message: str, data: dict = None):
|
|
"""Écrit un log NDJSON pour le debug."""
|
|
log_entry = {
|
|
"sessionId": "debug-session",
|
|
"runId": "run1",
|
|
"hypothesisId": hypothesisId,
|
|
"location": location,
|
|
"message": message,
|
|
"data": data or {},
|
|
"timestamp": datetime.now().timestamp() * 1000
|
|
}
|
|
with open(r"d:\\dev_new_pc\\chartbastan\\.cursor\\debug.log", "a") as f:
|
|
f.write(json.dumps(log_entry) + "\n")
|
|
# #endregion
|
|
|
|
app = FastAPI(
|
|
title="Chartbastan API",
|
|
description="""
|
|
API publique pour les prédictions de matchs basées sur l'énergie collective.
|
|
|
|
## Endpoints Publics
|
|
- **Prédictions**: Accès aux prédictions de matchs avec filtres
|
|
- **Matchs**: Liste des matchs avec filtres par ligue et statut
|
|
|
|
## Authentification
|
|
Les endpoints publics ne nécessitent pas d'authentification.
|
|
Pour un usage intensif, générez une clé API via le dashboard développeur.
|
|
""",
|
|
version="1.0.0",
|
|
docs_url="/docs",
|
|
redoc_url="/redoc",
|
|
openapi_url="/openapi.json"
|
|
)
|
|
|
|
# #region agent log
|
|
write_debug_log("A", "main.py:1", "FastAPI app initialized", {"app_title": "Chartbastan API"})
|
|
# #endregion
|
|
|
|
# Rate limiting middleware DÉSACTIVÉ PERMANENTMENT
|
|
# app.add_middleware(RateLimitMiddleware, public_limit=10, authenticated_limit=100)
|
|
|
|
# #region agent log
|
|
write_debug_log("E", "main.py:27", "RateLimitMiddleware DISABLED permanently", {"status": "disabled"})
|
|
# #endregion
|
|
|
|
# CORS configuration
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"], # Allow all origins for public API
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# #region agent log
|
|
write_debug_log("E", "main.py:35", "CORSMiddleware configured", {"allow_origins": ["*"], "allow_methods": ["*"], "allow_headers": ["*"]})
|
|
# #endregion
|
|
|
|
@app.middleware("http")
|
|
async def log_requests(request: Request, call_next):
|
|
"""Log toutes les requêtes entrantes pour diagnostic."""
|
|
start_time = datetime.now()
|
|
write_debug_log("A", "main.py:44", "Incoming request", {
|
|
"method": request.method,
|
|
"url": str(request.url),
|
|
"path": request.url.path,
|
|
"headers": dict(request.headers)
|
|
})
|
|
response = await call_next(request)
|
|
process_time = (datetime.now() - start_time).total_seconds()
|
|
write_debug_log("A", "main.py:44", "Request completed", {
|
|
"method": request.method,
|
|
"url": str(request.url),
|
|
"path": request.url.path,
|
|
"status_code": response.status_code,
|
|
"process_time": process_time
|
|
})
|
|
return response
|
|
|
|
@app.get("/")
|
|
def read_root():
|
|
"""Endpoint racine de l'API."""
|
|
write_debug_log("A", "main.py:58", "Root endpoint called", {"path": "/"})
|
|
return {"message": "Chartbastan API"}
|
|
|
|
@app.get("/health")
|
|
def health_check():
|
|
"""Endpoint de vérification de santé."""
|
|
return {"status": "healthy"}
|
|
|
|
|
|
# ============================================================
|
|
# AUTH ENDPOINTS - Définis directement ici pour garantir l'enregistrement
|
|
# ============================================================
|
|
from fastapi import Depends, HTTPException, status
|
|
from sqlalchemy.orm import Session
|
|
from pydantic import BaseModel, EmailStr
|
|
from typing import Optional
|
|
from passlib.context import CryptContext
|
|
|
|
from app.database import get_db
|
|
from app.models.user import User
|
|
|
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
|
|
|
|
class LoginRequest(BaseModel):
|
|
email: EmailStr
|
|
password: str
|
|
|
|
|
|
class RegisterRequest(BaseModel):
|
|
email: EmailStr
|
|
password: str
|
|
name: Optional[str] = None
|
|
referral_code: Optional[str] = None
|
|
|
|
|
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
|
return pwd_context.verify(plain_password, hashed_password)
|
|
|
|
|
|
def get_password_hash(password: str) -> str:
|
|
return pwd_context.hash(password)
|
|
|
|
|
|
@app.post("/api/v1/auth/login")
|
|
def login_user(request: LoginRequest, db: Session = Depends(get_db)):
|
|
"""Connecter un utilisateur."""
|
|
user = db.query(User).filter(User.email == request.email).first()
|
|
|
|
if not user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Email ou mot de passe incorrect"
|
|
)
|
|
|
|
if not user.password_hash:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Email ou mot de passe incorrect"
|
|
)
|
|
|
|
if not verify_password(request.password, user.password_hash):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Email ou mot de passe incorrect"
|
|
)
|
|
|
|
return {
|
|
"data": {
|
|
"id": user.id,
|
|
"email": user.email,
|
|
"name": user.name,
|
|
"is_premium": user.is_premium,
|
|
"referral_code": user.referral_code,
|
|
"created_at": str(user.created_at),
|
|
},
|
|
"meta": {
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"version": "v1"
|
|
}
|
|
}
|
|
|
|
|
|
@app.post("/api/v1/auth/register", status_code=status.HTTP_201_CREATED)
|
|
def register_user(request: RegisterRequest, db: Session = Depends(get_db)):
|
|
"""Inscrire un nouvel utilisateur."""
|
|
import secrets
|
|
import string
|
|
|
|
existing_user = db.query(User).filter(User.email == request.email).first()
|
|
if existing_user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail="Cet email est déjà utilisé"
|
|
)
|
|
|
|
# Générer code de parrainage
|
|
alphabet = string.ascii_uppercase + string.digits
|
|
referral_code = ''.join(secrets.choice(alphabet) for _ in range(8))
|
|
|
|
password_hash = get_password_hash(request.password)
|
|
|
|
new_user = User(
|
|
email=request.email,
|
|
password_hash=password_hash,
|
|
name=request.name,
|
|
is_premium=False,
|
|
referral_code=referral_code,
|
|
created_at=datetime.utcnow(),
|
|
updated_at=datetime.utcnow()
|
|
)
|
|
|
|
db.add(new_user)
|
|
db.commit()
|
|
db.refresh(new_user)
|
|
|
|
return {
|
|
"data": {
|
|
"id": new_user.id,
|
|
"email": new_user.email,
|
|
"name": new_user.name,
|
|
"is_premium": new_user.is_premium,
|
|
"referral_code": new_user.referral_code,
|
|
"created_at": str(new_user.created_at),
|
|
},
|
|
"meta": {
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"version": "v1"
|
|
}
|
|
}
|
|
|
|
|
|
@app.post("/api/v1/auth/logout")
|
|
def logout_user():
|
|
"""Déconnecter l'utilisateur."""
|
|
return {
|
|
"data": {"message": "Déconnexion réussie"},
|
|
"meta": {
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"version": "v1"
|
|
}
|
|
}
|
|
# ============================================================
|
|
# FIN AUTH ENDPOINTS
|
|
# ============================================================
|
|
|
|
# Include API routers
|
|
from app.api.v1 import users
|
|
from app.api.v1 import auth
|
|
from app.api.v1 import predictions
|
|
from app.api.v1 import backtesting
|
|
from app.api.v1 import leaderboard
|
|
from app.api.v1 import badges
|
|
from app.api.public.v1 import predictions as public_predictions
|
|
from app.api.public.v1 import matches as public_matches
|
|
|
|
# #region agent log
|
|
write_debug_log("C", "main.py:101", "Including routers", {
|
|
"routers": ["users", "auth", "predictions", "backtesting", "leaderboard", "badges", "public_predictions", "public_matches"]
|
|
})
|
|
# #endregion
|
|
|
|
app.include_router(users.router, prefix="/api/v1/users", tags=["users"])
|
|
app.include_router(auth.router, prefix="/api/v1/auth", tags=["auth"])
|
|
app.include_router(predictions.router)
|
|
app.include_router(backtesting.router)
|
|
app.include_router(leaderboard.router)
|
|
app.include_router(badges.router, prefix="/api/v1/badges", tags=["badges"])
|
|
|
|
# Public API routers
|
|
app.include_router(public_predictions.router)
|
|
app.include_router(public_matches.router)
|
|
|
|
# Trigger reload
|