Files
nuzlocke-tracker/backend/src/app/api/users.py

108 lines
2.9 KiB
Python
Raw Normal View History

from uuid import UUID
from fastapi import APIRouter, Depends
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.auth import AuthUser, require_auth
from app.core.database import get_session
from app.models.user import User
from app.schemas.base import CamelModel
router = APIRouter()
class UserResponse(CamelModel):
id: UUID
email: str
display_name: str | None = None
feat: auth-aware UI and role-based access control (#67) ## Summary - Add `is_admin` column to users table with Alembic migration and a `require_admin` FastAPI dependency that protects all admin-facing write endpoints (games, pokemon, evolutions, bosses, routes CRUD) - Expose admin status to frontend via user API and update AuthContext to fetch/store `isAdmin` after login - Make navigation menu auth-aware (different links for logged-out, logged-in, and admin users) and protect frontend routes with `ProtectedRoute` and `AdminRoute` components, preserving deep-linking through redirects - Fix test reliability: `drop_all` before `create_all` to clear stale PostgreSQL enums from interrupted test runs - Fix test auth: add `admin_client` fixture and use valid UUID for mock user so tests pass with new admin-protected endpoints ## Test plan - [x] All 252 backend tests pass - [ ] Verify non-admin users cannot access admin write endpoints (games, pokemon, evolutions, bosses CRUD) - [ ] Verify admin users can access admin endpoints normally - [ ] Verify navigation shows correct links for logged-out, logged-in, and admin states - [ ] Verify `/admin/*` routes redirect non-admin users with a toast - [ ] Verify `/runs/new` and `/genlockes/new` redirect unauthenticated users to login, then back after auth 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: https://gitea.nerdboden.de/pokemon/nuzlocke-tracker/pulls/67 Co-authored-by: Julian Tabel <juliantabel.jt@gmail.com> Co-committed-by: Julian Tabel <juliantabel.jt@gmail.com>
2026-03-21 11:44:05 +01:00
is_admin: bool = False
@router.post("/me", response_model=UserResponse)
async def sync_current_user(
session: AsyncSession = Depends(get_session),
auth_user: AuthUser = Depends(require_auth),
):
"""
Sync the current authenticated user from Supabase to local DB.
Creates user on first login, updates email if changed.
"""
user_id = UUID(auth_user.id)
result = await session.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if user is None:
# First login - create user record
user = User(
id=user_id,
email=auth_user.email or "",
display_name=None,
)
session.add(user)
elif auth_user.email and user.email != auth_user.email:
# Email changed in Supabase - update local record
user.email = auth_user.email
await session.commit()
await session.refresh(user)
return user
@router.get("/me", response_model=UserResponse)
async def get_current_user(
session: AsyncSession = Depends(get_session),
auth_user: AuthUser = Depends(require_auth),
):
"""Get the current authenticated user's profile."""
user_id = UUID(auth_user.id)
result = await session.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if user is None:
# Auto-create if not exists (shouldn't happen if /me POST is called on login)
user = User(
id=user_id,
email=auth_user.email or "",
display_name=None,
)
session.add(user)
await session.commit()
await session.refresh(user)
return user
class UserUpdateRequest(CamelModel):
display_name: str | None = None
@router.patch("/me", response_model=UserResponse)
async def update_current_user(
data: UserUpdateRequest,
session: AsyncSession = Depends(get_session),
auth_user: AuthUser = Depends(require_auth),
):
"""Update the current user's profile (display name)."""
user_id = UUID(auth_user.id)
result = await session.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if user is None:
user = User(
id=user_id,
email=auth_user.email or "",
display_name=data.display_name,
)
session.add(user)
else:
if data.display_name is not None:
user.display_name = data.display_name
await session.commit()
await session.refresh(user)
return user