Full-stack mobile app for pole dance championship management. Backend: FastAPI + SQLAlchemy 2 (async) + SQLite (dev) / PostgreSQL (prod) - JWT auth with refresh token rotation - Championship CRUD with Instagram Graph API sync (APScheduler) - Registration flow with status management - Participant list publish with Expo push notifications - Alembic migrations, pytest test suite Mobile: React Native + Expo (TypeScript) - Auth gate: pending approval screen for new members - Championships list & detail screens - Registration form with status tracking - React Query + Zustand + React Navigation v6 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
78 lines
2.9 KiB
Python
78 lines
2.9 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.crud import crud_refresh_token, crud_user
|
|
from app.database import get_db
|
|
from app.dependencies import get_current_user
|
|
from app.models.user import User
|
|
from app.schemas.auth import LoginRequest, RefreshRequest, RegisterRequest, TokenResponse, UserOut
|
|
from app.services.auth_service import (
|
|
create_access_token,
|
|
create_refresh_token,
|
|
hash_token,
|
|
verify_password,
|
|
)
|
|
|
|
router = APIRouter(prefix="/auth", tags=["auth"])
|
|
|
|
|
|
@router.post("/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED)
|
|
async def register(body: RegisterRequest, db: AsyncSession = Depends(get_db)):
|
|
if await crud_user.get_by_email(db, body.email):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT, detail="Email already registered"
|
|
)
|
|
user = await crud_user.create(
|
|
db,
|
|
email=body.email,
|
|
password=body.password,
|
|
full_name=body.full_name,
|
|
phone=body.phone,
|
|
)
|
|
return await _issue_tokens(db, user)
|
|
|
|
|
|
@router.post("/login", response_model=TokenResponse)
|
|
async def login(body: LoginRequest, db: AsyncSession = Depends(get_db)):
|
|
user = await crud_user.get_by_email(db, body.email)
|
|
if not user or not verify_password(body.password, user.hashed_password):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials"
|
|
)
|
|
return await _issue_tokens(db, user)
|
|
|
|
|
|
@router.post("/refresh", response_model=TokenResponse)
|
|
async def refresh(body: RefreshRequest, db: AsyncSession = Depends(get_db)):
|
|
hashed = hash_token(body.refresh_token)
|
|
rt = await crud_refresh_token.get_by_hash(db, hashed)
|
|
if not rt or not crud_refresh_token.is_valid(rt):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or expired refresh token"
|
|
)
|
|
await crud_refresh_token.revoke(db, rt)
|
|
user = await crud_user.get(db, rt.user_id)
|
|
if not user:
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
|
return await _issue_tokens(db, user)
|
|
|
|
|
|
@router.post("/logout", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def logout(body: RefreshRequest, db: AsyncSession = Depends(get_db)):
|
|
hashed = hash_token(body.refresh_token)
|
|
rt = await crud_refresh_token.get_by_hash(db, hashed)
|
|
if rt:
|
|
await crud_refresh_token.revoke(db, rt)
|
|
|
|
|
|
@router.get("/me", response_model=UserOut)
|
|
async def me(user: User = Depends(get_current_user)):
|
|
return user
|
|
|
|
|
|
async def _issue_tokens(db: AsyncSession, user: User) -> TokenResponse:
|
|
access = create_access_token(str(user.id), user.role, user.status)
|
|
raw_rt, hashed_rt, expires_at = create_refresh_token()
|
|
await crud_refresh_token.create(db, user.id, hashed_rt, expires_at)
|
|
return TokenResponse(access_token=access, refresh_token=raw_rt)
|