~homeworkprod/byceps

ref: 4237b3ec9496efe95dcce82bea3207ab9de4d520 byceps/byceps/services/verification_token/models.py -rw-r--r-- 2.2 KiB
4237b3ec — Jochen Kupperschmidt Move ticketing blueprint into `site` subpackage 1 year, 11 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
"""
byceps.services.verification_token.models
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:Copyright: 2006-2020 Jochen Kupperschmidt
:License: Modified BSD, see LICENSE for details.
"""

from datetime import datetime, timedelta
from enum import Enum
import secrets

from sqlalchemy.ext.hybrid import hybrid_property

from ...database import BaseQuery, db
from ...typing import UserID
from ...util.instances import ReprBuilder


Purpose = Enum('Purpose',
    ['email_address_confirmation', 'password_reset', 'terms_consent'])


def _generate_token_value():
    """Return a cryptographic, URL-safe token."""
    return secrets.token_urlsafe()


class TokenQuery(BaseQuery):

    def for_purpose(self, purpose) -> BaseQuery:
        return self.filter_by(_purpose=purpose.name)


class Token(db.Model):
    """A private token to authenticate as a certain user for a certain
    action.
    """

    __tablename__ = 'verification_tokens'
    query_class = TokenQuery

    token = db.Column(db.UnicodeText, default=_generate_token_value, primary_key=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
    user_id = db.Column(db.Uuid, db.ForeignKey('users.id'), index=True, nullable=False)
    _purpose = db.Column('purpose', db.UnicodeText, index=True, nullable=False)

    def __init__(self, user_id: UserID, purpose: Purpose) -> None:
        self.user_id = user_id
        self.purpose = purpose

    @hybrid_property
    def purpose(self) -> Purpose:
        return Purpose[self._purpose]

    @purpose.setter
    def purpose(self, purpose: Purpose) -> None:
        assert purpose is not None
        self._purpose = purpose.name

    @property
    def is_expired(self) -> bool:
        """Return `True` if expired, i.e. it is no longer valid."""
        if self.purpose == Purpose.password_reset:
            now = datetime.utcnow()
            expires_after = timedelta(hours=24)
            return now >= (self.created_at + expires_after)
        else:
            return False

    def __repr__(self) -> str:
        return ReprBuilder(self) \
            .add_with_lookup('token') \
            .add('user', self.user.screen_name) \
            .add('purpose', self.purpose.name) \
            .build()