## 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: #67 Co-authored-by: Julian Tabel <juliantabel.jt@gmail.com> Co-committed-by: Julian Tabel <juliantabel.jt@gmail.com>
108 lines
2.9 KiB
Python
108 lines
2.9 KiB
Python
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
|
|
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
|