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

@@ -0,0 +1,148 @@
"""Add admin platform roles (super admin + platform admin)
Revision ID: z9j0k1l2m3n4
Revises: z8i9j0k1l2m3
Create Date: 2026-01-24
Adds support for super admin and platform admin roles:
- is_super_admin column on users table
- admin_platforms junction table for platform admin assignments
Super admins have access to all platforms.
Platform admins are assigned to specific platforms via admin_platforms.
Existing admins are migrated to super admins for backward compatibility.
"""
from datetime import UTC, datetime
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "z9j0k1l2m3n4"
down_revision = "z8i9j0k1l2m3"
branch_labels = None
depends_on = None
def upgrade() -> None:
# 1. Add is_super_admin column to users table
op.add_column(
"users",
sa.Column(
"is_super_admin",
sa.Boolean(),
nullable=False,
server_default="false",
comment="Whether this admin has access to all platforms (super admin)",
),
)
# 2. Create admin_platforms junction table
op.create_table(
"admin_platforms",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column(
"user_id",
sa.Integer(),
nullable=False,
comment="Reference to the admin user",
),
sa.Column(
"platform_id",
sa.Integer(),
nullable=False,
comment="Reference to the platform",
),
sa.Column(
"is_active",
sa.Boolean(),
nullable=False,
server_default="true",
comment="Whether the admin assignment is active",
),
sa.Column(
"assigned_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.func.now(),
comment="When the admin was assigned to this platform",
),
sa.Column(
"assigned_by_user_id",
sa.Integer(),
nullable=True,
comment="Super admin who made this assignment",
),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.func.now(),
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.func.now(),
onupdate=sa.func.now(),
),
sa.ForeignKeyConstraint(
["user_id"],
["users.id"],
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["platform_id"],
["platforms.id"],
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["assigned_by_user_id"],
["users.id"],
ondelete="SET NULL",
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("user_id", "platform_id", name="uq_admin_platform"),
)
# Create indexes for performance
op.create_index(
"idx_admin_platforms_user_id",
"admin_platforms",
["user_id"],
)
op.create_index(
"idx_admin_platforms_platform_id",
"admin_platforms",
["platform_id"],
)
op.create_index(
"idx_admin_platform_active",
"admin_platforms",
["user_id", "platform_id", "is_active"],
)
op.create_index(
"idx_admin_platform_user_active",
"admin_platforms",
["user_id", "is_active"],
)
# 3. Migrate existing admins to super admins for backward compatibility
# All current admins get super admin access to maintain their existing permissions
op.execute("UPDATE users SET is_super_admin = TRUE WHERE role = 'admin'")
def downgrade() -> None:
# Drop indexes
op.drop_index("idx_admin_platform_user_active", table_name="admin_platforms")
op.drop_index("idx_admin_platform_active", table_name="admin_platforms")
op.drop_index("idx_admin_platforms_platform_id", table_name="admin_platforms")
op.drop_index("idx_admin_platforms_user_id", table_name="admin_platforms")
# Drop admin_platforms table
op.drop_table("admin_platforms")
# Drop is_super_admin column
op.drop_column("users", "is_super_admin")