diligence/backend/app/routers/auth.py
Claude 69320e1e82 Fix all 7 launch blockers from security/QA review
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(-)
2026-06-16 01:50:09 +00:00

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),
}