266 lines
7.9 KiB
Python
266 lines
7.9 KiB
Python
"""
|
|
API Anime Tracker - Synchronisation multi-appareils
|
|
"""
|
|
from fastapi import FastAPI, Depends, HTTPException, status
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
from sqlalchemy.orm import Session
|
|
from datetime import datetime, timedelta
|
|
from typing import Optional, List
|
|
import os
|
|
from dotenv import load_dotenv
|
|
|
|
from database import get_db, init_db
|
|
from models import User, Anime
|
|
from schemas import (
|
|
UserCreate, UserLogin, UserResponse, Token,
|
|
AnimeCreate, AnimeUpdate, AnimeResponse
|
|
)
|
|
from auth import (
|
|
verify_password, get_password_hash,
|
|
create_access_token, verify_token,
|
|
get_current_user
|
|
)
|
|
|
|
load_dotenv()
|
|
|
|
app = FastAPI(
|
|
title="Anime Tracker API",
|
|
description="API de synchronisation pour l'extension Anime Tracker",
|
|
version="1.0.0"
|
|
)
|
|
|
|
# CORS pour permettre les requêtes depuis l'extension Chrome
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"], # En production, spécifier les domaines autorisés
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
security = HTTPBearer()
|
|
|
|
# Initialiser la base de données au démarrage
|
|
@app.on_event("startup")
|
|
async def startup_event():
|
|
init_db()
|
|
|
|
|
|
# ==================== AUTHENTIFICATION ====================
|
|
|
|
@app.post("/api/auth/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
|
async def register(user_data: UserCreate, db: Session = Depends(get_db)):
|
|
"""Inscription d'un nouvel utilisateur"""
|
|
# Vérifier si l'utilisateur existe déjà
|
|
existing_user = db.query(User).filter(User.email == user_data.email).first()
|
|
if existing_user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Un utilisateur avec cet email existe déjà"
|
|
)
|
|
|
|
# Vérifier si le nom d'utilisateur existe déjà
|
|
existing_username = db.query(User).filter(User.username == user_data.username).first()
|
|
if existing_username:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Ce nom d'utilisateur est déjà pris"
|
|
)
|
|
|
|
# Créer le nouvel utilisateur
|
|
hashed_password = get_password_hash(user_data.password)
|
|
new_user = User(
|
|
username=user_data.username,
|
|
email=user_data.email,
|
|
hashed_password=hashed_password
|
|
)
|
|
|
|
db.add(new_user)
|
|
db.commit()
|
|
db.refresh(new_user)
|
|
|
|
return UserResponse(
|
|
id=new_user.id,
|
|
username=new_user.username,
|
|
email=new_user.email,
|
|
created_at=new_user.created_at
|
|
)
|
|
|
|
|
|
@app.post("/api/auth/login", response_model=Token)
|
|
async def login(credentials: UserLogin, db: Session = Depends(get_db)):
|
|
"""Connexion d'un utilisateur"""
|
|
# Trouver l'utilisateur par email ou username
|
|
user = db.query(User).filter(
|
|
(User.email == credentials.email_or_username) |
|
|
(User.username == credentials.email_or_username)
|
|
).first()
|
|
|
|
if not user or not verify_password(credentials.password, user.hashed_password):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Email/username ou mot de passe incorrect",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
# Créer le token JWT
|
|
access_token = create_access_token(data={"sub": str(user.id)})
|
|
|
|
return Token(access_token=access_token, token_type="bearer")
|
|
|
|
|
|
@app.get("/api/auth/me", response_model=UserResponse)
|
|
async def get_current_user_info(current_user: User = Depends(get_current_user)):
|
|
"""Récupérer les informations de l'utilisateur connecté"""
|
|
return UserResponse(
|
|
id=current_user.id,
|
|
username=current_user.username,
|
|
email=current_user.email,
|
|
created_at=current_user.created_at
|
|
)
|
|
|
|
|
|
# ==================== ANIMES ====================
|
|
|
|
@app.get("/api/animes", response_model=List[AnimeResponse])
|
|
async def get_animes(
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Récupérer tous les animés de l'utilisateur"""
|
|
animes = db.query(Anime).filter(Anime.user_id == current_user.id).all()
|
|
return [AnimeResponse.from_orm(anime) for anime in animes]
|
|
|
|
|
|
@app.post("/api/animes", response_model=AnimeResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_anime(
|
|
anime_data: AnimeCreate,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Créer ou mettre à jour un animé"""
|
|
# Vérifier si l'animé existe déjà pour cet utilisateur
|
|
existing_anime = db.query(Anime).filter(
|
|
Anime.user_id == current_user.id,
|
|
Anime.anime_id == anime_data.anime_id
|
|
).first()
|
|
|
|
if existing_anime:
|
|
# Mettre à jour l'animé existant
|
|
for key, value in anime_data.dict(exclude_unset=True).items():
|
|
setattr(existing_anime, key, value)
|
|
existing_anime.last_updated = datetime.utcnow()
|
|
db.commit()
|
|
db.refresh(existing_anime)
|
|
return AnimeResponse.from_orm(existing_anime)
|
|
else:
|
|
# Créer un nouvel animé
|
|
new_anime = Anime(
|
|
user_id=current_user.id,
|
|
**anime_data.dict()
|
|
)
|
|
db.add(new_anime)
|
|
db.commit()
|
|
db.refresh(new_anime)
|
|
return AnimeResponse.from_orm(new_anime)
|
|
|
|
|
|
@app.put("/api/animes/{anime_id}", response_model=AnimeResponse)
|
|
async def update_anime(
|
|
anime_id: str,
|
|
anime_data: AnimeUpdate,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Mettre à jour un animé spécifique"""
|
|
anime = db.query(Anime).filter(
|
|
Anime.user_id == current_user.id,
|
|
Anime.anime_id == anime_id
|
|
).first()
|
|
|
|
if not anime:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Animé non trouvé"
|
|
)
|
|
|
|
for key, value in anime_data.dict(exclude_unset=True).items():
|
|
setattr(anime, key, value)
|
|
|
|
anime.last_updated = datetime.utcnow()
|
|
db.commit()
|
|
db.refresh(anime)
|
|
|
|
return AnimeResponse.from_orm(anime)
|
|
|
|
|
|
@app.delete("/api/animes/{anime_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_anime(
|
|
anime_id: str,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Supprimer un animé"""
|
|
anime = db.query(Anime).filter(
|
|
Anime.user_id == current_user.id,
|
|
Anime.anime_id == anime_id
|
|
).first()
|
|
|
|
if not anime:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Animé non trouvé"
|
|
)
|
|
|
|
db.delete(anime)
|
|
db.commit()
|
|
return None
|
|
|
|
|
|
@app.post("/api/animes/sync", response_model=List[AnimeResponse])
|
|
async def sync_animes(
|
|
animes_data: List[AnimeCreate],
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Synchroniser plusieurs animés en une seule requête"""
|
|
synced_animes = []
|
|
|
|
for anime_data in animes_data:
|
|
existing_anime = db.query(Anime).filter(
|
|
Anime.user_id == current_user.id,
|
|
Anime.anime_id == anime_data.anime_id
|
|
).first()
|
|
|
|
if existing_anime:
|
|
# Mettre à jour
|
|
for key, value in anime_data.dict(exclude_unset=True).items():
|
|
setattr(existing_anime, key, value)
|
|
existing_anime.last_updated = datetime.utcnow()
|
|
else:
|
|
# Créer
|
|
new_anime = Anime(
|
|
user_id=current_user.id,
|
|
**anime_data.dict()
|
|
)
|
|
db.add(new_anime)
|
|
existing_anime = new_anime
|
|
|
|
synced_animes.append(existing_anime)
|
|
|
|
db.commit()
|
|
|
|
for anime in synced_animes:
|
|
db.refresh(anime)
|
|
|
|
return [AnimeResponse.from_orm(anime) for anime in synced_animes]
|
|
|
|
|
|
@app.get("/api/health")
|
|
async def health_check():
|
|
"""Vérification de l'état de l'API"""
|
|
return {"status": "ok", "message": "API Anime Tracker is running"}
|
|
|
|
|