from __future__ import annotations from datetime import datetime, timezone from enum import Enum from flask_login import UserMixin from sqlalchemy import Boolean, DateTime, String from sqlalchemy.orm import Mapped, mapped_column, relationship from werkzeug.security import check_password_hash, generate_password_hash from app.extensions import db def utcnow() -> datetime: return datetime.now(timezone.utc) class Role(str, Enum): ADMIN = "admin" OPERATOR = "operator" VIEWER = "viewer" @property def label(self) -> str: return self.value.title() class Account(UserMixin, db.Model): __tablename__ = "accounts" id: Mapped[int] = mapped_column(primary_key=True) username: Mapped[str] = mapped_column(String(80), unique=True, index=True, nullable=False) email: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False) full_name: Mapped[str] = mapped_column(String(160), nullable=False) role: Mapped[str] = mapped_column(String(20), default=Role.VIEWER.value, nullable=False) password_hash: Mapped[str] = mapped_column(String(255), nullable=False) active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, nullable=False) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, onupdate=utcnow, nullable=False, ) attendance_events = relationship("AttendanceEvent", back_populates="created_by") @property def is_active(self) -> bool: return self.active def set_password(self, password: str) -> None: self.password_hash = generate_password_hash(password, method="scrypt") def check_password(self, password: str) -> bool: return check_password_hash(self.password_hash, password) def has_role(self, *roles: str | Role) -> bool: normalized = {role.value if isinstance(role, Role) else role for role in roles} return self.role in normalized @property def is_admin(self) -> bool: return self.role == Role.ADMIN.value @property def can_operate(self) -> bool: return self.role in {Role.ADMIN.value, Role.OPERATOR.value}