feat: add role-based admin support for multiple admin users
- Add Role model and user_roles association table (many-to-many) - Add password_hash column to User model (nullable, for future bcrypt) - Keep is_admin boolean for backward compatibility - Add is_admin_effective property: true if legacy is_admin OR 'admin' role - Update auth dependency (get_current_admin_user) to use is_admin_effective - Update bootstrap login to auto-create 'admin' role and assign on login - Update Google OAuth login to use is_admin_effective for token creation - Add migrate_roles.py: idempotent migration script to backfill roles - Add unit tests for Role, User, association table, and is_admin_effective
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
Migration seed: create the 'admin' role and assign it to existing is_admin=True users.
|
||||
|
||||
Run once after deploying the updated schema. Idempotent — safe to run multiple times.
|
||||
|
||||
Usage:
|
||||
KMTN_DATABASE_URL=... python migrate_roles.py
|
||||
(run from the backend/ directory)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from sqlalchemy import select, text
|
||||
|
||||
from app.database import async_session
|
||||
from app.user_models import User, Role, user_roles
|
||||
|
||||
|
||||
async def migrate() -> None:
|
||||
"""Ensure 'admin' role exists and assign to existing is_admin=True users."""
|
||||
async with async_session() as session:
|
||||
# 1) Create 'admin' role if it doesn't exist
|
||||
result = await session.execute(select(Role).where(Role.name == "admin"))
|
||||
admin_role = result.scalar_one_or_none()
|
||||
if admin_role is None:
|
||||
admin_role = Role(name="admin", description="Full administrative access")
|
||||
session.add(admin_role)
|
||||
await session.commit()
|
||||
await session.refresh(admin_role)
|
||||
print(f" ✓ Created 'admin' role (id={admin_role.id})")
|
||||
else:
|
||||
print(f" ✓ 'admin' role already exists (id={admin_role.id})")
|
||||
|
||||
# 2) Assign 'admin' role to all existing is_admin=True users
|
||||
result = await session.execute(
|
||||
select(User).where(User.is_admin == True) # noqa: E712
|
||||
)
|
||||
admin_users = result.scalars().all()
|
||||
migrated = 0
|
||||
for user in admin_users:
|
||||
if admin_role not in user.roles:
|
||||
user.roles.append(admin_role)
|
||||
migrated += 1
|
||||
if migrated:
|
||||
await session.commit()
|
||||
print(f" ✓ Assigned 'admin' role to {migrated} existing admin user(s)")
|
||||
else:
|
||||
print(" ✓ No new role assignments needed")
|
||||
|
||||
# 3) Summary
|
||||
result = await session.execute(
|
||||
select(User).where(user_roles.c.role_id == admin_role.id)
|
||||
)
|
||||
role_admins = result.scalars().all()
|
||||
print(f" → {len(role_admins)} user(s) now have the 'admin' role")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Running role migration...")
|
||||
asyncio.run(migrate())
|
||||
print("Done.")
|
||||
Reference in New Issue
Block a user