1
|
|
|
"""User authentification """ |
2
|
|
|
# pylint: disable=no-name-in-module, no-self-argument |
3
|
|
|
import hashlib |
4
|
|
|
import os |
5
|
|
|
from datetime import datetime |
6
|
|
|
from typing import Literal, Optional |
7
|
|
|
|
8
|
|
|
from pydantic import (BaseModel, EmailStr, Field, ValidationInfo, |
9
|
|
|
field_validator) |
10
|
|
|
from typing_extensions import Annotated |
11
|
|
|
|
12
|
|
|
|
13
|
|
|
class DocumentBaseModel(BaseModel): |
14
|
|
|
"""DocumentBaseModel""" |
15
|
|
|
|
16
|
|
|
id: str = Field(None, alias="_id") |
17
|
|
|
inserted_at: Optional[datetime] = None |
18
|
|
|
updated_at: Optional[datetime] = None |
19
|
|
|
deleted_at: Optional[datetime] = None |
20
|
|
|
|
21
|
|
|
def model_dump(self, **kwargs) -> dict: |
22
|
|
|
"""Model to dict.""" |
23
|
|
|
values = super().model_dump(**kwargs) |
24
|
|
|
if "id" in values and values["id"]: |
25
|
|
|
values["_id"] = values["id"] |
26
|
|
|
if "exclude" in kwargs and "_id" in kwargs["exclude"]: |
27
|
|
|
values.pop("_id") |
28
|
|
|
return values |
29
|
|
|
|
30
|
|
|
|
31
|
|
|
def hashing(password: bytes, values: dict) -> str: |
32
|
|
|
"""Hash password and return it as string""" |
33
|
|
|
return hashlib.scrypt(password=password, salt=values['salt'], |
34
|
|
|
n=values['n'], r=values['r'], |
35
|
|
|
p=values['p']).hex() |
36
|
|
|
|
37
|
|
|
|
38
|
|
|
def validate_password(password: str, values: ValidationInfo): |
39
|
|
|
"""Check if password has at least a letter and a number""" |
40
|
|
|
upper = False |
41
|
|
|
lower = False |
42
|
|
|
number = False |
43
|
|
|
for char in password: |
44
|
|
|
if char.isupper(): |
45
|
|
|
upper = True |
46
|
|
|
if char.isnumeric(): |
47
|
|
|
number = True |
48
|
|
|
if char.islower(): |
49
|
|
|
lower = True |
50
|
|
|
if number and upper and lower: |
51
|
|
|
return hashing(password.encode(), values.data['hash'].model_dump()) |
52
|
|
|
raise ValueError('value should contain ' + |
53
|
|
|
'minimun 8 characters, ' + |
54
|
|
|
'at least one upper case character, ' + |
55
|
|
|
'at least 1 numeric character [0-9]') |
56
|
|
|
|
57
|
|
|
|
58
|
|
|
class HashSubDoc(BaseModel): |
59
|
|
|
"""HashSubDoc. Parameters for hash.scrypt function""" |
60
|
|
|
salt: bytes = Field(default=None, validate_default=True) |
61
|
|
|
n: int = 8192 |
62
|
|
|
r: int = 8 |
63
|
|
|
p: int = 1 |
64
|
|
|
|
65
|
|
|
@field_validator('salt', mode='before') |
66
|
|
|
@classmethod |
67
|
|
|
def create_salt(cls, salt): |
68
|
|
|
"""Create random salt value""" |
69
|
|
|
return salt or os.urandom(16) |
70
|
|
|
|
71
|
|
|
|
72
|
|
|
class UserDoc(DocumentBaseModel): |
73
|
|
|
"""UserDocumentModel.""" |
74
|
|
|
|
75
|
|
|
username: str = Field( |
76
|
|
|
min_length=1, max_length=64, pattern=r'^[a-zA-Z0-9_-]+$' |
77
|
|
|
) |
78
|
|
|
hash: HashSubDoc |
79
|
|
|
state: Literal['active', 'inactive'] = 'active' |
80
|
|
|
email: EmailStr |
81
|
|
|
password: str = Field(min_length=8, max_length=64) |
82
|
|
|
|
83
|
|
|
_validate_password = field_validator('password')(validate_password) |
84
|
|
|
|
85
|
|
|
@staticmethod |
86
|
|
|
def projection() -> dict: |
87
|
|
|
"""Base model for projection.""" |
88
|
|
|
return { |
89
|
|
|
"_id": 0, |
90
|
|
|
"username": 1, |
91
|
|
|
"email": 1, |
92
|
|
|
'password': 1, |
93
|
|
|
'hash': 1, |
94
|
|
|
'state': 1, |
95
|
|
|
'inserted_at': 1, |
96
|
|
|
'updated_at': 1, |
97
|
|
|
'deleted_at': 1 |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
@staticmethod |
101
|
|
|
def projection_nopw() -> dict: |
102
|
|
|
"""Model for projection without password""" |
103
|
|
|
return { |
104
|
|
|
"_id": 0, |
105
|
|
|
"username": 1, |
106
|
|
|
"email": 1, |
107
|
|
|
'state': 1, |
108
|
|
|
'inserted_at': 1, |
109
|
|
|
'updated_at': 1, |
110
|
|
|
'deleted_at': 1 |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
|
114
|
|
|
class UserDocUpdate(DocumentBaseModel): |
115
|
|
|
"UserDocUpdate use to validate data before updating" |
116
|
|
|
|
117
|
|
|
username: Optional[Annotated[str, |
118
|
|
|
Field(min_length=1, max_length=64, |
119
|
|
|
pattern=r'^[a-zA-Z0-9_-]+$')]] = None |
120
|
|
|
email: Optional[EmailStr] = None |
121
|
|
|
hash: Optional[HashSubDoc] = None |
122
|
|
|
password: Optional[Annotated[str, |
123
|
|
|
Field(min_length=8, max_length=64)]] = None |
124
|
|
|
|
125
|
|
|
_validate_password = field_validator('password')(validate_password) |
126
|
|
|
|