Initial commit: full fitness-rewards app
Backend: FastAPI + PostgreSQL + SQLAlchemy async - Auth (JWT), onboarding (PAR-Q+, TTM, BREQ-2), activities, food log - Points engine with daily gate + weekly targets - Strava + Polar OAuth integration - Open Food Facts barcode lookup - Resource recommendation engine - 10 seeded Darebee/StrongLifts/YouTube programs Frontend: React + Vite, mobile-first dark theme - Login/Register, 8-step onboarding flow - Dashboard with daily gate status - Activity logger, food logger (manual + barcode scan + search) - Reward shop with redemption - Weekly summary view - Settings (point rules, targets, integrations) Deployment: Docker Compose for Coolify (Traefik)
This commit is contained in:
commit
4db2b0846b
61 changed files with 4280 additions and 0 deletions
65
backend/app/routers/auth.py
Normal file
65
backend/app/routers/auth.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
from typing import Annotated
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
from app.models.profile import UserProfile
|
||||
from app.models.points import PointRule, DailyTarget, DEFAULT_POINT_RULES, TTM_DAILY_TARGETS
|
||||
from app.schemas.auth import LoginRequest, RegisterRequest, TokenResponse, UserResponse
|
||||
from app.utils.auth import hash_password, verify_password, create_access_token, get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
|
||||
|
||||
@router.post("/register", response_model=TokenResponse)
|
||||
async def register(req: RegisterRequest, db: Annotated[AsyncSession, Depends(get_db)]):
|
||||
existing = await db.execute(select(User).where(User.username == req.username))
|
||||
if existing.scalar_one_or_none():
|
||||
raise HTTPException(status_code=400, detail="Username already taken")
|
||||
|
||||
user = User(
|
||||
username=req.username,
|
||||
display_name=req.display_name,
|
||||
password_hash=hash_password(req.password),
|
||||
email=req.email,
|
||||
)
|
||||
db.add(user)
|
||||
await db.flush()
|
||||
|
||||
# Create empty profile
|
||||
profile = UserProfile(user_id=user.id)
|
||||
db.add(profile)
|
||||
|
||||
# Create default point rules
|
||||
for rule_data in DEFAULT_POINT_RULES:
|
||||
db.add(PointRule(user_id=user.id, **rule_data))
|
||||
|
||||
# Create default daily targets
|
||||
db.add(DailyTarget(user_id=user.id))
|
||||
|
||||
await db.flush()
|
||||
token = create_access_token(str(user.id))
|
||||
return TokenResponse(access_token=token)
|
||||
|
||||
|
||||
@router.post("/login", response_model=TokenResponse)
|
||||
async def login(req: LoginRequest, db: Annotated[AsyncSession, Depends(get_db)]):
|
||||
result = await db.execute(select(User).where(User.username == req.username))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user or not verify_password(req.password, user.password_hash):
|
||||
raise HTTPException(status_code=401, detail="Invalid username or password")
|
||||
token = create_access_token(str(user.id))
|
||||
return TokenResponse(access_token=token)
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
async def get_me(user: Annotated[User, Depends(get_current_user)]):
|
||||
return UserResponse(
|
||||
id=str(user.id),
|
||||
username=user.username,
|
||||
display_name=user.display_name,
|
||||
email=user.email,
|
||||
timezone=user.timezone,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue