Files
vynl/backend/app/api/endpoints/auth.py
root db2767bfda Add user settings page and PWA install support
- Add profile update, password change, and account deletion endpoints
- Create Settings page with profile editing, password change, and danger zone
- Add Settings link to user dropdown menu in Layout
- Add /settings route to App.tsx
- Add API functions for profile management
- Create PWA manifest.json and add meta tags to index.html
2026-03-31 20:49:57 -05:00

131 lines
4.8 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 pydantic import BaseModel, EmailStr
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))
class UpdateProfileRequest(BaseModel):
name: str | None = None
email: EmailStr | None = None
class ChangePasswordRequest(BaseModel):
current_password: str
new_password: str
@router.put("/me")
async def update_profile(data: UpdateProfileRequest, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
if data.name:
user.name = data.name
if data.email and data.email != user.email:
existing = await db.execute(select(User).where(User.email == data.email))
if existing.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Email already in use")
user.email = data.email
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.post("/change-password")
async def change_password(data: ChangePasswordRequest, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
if not user.hashed_password or not verify_password(data.current_password, user.hashed_password):
raise HTTPException(status_code=400, detail="Current password is incorrect")
user.hashed_password = hash_password(data.new_password)
return {"ok": True}
@router.delete("/me")
async def delete_account(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
await db.delete(user)
return {"ok": True}
@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))