- 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
66 lines
2.4 KiB
Python
66 lines
2.4 KiB
Python
"""User, Role, and UserRole models — supports multiple admin roles."""
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import (
|
|
Boolean,
|
|
Column,
|
|
DateTime,
|
|
ForeignKey,
|
|
Integer,
|
|
String,
|
|
Table,
|
|
Text,
|
|
UniqueConstraint,
|
|
)
|
|
from sqlalchemy.orm import DeclarativeBase, relationship
|
|
|
|
from app.models import Base
|
|
|
|
|
|
# ── Many-to-many: users ↔ roles ──────────────────────────────────────────────
|
|
|
|
user_roles = Table(
|
|
"user_roles",
|
|
Base.metadata,
|
|
Column("user_id", Integer, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True),
|
|
Column("role_id", Integer, ForeignKey("roles.id", ondelete="CASCADE"), primary_key=True),
|
|
)
|
|
|
|
|
|
# ── Role ─────────────────────────────────────────────────────────────────────
|
|
|
|
class Role(Base):
|
|
__tablename__ = "roles"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
name = Column(String(50), unique=True, nullable=False)
|
|
description = Column(Text, nullable=True)
|
|
|
|
users = relationship("User", secondary=user_roles, back_populates="roles")
|
|
|
|
|
|
# ── User ─────────────────────────────────────────────────────────────────────
|
|
|
|
class User(Base):
|
|
__tablename__ = "users"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
email = Column(String(255), unique=True, nullable=False)
|
|
display_name = Column(String(100), nullable=False)
|
|
avatar_url = Column(String(500), nullable=True)
|
|
auth_provider = Column(String(20), nullable=False, default="google")
|
|
password_hash = Column(String(255), nullable=True)
|
|
is_admin = Column(Boolean, default=False)
|
|
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
|
|
|
# Role-based admin (many-to-many)
|
|
roles = relationship("Role", secondary=user_roles, back_populates="users")
|
|
|
|
@property
|
|
def is_admin_effective(self) -> bool:
|
|
"""True when is_admin=True (legacy) OR the user has an 'admin' role."""
|
|
if self.is_admin:
|
|
return True
|
|
return any(role.name == "admin" for role in self.roles)
|