SEC-16: Add frontend ports mapping (80:80) to docker-compose.yml UI-01: Add routes for Welcome, SettingsIntegrations, MealPlan in App.jsx SEC-01: Remove traceback leak from auth error responses SEC-02: Fix require_admin to use is_admin column (was undefined ADMIN_USERNAME) SEC-03: Add API_TOKEN auth for MCP connector -> backend communication SEC-04: OAuth state now uses signed JWT tokens (was raw user UUID) OS-01: First registered user gets admin immediately during registration 10 files changed, 97 insertions(+), 25 deletions(-)
88 lines
3.2 KiB
Python
88 lines
3.2 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Annotated
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from fastapi.responses import JSONResponse
|
|
from sqlalchemy import select, func
|
|
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
|
|
|
|
logger = logging.getLogger("fitness-rewards")
|
|
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
|
|
|
|
|
@router.post("/register")
|
|
async def register(req: RegisterRequest, db: Annotated[AsyncSession, Depends(get_db)]):
|
|
try:
|
|
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")
|
|
|
|
# Grant admin to first user
|
|
admin_count = await db.execute(select(func.count(User.id)).where(User.is_admin == True))
|
|
is_first_user = (admin_count.scalar() or 0) == 0
|
|
|
|
user = User(
|
|
username=req.username,
|
|
display_name=req.display_name,
|
|
password_hash=hash_password(req.password),
|
|
email=req.email,
|
|
is_admin=is_first_user,
|
|
)
|
|
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 {"access_token": token, "token_type": "bearer"}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Registration failed: {e}", exc_info=True)
|
|
return JSONResponse(status_code=500, content={"detail": "Internal server error"})
|
|
|
|
|
|
@router.post("/login")
|
|
async def login(req: LoginRequest, db: Annotated[AsyncSession, Depends(get_db)]):
|
|
try:
|
|
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 {"access_token": token, "token_type": "bearer"}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Login failed: {e}", exc_info=True)
|
|
return JSONResponse(status_code=500, content={"detail": "Internal server error"})
|
|
|
|
|
|
@router.get("/me")
|
|
async def get_me(user: Annotated[User, Depends(get_current_user)]):
|
|
return {
|
|
"id": str(user.id),
|
|
"username": user.username,
|
|
"display_name": user.display_name,
|
|
"email": user.email,
|
|
"timezone": user.timezone,
|
|
"is_admin": getattr(user, "is_admin", False),
|
|
}
|