Passed
Push — master ( a65c88...e2c8b9 )
by Jochen
02:49
created

byceps.services.verification_token.models._generate_token_value()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
"""
2
byceps.services.verification_token.models
3
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
5
:Copyright: 2006-2019 Jochen Kupperschmidt
6
:License: Modified BSD, see LICENSE for details.
7
"""
8
9
from datetime import datetime, timedelta
10
from enum import Enum
11
import secrets
12
13
from sqlalchemy.ext.hybrid import hybrid_property
14
15
from ...database import BaseQuery, db
16
from ...typing import UserID
17
from ...util.instances import ReprBuilder
18
19
from ..user.models.user import User
20
21
22
Purpose = Enum('Purpose',
23
    ['email_address_confirmation', 'password_reset', 'terms_consent'])
24
25
26
def _generate_token_value():
27
    """Return a cryptographic, URL-safe token."""
28
    return secrets.token_urlsafe()
29
30
31
class TokenQuery(BaseQuery):
32
33
    def for_purpose(self, purpose) -> BaseQuery:
34
        return self.filter_by(_purpose=purpose.name)
35
36
37
class Token(db.Model):
38
    """A private token to authenticate as a certain user for a certain
39
    action.
40
    """
41
    __tablename__ = 'verification_tokens'
42
    query_class = TokenQuery
43
44
    token = db.Column(db.UnicodeText, default=_generate_token_value, primary_key=True)
45
    created_at = db.Column(db.DateTime, default=datetime.now, nullable=False)
46
    user_id = db.Column(db.Uuid, db.ForeignKey('users.id'), index=True, nullable=False)
47
    user = db.relationship(User)
48
    _purpose = db.Column('purpose', db.UnicodeText, index=True, nullable=False)
49
50
    def __init__(self, user_id: UserID, purpose: Purpose) -> None:
51
        self.user_id = user_id
52
        self.purpose = purpose
53
54
    @hybrid_property
55
    def purpose(self) -> Purpose:
56
        return Purpose[self._purpose]
57
58
    @purpose.setter
59
    def purpose(self, purpose: Purpose) -> None:
60
        assert purpose is not None
61
        self._purpose = purpose.name
62
63
    @property
64
    def is_expired(self) -> bool:
65
        """Return `True` if expired, i.e. it is no longer valid."""
66
        if self.purpose == Purpose.password_reset:
67
            now = datetime.now()
68
            expires_after = timedelta(hours=24)
69
            return now >= (self.created_at + expires_after)
70
        else:
71
            return False
72
73
    def __repr__(self) -> str:
74
        return ReprBuilder(self) \
75
            .add_with_lookup('token') \
76
            .add('user', self.user.screen_name) \
77
            .add('purpose', self.purpose.name) \
78
            .build()
79