Source code for user.models.tan

from __future__ import annotations

from datetime import datetime, timedelta
from uuid import uuid4, UUID

from onegov.core.orm import Base
from onegov.core.orm.mixins import ContentMixin, TimestampMixin
from sqlalchemy import Index
from sqlalchemy.ext.hybrid import hybrid_method
from sqlalchemy.orm import mapped_column, Mapped


from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from sqlalchemy.sql import ColumnElement


[docs] DEFAULT_EXPIRES_AFTER = timedelta(hours=1)
[docs] class TAN(Base, TimestampMixin, ContentMixin): """ A single use TAN for temporarily elevating access or to serve as a second authentication factor through e.g. a mobile phone number. """
[docs] __tablename__ = 'tans'
[docs] __table_args__ = ( # TimestampMixin by default does not generate an index for # the created column, so we do it here instead Index('ix_tans_created', 'created'), )
[docs] id: Mapped[UUID] = mapped_column( primary_key=True, default=uuid4 )
[docs] hashed_tan: Mapped[str] = mapped_column(index=True)
[docs] scope: Mapped[str] = mapped_column(index=True)
[docs] client: Mapped[str]
[docs] expired: Mapped[datetime | None] = mapped_column(index=True)
@hybrid_method
[docs] def is_active( self, expires_after: timedelta | None = DEFAULT_EXPIRES_AFTER ) -> bool: now = self.timestamp() if self.expired and self.expired <= now: return False if expires_after is not None: if now >= (self.created + expires_after): return False return True
@is_active.expression # type:ignore[no-redef] def is_active( cls, expires_after: timedelta | None = DEFAULT_EXPIRES_AFTER ) -> ColumnElement[bool]: now = cls.timestamp() expr = (cls.expired > now) | cls.expired.is_(None) if expires_after is not None: expr &= cls.created >= (now - expires_after) return expr
[docs] def expire(self) -> None: if self.expired: raise ValueError('TAN already expired') self.expired = self.timestamp()