All checks were successful
CI / lint-and-test (push) Successful in 22s
- Introduced a new roles table in the database to manage user roles ('user' and 'admin') for access control.
- Updated the user model to include a foreign key reference to the roles table, allowing for role assignment.
- Enhanced command handlers to support the `/set_role` command for admins to assign roles to users.
- Refactored access control logic to utilize role checks instead of username/phone allowlists, improving security and maintainability.
- Updated documentation to reflect changes in access control mechanisms and role management.
- Added unit tests to ensure correct functionality of role assignment and access checks.
87 lines
3.2 KiB
Python
87 lines
3.2 KiB
Python
"""SQLAlchemy ORM models for users and duties."""
|
|
|
|
from sqlalchemy import Boolean, ForeignKey, Integer, BigInteger, Text
|
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
|
|
|
|
|
class Base(DeclarativeBase):
|
|
"""Declarative base for all models."""
|
|
|
|
pass
|
|
|
|
|
|
class Role(Base):
|
|
"""Role for access control: 'user' (miniapp access), 'admin' (admin actions)."""
|
|
|
|
__tablename__ = "roles"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
name: Mapped[str] = mapped_column(Text, nullable=False, unique=True)
|
|
|
|
users: Mapped[list["User"]] = relationship("User", back_populates="role")
|
|
|
|
|
|
class User(Base):
|
|
"""Telegram user and display name; may have telegram_user_id=None for import-only users."""
|
|
|
|
__tablename__ = "users"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
telegram_user_id: Mapped[int | None] = mapped_column(
|
|
BigInteger, unique=True, nullable=True
|
|
)
|
|
full_name: Mapped[str] = mapped_column(Text, nullable=False)
|
|
username: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
first_name: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
last_name: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
phone: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
name_manually_edited: Mapped[bool] = mapped_column(
|
|
Boolean, nullable=False, server_default="0", default=False
|
|
)
|
|
role_id: Mapped[int | None] = mapped_column(
|
|
Integer, ForeignKey("roles.id"), nullable=True
|
|
)
|
|
|
|
role: Mapped["Role | None"] = relationship("Role", back_populates="users")
|
|
duties: Mapped[list["Duty"]] = relationship("Duty", back_populates="user")
|
|
|
|
|
|
class CalendarSubscriptionToken(Base):
|
|
"""One active calendar subscription token per user; token_hash is unique."""
|
|
|
|
__tablename__ = "calendar_subscription_tokens"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
user_id: Mapped[int] = mapped_column(
|
|
Integer, ForeignKey("users.id"), nullable=False
|
|
)
|
|
token_hash: Mapped[str] = mapped_column(Text, nullable=False, unique=True)
|
|
created_at: Mapped[str] = mapped_column(Text, nullable=False)
|
|
|
|
|
|
class Duty(Base):
|
|
"""Single duty/unavailable/vacation slot (UTC start_at/end_at, event_type)."""
|
|
|
|
__tablename__ = "duties"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
user_id: Mapped[int] = mapped_column(
|
|
Integer, ForeignKey("users.id"), nullable=False
|
|
)
|
|
# UTC, ISO 8601 with Z suffix (e.g. 2025-01-15T09:00:00Z)
|
|
start_at: Mapped[str] = mapped_column(Text, nullable=False)
|
|
end_at: Mapped[str] = mapped_column(Text, nullable=False)
|
|
# duty | unavailable | vacation
|
|
event_type: Mapped[str] = mapped_column(Text, nullable=False, server_default="duty")
|
|
|
|
user: Mapped["User"] = relationship("User", back_populates="duties")
|
|
|
|
|
|
class GroupDutyPin(Base):
|
|
"""Stores which message to update in each group for the pinned duty notice."""
|
|
|
|
__tablename__ = "group_duty_pins"
|
|
|
|
chat_id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
|
message_id: Mapped[int] = mapped_column(Integer, nullable=False)
|