feat: implement super admin and platform admin roles

Add multi-platform admin authorization system with:
- AdminPlatform junction table for admin-platform assignments
- is_super_admin flag on User model for global admin access
- Platform selection flow for platform admins after login
- JWT token updates to include platform context
- New API endpoints for admin user management (super admin only)
- Auth dependencies for super admin and platform access checks

Includes comprehensive test coverage:
- Unit tests for AdminPlatform model and User admin methods
- Unit tests for AdminPlatformService operations
- Integration tests for admin users API endpoints

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-24 18:44:49 +01:00
parent 7e39bb0564
commit 53e05dd497
18 changed files with 2792 additions and 6 deletions

View File

@@ -14,11 +14,14 @@ import logging
from fastapi import APIRouter, Depends, Response
from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api
from app.api.deps import get_current_admin_api, get_current_admin_from_cookie_or_header
from app.core.database import get_db
from app.core.environment import should_use_secure_cookies
from app.exceptions import InvalidCredentialsException
from app.exceptions import InsufficientPermissionsException, InvalidCredentialsException
from app.services.admin_platform_service import admin_platform_service
from app.services.auth_service import auth_service
from middleware.auth import AuthManager
from models.database.platform import Platform
from models.database.user import User
from models.schema.auth import LoginResponse, LogoutResponse, UserLogin, UserResponse
@@ -123,3 +126,99 @@ def admin_logout(response: Response):
logger.debug("Deleted admin_token cookies (both /admin and / paths)")
return LogoutResponse(message="Logged out successfully")
@router.get("/accessible-platforms")
def get_accessible_platforms(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin_from_cookie_or_header),
):
"""
Get list of platforms this admin can access.
Returns:
- For super admins: All active platforms
- For platform admins: Only assigned platforms
"""
if current_user.is_super_admin:
platforms = db.query(Platform).filter(Platform.is_active == True).all()
else:
platforms = admin_platform_service.get_platforms_for_admin(db, current_user.id)
return {
"platforms": [
{
"id": p.id,
"code": p.code,
"name": p.name,
"logo": p.logo,
}
for p in platforms
],
"is_super_admin": current_user.is_super_admin,
"requires_platform_selection": not current_user.is_super_admin and len(platforms) > 0,
}
@router.post("/select-platform")
def select_platform(
platform_id: int,
response: Response,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin_from_cookie_or_header),
):
"""
Select platform context for platform admin.
Issues a new JWT token with platform context.
Super admins skip this step (they have global access).
Args:
platform_id: Platform ID to select
Returns:
LoginResponse with new token containing platform context
"""
if current_user.is_super_admin:
raise InvalidCredentialsException(
"Super admins don't need platform selection - they have global access"
)
# Verify admin has access to this platform
if not current_user.can_access_platform(platform_id):
raise InsufficientPermissionsException(
f"You don't have access to this platform"
)
# Load platform
platform = db.query(Platform).filter(Platform.id == platform_id).first()
if not platform:
raise InvalidCredentialsException("Platform not found")
# Issue new token with platform context
auth_manager = AuthManager()
token_data = auth_manager.create_access_token(
user=current_user,
platform_id=platform.id,
platform_code=platform.code,
)
# Set cookie with new token
response.set_cookie(
key="admin_token",
value=token_data["access_token"],
httponly=True,
secure=should_use_secure_cookies(),
samesite="lax",
max_age=token_data["expires_in"],
path="/admin",
)
logger.info(f"Admin {current_user.username} selected platform {platform.code}")
return LoginResponse(
access_token=token_data["access_token"],
token_type=token_data["token_type"],
expires_in=token_data["expires_in"],
user=current_user,
)