Backend (FastAPI): - User auth with email/password and Spotify OAuth - Spotify playlist import with audio feature extraction - AI recommendation engine using Claude API with taste profiling - Save/bookmark recommendations - Rate limiting for free tier (10 recs/day, 1 playlist) - PostgreSQL models with Alembic migrations - Redis-ready configuration Frontend (React 19 + TypeScript + Vite + Tailwind): - Landing page, auth flows (email + Spotify OAuth) - Dashboard with stats and quick discover - Playlist management and import from Spotify - Discover page with custom query support - Recommendation cards with explanations and save toggle - Taste profile visualization - Responsive layout with mobile navigation - PWA-ready configuration Infrastructure: - Docker Compose with PostgreSQL, Redis, backend, frontend - Environment-based configuration
94 lines
3.3 KiB
Python
94 lines
3.3 KiB
Python
import secrets
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.database import get_db
|
|
from app.core.security import hash_password, verify_password, create_access_token, get_current_user
|
|
from app.models.user import User
|
|
from app.schemas.auth import RegisterRequest, LoginRequest, TokenResponse, UserResponse
|
|
from app.services.spotify import get_spotify_auth_url, exchange_spotify_code, get_spotify_user
|
|
|
|
router = APIRouter(prefix="/auth", tags=["auth"])
|
|
|
|
|
|
@router.post("/register", response_model=TokenResponse)
|
|
async def register(data: RegisterRequest, db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(select(User).where(User.email == data.email))
|
|
if result.scalar_one_or_none():
|
|
raise HTTPException(status_code=400, detail="Email already registered")
|
|
|
|
user = User(
|
|
email=data.email,
|
|
name=data.name,
|
|
hashed_password=hash_password(data.password),
|
|
)
|
|
db.add(user)
|
|
await db.flush()
|
|
return TokenResponse(access_token=create_access_token(user.id))
|
|
|
|
|
|
@router.post("/login", response_model=TokenResponse)
|
|
async def login(data: LoginRequest, db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(select(User).where(User.email == data.email))
|
|
user = result.scalar_one_or_none()
|
|
if not user or not user.hashed_password or not verify_password(data.password, user.hashed_password):
|
|
raise HTTPException(status_code=401, detail="Invalid email or password")
|
|
return TokenResponse(access_token=create_access_token(user.id))
|
|
|
|
|
|
@router.get("/me", response_model=UserResponse)
|
|
async def get_me(user: User = Depends(get_current_user)):
|
|
return UserResponse(
|
|
id=user.id,
|
|
email=user.email,
|
|
name=user.name,
|
|
is_pro=user.is_pro,
|
|
spotify_connected=user.spotify_id is not None,
|
|
)
|
|
|
|
|
|
@router.get("/spotify/url")
|
|
async def spotify_auth_url():
|
|
state = secrets.token_urlsafe(32)
|
|
url = get_spotify_auth_url(state)
|
|
return {"url": url, "state": state}
|
|
|
|
|
|
@router.post("/spotify/callback", response_model=TokenResponse)
|
|
async def spotify_callback(code: str, db: AsyncSession = Depends(get_db)):
|
|
token_data = await exchange_spotify_code(code)
|
|
access_token = token_data["access_token"]
|
|
refresh_token = token_data.get("refresh_token")
|
|
|
|
spotify_user = await get_spotify_user(access_token)
|
|
spotify_id = spotify_user["id"]
|
|
email = spotify_user.get("email", f"{spotify_id}@spotify.user")
|
|
name = spotify_user.get("display_name") or spotify_id
|
|
|
|
# Check if user exists by spotify_id or email
|
|
result = await db.execute(select(User).where(User.spotify_id == spotify_id))
|
|
user = result.scalar_one_or_none()
|
|
|
|
if not user:
|
|
result = await db.execute(select(User).where(User.email == email))
|
|
user = result.scalar_one_or_none()
|
|
|
|
if user:
|
|
user.spotify_id = spotify_id
|
|
user.spotify_access_token = access_token
|
|
user.spotify_refresh_token = refresh_token or user.spotify_refresh_token
|
|
else:
|
|
user = User(
|
|
email=email,
|
|
name=name,
|
|
spotify_id=spotify_id,
|
|
spotify_access_token=access_token,
|
|
spotify_refresh_token=refresh_token,
|
|
)
|
|
db.add(user)
|
|
|
|
await db.flush()
|
|
return TokenResponse(access_token=create_access_token(user.id))
|