arbret/backend/models.py

168 lines
5.8 KiB
Python
Raw Normal View History

2025-12-18 23:33:32 +01:00
from datetime import datetime, UTC
from enum import Enum as PyEnum
2025-12-19 00:12:43 +01:00
from typing import TypedDict
2025-12-18 23:33:32 +01:00
from sqlalchemy import Integer, String, Float, DateTime, ForeignKey, Table, Column, Enum, select
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.ext.asyncio import AsyncSession
2025-12-18 21:48:41 +01:00
from database import Base
2025-12-19 00:12:43 +01:00
class RoleConfig(TypedDict):
description: str
permissions: list["Permission"]
2025-12-18 23:33:32 +01:00
class Permission(str, PyEnum):
"""All available permissions in the system."""
# Counter permissions
VIEW_COUNTER = "view_counter"
INCREMENT_COUNTER = "increment_counter"
# Sum permissions
USE_SUM = "use_sum"
# Audit permissions
VIEW_AUDIT = "view_audit"
2025-12-18 21:48:41 +01:00
2025-12-18 23:33:32 +01:00
2025-12-19 00:12:43 +01:00
# Role name constants
ROLE_ADMIN = "admin"
ROLE_REGULAR = "regular"
2025-12-18 23:33:32 +01:00
# Role definitions with their permissions
2025-12-19 00:12:43 +01:00
ROLE_DEFINITIONS: dict[str, RoleConfig] = {
ROLE_ADMIN: {
2025-12-18 23:33:32 +01:00
"description": "Administrator with audit access",
"permissions": [
Permission.VIEW_AUDIT,
],
},
2025-12-19 00:12:43 +01:00
ROLE_REGULAR: {
2025-12-18 23:33:32 +01:00
"description": "Regular user with counter and sum access",
"permissions": [
Permission.VIEW_COUNTER,
Permission.INCREMENT_COUNTER,
Permission.USE_SUM,
],
},
}
# Association table: Role <-> Permission (many-to-many)
role_permissions = Table(
"role_permissions",
Base.metadata,
Column("role_id", Integer, ForeignKey("roles.id", ondelete="CASCADE"), primary_key=True),
Column("permission", Enum(Permission), primary_key=True),
)
# Association table: User <-> Role (many-to-many)
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),
)
class Role(Base):
__tablename__ = "roles"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
description: Mapped[str] = mapped_column(String(255), nullable=True)
# Relationship to users
2025-12-19 00:12:43 +01:00
users: Mapped[list["User"]] = relationship(
2025-12-18 23:33:32 +01:00
"User",
secondary=user_roles,
back_populates="roles",
)
2025-12-19 00:12:43 +01:00
async def get_permissions(self, db: AsyncSession) -> set[Permission]:
2025-12-18 23:33:32 +01:00
"""Get all permissions for this role."""
result = await db.execute(
select(role_permissions.c.permission).where(role_permissions.c.role_id == self.id)
)
return {row[0] for row in result.fetchall()}
2025-12-19 00:12:43 +01:00
async def set_permissions(self, db: AsyncSession, permissions: list[Permission]) -> None:
2025-12-18 23:33:32 +01:00
"""Set all permissions for this role (replaces existing)."""
await db.execute(role_permissions.delete().where(role_permissions.c.role_id == self.id))
for perm in permissions:
await db.execute(role_permissions.insert().values(role_id=self.id, permission=perm))
2025-12-18 21:48:41 +01:00
2025-12-18 22:08:31 +01:00
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
2025-12-18 23:33:32 +01:00
2025-12-19 10:12:55 +01:00
# Contact details (all optional)
contact_email: Mapped[str | None] = mapped_column(String(255), nullable=True)
telegram: Mapped[str | None] = mapped_column(String(64), nullable=True)
signal: Mapped[str | None] = mapped_column(String(64), nullable=True)
2025-12-19 10:30:23 +01:00
nostr_npub: Mapped[str | None] = mapped_column(String(63), nullable=True)
2025-12-19 10:12:55 +01:00
2025-12-18 23:33:32 +01:00
# Relationship to roles
2025-12-19 00:12:43 +01:00
roles: Mapped[list[Role]] = relationship(
2025-12-18 23:33:32 +01:00
"Role",
secondary=user_roles,
back_populates="users",
lazy="selectin",
)
2025-12-19 00:12:43 +01:00
async def get_permissions(self, db: AsyncSession) -> set[Permission]:
"""Get all permissions from all roles in a single query."""
result = await db.execute(
select(role_permissions.c.permission)
.join(user_roles, role_permissions.c.role_id == user_roles.c.role_id)
.where(user_roles.c.user_id == self.id)
)
return {row[0] for row in result.fetchall()}
2025-12-18 23:33:32 +01:00
async def has_permission(self, db: AsyncSession, permission: Permission) -> bool:
"""Check if user has a specific permission through any of their roles."""
permissions = await self.get_permissions(db)
return permission in permissions
@property
2025-12-19 00:12:43 +01:00
def role_names(self) -> list[str]:
2025-12-18 23:33:32 +01:00
"""Get list of role names for API responses."""
return [role.name for role in self.roles]
class Counter(Base):
__tablename__ = "counter"
id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1)
value: Mapped[int] = mapped_column(Integer, default=0)
2025-12-18 22:08:31 +01:00
2025-12-18 22:51:43 +01:00
class SumRecord(Base):
__tablename__ = "sum_records"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False, index=True)
a: Mapped[float] = mapped_column(Float, nullable=False)
b: Mapped[float] = mapped_column(Float, nullable=False)
result: Mapped[float] = mapped_column(Float, nullable=False)
2025-12-18 23:33:32 +01:00
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(UTC)
)
2025-12-18 22:51:43 +01:00
class CounterRecord(Base):
__tablename__ = "counter_records"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False, index=True)
value_before: Mapped[int] = mapped_column(Integer, nullable=False)
value_after: Mapped[int] = mapped_column(Integer, nullable=False)
2025-12-18 23:33:32 +01:00
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(UTC)
)