jknife 0.0.1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jknife/__init__.py +0 -0
- jknife/commands/__init__.py +0 -0
- jknife/commands/jknife.py +376 -0
- jknife/db/__init__.py +142 -0
- jknife/db/models/__init__.py +0 -0
- jknife/db/models/mongo/__init__.py +57 -0
- jknife/db/models/mongo/network.py +27 -0
- jknife/db/models/mongo/personnel_info.py +108 -0
- jknife/db/models/mongo/settings.py +58 -0
- jknife/db/models/mongo/token.py +62 -0
- jknife/db/models/mongo/users.py +91 -0
- jknife/db/models/rdbms/__init__.py +53 -0
- jknife/db/models/rdbms/network.py +24 -0
- jknife/db/models/rdbms/personnel_info.py +88 -0
- jknife/db/models/rdbms/settings.py +39 -0
- jknife/db/models/rdbms/token.py +53 -0
- jknife/db/models/rdbms/users.py +94 -0
- jknife/dependencies/__init__.py +0 -0
- jknife/dependencies/token.py +202 -0
- jknife/dependencies/users.py +76 -0
- jknife/logging.py +69 -0
- jknife/views/__init__.py +23 -0
- jknife/views/error_message.py +38 -0
- jknife/views/personnel_info.py +42 -0
- jknife/views/tokens.py +22 -0
- jknife/views/users.py +70 -0
- jknife-0.0.1.dist-info/METADATA +17 -0
- jknife-0.0.1.dist-info/RECORD +31 -0
- jknife-0.0.1.dist-info/WHEEL +5 -0
- jknife-0.0.1.dist-info/entry_points.txt +2 -0
- jknife-0.0.1.dist-info/licenses/LICENSE +7 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# import packages from default or pip library
|
|
2
|
+
from datetime import date
|
|
3
|
+
from typing_extensions import Annotated, Doc
|
|
4
|
+
from mongoengine import Document, DateField, StringField, EmailField
|
|
5
|
+
from fastapi.exceptions import RequestValidationError
|
|
6
|
+
from pycountry import countries as pycountries
|
|
7
|
+
|
|
8
|
+
# import packages from this framework
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# define Class for Common SQLModel
|
|
12
|
+
class AddressMixin(Document):
|
|
13
|
+
meta = {'abstract': True}
|
|
14
|
+
|
|
15
|
+
address: Annotated[str,
|
|
16
|
+
Doc("Address.")] = StringField(null=False)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BirthdateMixin(Document):
|
|
20
|
+
meta = {'abstract': True}
|
|
21
|
+
|
|
22
|
+
birthdate: Annotated[date,
|
|
23
|
+
Doc("User's Birthdate.")] = DateField(null=False)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CPNumMixin(Document):
|
|
27
|
+
meta = {'abstract': True}
|
|
28
|
+
|
|
29
|
+
cp_num: Annotated[str,
|
|
30
|
+
Doc("cellphone number.")] = StringField(null=False)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class EmailMixin(Document):
|
|
34
|
+
meta = {'abstract': True}
|
|
35
|
+
|
|
36
|
+
email: Annotated[str,
|
|
37
|
+
Doc("email address. this can replace username for signup and signin.")] = EmailField(null=False, unique=True)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class FirstnameMixin(Document):
|
|
41
|
+
meta = {'abstract': True}
|
|
42
|
+
|
|
43
|
+
firstname: Annotated[str,
|
|
44
|
+
Doc("First name of application user.")] = StringField(null=False)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class LastnameMixin(Document):
|
|
48
|
+
meta = {'abstract': True}
|
|
49
|
+
|
|
50
|
+
lastname: Annotated[str,
|
|
51
|
+
Doc("Last name of application user.")] = StringField(null=False)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class NationMixin(Document):
|
|
55
|
+
meta = {'abstract': True}
|
|
56
|
+
|
|
57
|
+
nation: Annotated[str,
|
|
58
|
+
Doc("the nation that user came from.")] = StringField(null=False)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class NicknameMixin(Document):
|
|
62
|
+
meta = {'abstract': True}
|
|
63
|
+
|
|
64
|
+
nickname: Annotated[str,
|
|
65
|
+
Doc("User's nickname that used in this application.")] = StringField(unique=True)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class PostalCodeMixin(Document):
|
|
69
|
+
meta = {'abstract': True}
|
|
70
|
+
|
|
71
|
+
postal_code: Annotated[str,
|
|
72
|
+
Doc("postal number of address.")] = StringField(null=False)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# define function to control MongoDB document event
|
|
76
|
+
# please follow the procedure below
|
|
77
|
+
# create event handler function with name starting with _.
|
|
78
|
+
# event handler function must have 3 args: sender, document, **kwargs)
|
|
79
|
+
# each db column can get from 'document'
|
|
80
|
+
# import pre_init or pre_save from 'mongoengine.signals' in your model file.
|
|
81
|
+
# pre_init.connect(EVNET_HANDLER_FUNC_NAME, sender=TABLE_CLASS_NAME)
|
|
82
|
+
def _convert_country_to_alpha2(sender, document, **kwargs):
|
|
83
|
+
if document.nation and len(document.nation) != 2:
|
|
84
|
+
try:
|
|
85
|
+
document.nation = pycountries.get(name=document.nation).alpha_2
|
|
86
|
+
except AttributeError:
|
|
87
|
+
raise RequestValidationError(errors={"input": "nation", "msg": "please input official country name."})
|
|
88
|
+
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
def _convert_country_to_alpha3(sender, document, **kwargs):
|
|
92
|
+
if document.nation and len(document.nation) != 3:
|
|
93
|
+
try:
|
|
94
|
+
document.nation = pycountries.get(name=document.nation).alpha_3
|
|
95
|
+
except AttributeError:
|
|
96
|
+
raise RequestValidationError(errors={"input": "nation", "msg": "please input official country name."})
|
|
97
|
+
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
def _lower_firstname(sender, document, **kwargs) -> None:
|
|
101
|
+
if document.firstname:
|
|
102
|
+
document.firstname = document.firstname.lower()
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
def _lower_lastname(sender, document, **kwargs) -> None:
|
|
106
|
+
if document.lastname:
|
|
107
|
+
document.lastname = document.lastname.lower()
|
|
108
|
+
return None
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# import packages from default or pip library
|
|
2
|
+
from pycountry import languages as pylanguage
|
|
3
|
+
from typing_extensions import Annotated, Doc
|
|
4
|
+
from mongoengine import Document, BooleanField, IntField, StringField
|
|
5
|
+
from fastapi.exceptions import RequestValidationError
|
|
6
|
+
|
|
7
|
+
# import packages from this framework
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# define Class for Common SQLModel
|
|
11
|
+
class AllowMaxAccessSettingMixin(Document):
|
|
12
|
+
meta = {'abstract': True}
|
|
13
|
+
|
|
14
|
+
allow_max_access: Annotated[int,
|
|
15
|
+
Doc("user setting for max access during trying sign in.")] = IntField(null=False, default=5, min_value=3, max_value=5)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LanguageSettingMixin(Document):
|
|
19
|
+
meta = {'abstract': True}
|
|
20
|
+
|
|
21
|
+
language: Annotated[str,
|
|
22
|
+
Doc("language setting for application user.")] = StringField(null=False)
|
|
23
|
+
|
|
24
|
+
_alpha_code_type: Annotated[str,
|
|
25
|
+
Doc("assign 'alpha_2' or 'alpha_3'. default is 'alpha_2'")] = "alpha_2"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DarkModeSettingMixin(Document):
|
|
29
|
+
meta = {'abstract': True}
|
|
30
|
+
|
|
31
|
+
is_dark_mode: Annotated[bool,
|
|
32
|
+
Doc("light or dark mode for application user.")] = BooleanField(null=False, default=False)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# define function to control MongoDB document event
|
|
36
|
+
# please follow the procedure below
|
|
37
|
+
# create event handler function with name starting with _.
|
|
38
|
+
# event handler function must have 3 args: sender, document, **kwargs)
|
|
39
|
+
# each db column can get from 'document'
|
|
40
|
+
# import pre_init or pre_save from 'mongoengine.signals' in your model file.
|
|
41
|
+
# pre_init.connect(EVNET_HANDLER_FUNC_NAME, sender=TABLE_CLASS_NAME)
|
|
42
|
+
def _convert_language_to_alpha2(sender, document, **kwargs):
|
|
43
|
+
if document.language and len(document.language) != 2:
|
|
44
|
+
try:
|
|
45
|
+
document.language = pylanguage.get(name=document.language).alpha_2
|
|
46
|
+
except AttributeError:
|
|
47
|
+
raise RequestValidationError(errors={"input": "nation", "msg": "please input official country name."})
|
|
48
|
+
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
def _convert_language_to_alpha3(sender, document, **kwargs):
|
|
52
|
+
if document.language and len(document.language) != 3:
|
|
53
|
+
try:
|
|
54
|
+
document.language = pylanguage.get(name=document.language).alpha_3
|
|
55
|
+
except AttributeError:
|
|
56
|
+
raise RequestValidationError(errors={"input": "nation", "msg": "please input official country name."})
|
|
57
|
+
|
|
58
|
+
return None
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# import packages from default or pip library
|
|
2
|
+
from datetime import datetime, timedelta, timezone
|
|
3
|
+
from typing_extensions import Annotated, Doc
|
|
4
|
+
from mongoengine import Document, DateTimeField, StringField, pre_init
|
|
5
|
+
|
|
6
|
+
# import packages from this framework
|
|
7
|
+
from settings import AUTHENTICATION
|
|
8
|
+
|
|
9
|
+
# settings
|
|
10
|
+
TOKEN_VALID_TIME: int = AUTHENTICATION.get("token").get("token_valid_time")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# define classes
|
|
14
|
+
class AccessTokenMixin(Document):
|
|
15
|
+
meta = {'abstract': True}
|
|
16
|
+
|
|
17
|
+
access_token: Annotated[str,
|
|
18
|
+
Doc("This is a access token that will be stored in DB or somewhere")] = StringField(primary_key=True,
|
|
19
|
+
null=False,
|
|
20
|
+
unique=True)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TokenValidDateTime(Document):
|
|
24
|
+
meta = {'abstract': True}
|
|
25
|
+
|
|
26
|
+
issued_dt: Annotated[datetime,
|
|
27
|
+
Doc("datetime when the token issued.")] = DateTimeField(null=False)
|
|
28
|
+
expiration_dt: Annotated[datetime,
|
|
29
|
+
Doc("datetime when the token will be expired.")] = DateTimeField(null=False)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RefreshTokenMixin(Document):
|
|
33
|
+
meta = {'abstract': True}
|
|
34
|
+
|
|
35
|
+
refresh_token: Annotated[str,
|
|
36
|
+
Doc("This is a refresh token that will be used for issuing access token again.")] = StringField(null=False,
|
|
37
|
+
unique=True)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class JWTTokens(RefreshTokenMixin, AccessTokenMixin):
|
|
41
|
+
meta = {'abstract': True}
|
|
42
|
+
|
|
43
|
+
token_type: Annotated[str,
|
|
44
|
+
Doc("Token type that used in this class.")] = StringField(null=False,
|
|
45
|
+
default="Bearer")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# define function to control MongoDB document event
|
|
49
|
+
# please follow the procedure below
|
|
50
|
+
# create event handler function with name starting with _.
|
|
51
|
+
# event handler function must have 3 args: sender, document, **kwargs)
|
|
52
|
+
# each db column can get from 'document'
|
|
53
|
+
# import pre_init or pre_save from 'mongoengine.signals' in your model file.
|
|
54
|
+
# pre_init.connect(EVNET_HANDLER_FUNC_NAME, sender=TABLE_CLASS_NAME)
|
|
55
|
+
def _fill_token_datetime_field(sender, document, **kwargs) -> None:
|
|
56
|
+
if document.issued_dt is None:
|
|
57
|
+
document.issued_dt = datetime.now(tz=timezone.utc)
|
|
58
|
+
|
|
59
|
+
if document.expiration_dt is None:
|
|
60
|
+
document.expiration_dt = document.issued_dt + timedelta(minutes=TOKEN_VALID_TIME)
|
|
61
|
+
|
|
62
|
+
return None
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# import packages from default or pip library
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from typing_extensions import Annotated, Doc
|
|
4
|
+
from mongoengine import Document, BooleanField, DateTimeField, IntField, StringField
|
|
5
|
+
|
|
6
|
+
# import packages from this framework
|
|
7
|
+
from src.jknife.db.models.rdbms.users import encrypt_password
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# define Class for Common SQLModel
|
|
11
|
+
class LastSigninDateTimeMixin(Document):
|
|
12
|
+
meta = {'abstract': True}
|
|
13
|
+
|
|
14
|
+
last_signin_dt: Annotated[datetime,
|
|
15
|
+
Doc("User's last signin datetime.")] = DateTimeField(null=True, default=None)
|
|
16
|
+
|
|
17
|
+
def update_signin_dt(self) -> None:
|
|
18
|
+
self.last_signin_dt = datetime.now(tz=timezone.utc)
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SigninFailMixin(Document):
|
|
23
|
+
meta = {'abstract': True}
|
|
24
|
+
|
|
25
|
+
is_active: Annotated[bool,
|
|
26
|
+
Doc("show whether the user is activated or not.")] = BooleanField(null=False, default=False)
|
|
27
|
+
signin_fail: Annotated[int,
|
|
28
|
+
Doc("If the user fail to login, this value will be incremented.")] = IntField(null=False, default=0, ge=0, le=5)
|
|
29
|
+
|
|
30
|
+
def init_signin_fail_count(self) -> None:
|
|
31
|
+
self.signin_fail = 0
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
def activate_user(self) -> None:
|
|
35
|
+
self.is_active = True
|
|
36
|
+
self.init_signin_fail_count()
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
def deactivate_user(self) -> None:
|
|
40
|
+
self.is_active = False
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
def add_signin_fail_count(self, limit: int) -> None:
|
|
44
|
+
if self.signin_fail < limit:
|
|
45
|
+
self.signin_fail += 1
|
|
46
|
+
|
|
47
|
+
if self.signin_fail == limit:
|
|
48
|
+
self.deactivate_user()
|
|
49
|
+
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class IsAdminMixin(Document):
|
|
54
|
+
meta = {'abstract': True}
|
|
55
|
+
|
|
56
|
+
is_admin: Annotated[bool,
|
|
57
|
+
Doc("show whether the user is admin or not.")] = BooleanField(null=False, default=False)
|
|
58
|
+
|
|
59
|
+
def grant_admin(self) -> None:
|
|
60
|
+
self.is_admin = True
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
def revoke_admin(self) -> None:
|
|
64
|
+
self.is_admin = False
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class PasswordMixin(Document):
|
|
69
|
+
meta = {'abstract': True}
|
|
70
|
+
|
|
71
|
+
password: Annotated[str,
|
|
72
|
+
Doc("password for application user. it is recommended to store password after encrypting.")] = StringField(null=False, unique=False, min_length=32)
|
|
73
|
+
|
|
74
|
+
def encrypt_password(self, enc_type:str="sha256"):
|
|
75
|
+
self.password = encrypt_password(password=self.password, enc_type=enc_type)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class UsernameMixin(Document):
|
|
79
|
+
meta = {'abstract': True}
|
|
80
|
+
|
|
81
|
+
username: Annotated[str,
|
|
82
|
+
Doc("username for application user. if you want to replace it to email, refer to 'contact' module.")] = StringField(null=False)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# define function to control MongoDB document event
|
|
86
|
+
# please follow the procedure below
|
|
87
|
+
# create event handler function with name starting with _.
|
|
88
|
+
# event handler function must have 3 args: sender, document, **kwargs)
|
|
89
|
+
# each db column can get from 'document'
|
|
90
|
+
# import pre_init or pre_save from 'mongoengine.signals' in your model file.
|
|
91
|
+
# pre_init.connect(EVNET_HANDLER_FUNC_NAME, sender=TABLE_CLASS_NAME)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# import packages from default or pip library
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from typing_extensions import (Annotated,
|
|
4
|
+
Doc,
|
|
5
|
+
Optional)
|
|
6
|
+
from uuid import UUID, uuid4
|
|
7
|
+
from sqlmodel import (SQLModel,
|
|
8
|
+
Field)
|
|
9
|
+
|
|
10
|
+
# import packages from this framework
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# define Class for Common SQLModel
|
|
14
|
+
class IdMixin(SQLModel):
|
|
15
|
+
id: Annotated[int,
|
|
16
|
+
Doc("Default integer id for each table row.")] = Field(primary_key=True, default=None)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class UUIDMixin(SQLModel):
|
|
20
|
+
id: Annotated[UUID,
|
|
21
|
+
Doc("UUID format id for each table row.")] = Field(primary_key=True, default=None)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RegisterDateTimeMixin(SQLModel):
|
|
25
|
+
register_dt: Annotated[datetime,
|
|
26
|
+
Doc("Datetime that the row was added at.")] = Field(nullable=False, default=None)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class UpdateDateTimeMixin(SQLModel):
|
|
30
|
+
update_dt: Annotated[Optional[datetime],
|
|
31
|
+
Doc("Datetime that the row was updated at.")] = Field(nullable=True, default=None)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# define function to control SQL event
|
|
35
|
+
# please follow the procedure below
|
|
36
|
+
# use decorator 'event.listens_for()' for event handling function
|
|
37
|
+
# event handler function must have 3 args: mapper, connection and target)
|
|
38
|
+
# in your model file, import listens_for from 'sqlalchemy.event'
|
|
39
|
+
# event.listens_for(target=TARGET_TABLE_CLASS_NAME, identifier=['before_insert', 'before_update',...])
|
|
40
|
+
def _assign_register_datetime(mapper, connection, target) -> None:
|
|
41
|
+
if target.register_dt is None:
|
|
42
|
+
target.register_dt = datetime.now(tz=timezone.utc)
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
def _assign_update_datetime(mapper, connection, target) -> None:
|
|
46
|
+
if target.update_dt is not None:
|
|
47
|
+
target.update_dt = datetime.now(tz=timezone.utc)
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
def _assign_uuid(mapper, connection, target) -> None:
|
|
51
|
+
if target.id is None:
|
|
52
|
+
target.id = uuid4()
|
|
53
|
+
return None
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# import packages from default or pip library
|
|
2
|
+
from typing_extensions import Annotated, Doc
|
|
3
|
+
from sqlmodel import SQLModel, Field
|
|
4
|
+
|
|
5
|
+
# import packages from this framework
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# define Class for Common SQLModel
|
|
9
|
+
class AllowIPsMixin(SQLModel):
|
|
10
|
+
allow_ips: Annotated[list[str],
|
|
11
|
+
Doc("Set IPv4 or IPv6 addresses or networks to allow user to access")] = Field(nullable=False,
|
|
12
|
+
default=["127.0.0.1"])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# define function to control SQL event
|
|
16
|
+
# please follow the procedure below
|
|
17
|
+
# use decorator 'event.listens_for()' for event handling function
|
|
18
|
+
# event handler function must have 3 args: mapper, connection and target)
|
|
19
|
+
# in your model file, import listens_for from 'sqlalchemy.event'
|
|
20
|
+
# event.listens_for(target=TARGET_TABLE_CLASS_NAME, identifier=['before_insert', 'before_update',...])
|
|
21
|
+
def _convert_ips_to_str(mapper, connection, target) -> None:
|
|
22
|
+
if target.allow_ips is not None:
|
|
23
|
+
target.allow_ips = [ str(ip) for ip in target.allow_ips ]
|
|
24
|
+
return None
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# import packages from default or pip library
|
|
2
|
+
from datetime import date
|
|
3
|
+
from typing_extensions import Annotated, Doc
|
|
4
|
+
from sqlmodel import SQLModel, Field
|
|
5
|
+
from fastapi.exceptions import RequestValidationError
|
|
6
|
+
from pycountry import countries as pycountries
|
|
7
|
+
|
|
8
|
+
# import packages from this framework
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# define Class for Common SQLModel
|
|
12
|
+
class AddressMixin(SQLModel):
|
|
13
|
+
address: Annotated[str,
|
|
14
|
+
Doc("Address.")] = Field(nullable=False)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BirthdateMixin(SQLModel):
|
|
18
|
+
birthdate: Annotated[date,
|
|
19
|
+
Doc("User's Birthdate.")] = Field(nullable=False)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CPNumMixin(SQLModel):
|
|
23
|
+
cp_num: Annotated[str,
|
|
24
|
+
Doc("cellphone number.")] = Field(nullable=False)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class EmailMixin(SQLModel):
|
|
28
|
+
email: Annotated[str,
|
|
29
|
+
Doc("email address. this can replace username for signup and signin.")] = Field(nullable=False, unique=True)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FirstnameMixin(SQLModel):
|
|
33
|
+
firstname: Annotated[str,
|
|
34
|
+
Doc("First name of application user.")] = Field(nullable=False, index=True)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class LastnameMixin(SQLModel):
|
|
38
|
+
lastname: Annotated[str,
|
|
39
|
+
Doc("Last name of application user.")] = Field(nullable=False, index=True)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class NationMixin(SQLModel):
|
|
43
|
+
nation: Annotated[str,
|
|
44
|
+
Doc("the nation that user came from.")] = Field(nullable=False)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class NicknameMixin(SQLModel):
|
|
48
|
+
nickname: Annotated[str,
|
|
49
|
+
Doc("User's nickname that used in this application.")] = Field(unique=True, index=True)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class PostalCodeMixin(SQLModel):
|
|
53
|
+
postal_code: Annotated[str,
|
|
54
|
+
Doc("postal number of address.")] = Field(nullable=False)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# define function to control SQL event
|
|
58
|
+
# please follow the procedure below
|
|
59
|
+
# use decorator 'event.listens_for()' for event handling function
|
|
60
|
+
# event handler function must have 3 args: mapper, connection and target)
|
|
61
|
+
# in your model file, import listens_for from 'sqlalchemy.event'
|
|
62
|
+
# event.listens_for(target=TARGET_TABLE_CLASS_NAME, identifier=['before_insert', 'before_update',...])
|
|
63
|
+
def _convert_country_to_alpha2(mapper, connection, target):
|
|
64
|
+
if target.nation is not None and len(target.nation) != 2:
|
|
65
|
+
try:
|
|
66
|
+
target.nation = pycountries.get(name=target.nation).alpha_2
|
|
67
|
+
except AttributeError:
|
|
68
|
+
raise RequestValidationError(errors={"input": "nation", "msg": "please input official country name."})
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
def _convert_country_to_alpha3(mapper, connection, target):
|
|
72
|
+
if target.nation is not None and len(target.nation) != 3:
|
|
73
|
+
try:
|
|
74
|
+
target.nation = pycountries.get(name=target.nation).alpha_3
|
|
75
|
+
except AttributeError:
|
|
76
|
+
raise RequestValidationError(errors={"input": "nation", "msg": "please input official country name."})
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
def _lower_firstname(mapper, connection, target) -> None:
|
|
80
|
+
if target.firstname is not None:
|
|
81
|
+
target.firstname = target.firstname.lower()
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
def _lower_lastname(mapper, connection, target) -> None:
|
|
85
|
+
if target.lastname is not None:
|
|
86
|
+
target.lastname = target.lastname.lower()
|
|
87
|
+
return None
|
|
88
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# import packages from default or pip library
|
|
2
|
+
from pycountry import languages as pylanguage
|
|
3
|
+
from typing_extensions import Annotated, Doc
|
|
4
|
+
from sqlmodel import SQLModel, Field
|
|
5
|
+
|
|
6
|
+
# import packages from this framework
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# define Class for Common SQLModel
|
|
10
|
+
class AllowMaxAccessSettingMixin(SQLModel):
|
|
11
|
+
allow_max_access: Annotated[int,
|
|
12
|
+
Doc("user setting for max access during trying sign in.")] = Field(nullable=False, default=5, ge=3, le=5)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LanguageSettingMixin(SQLModel):
|
|
16
|
+
language: Annotated[str,
|
|
17
|
+
Doc("language setting for application user.")] = Field(nullable=False)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DarkModeSettingMixin(SQLModel):
|
|
21
|
+
is_dark_mode: Annotated[bool,
|
|
22
|
+
Doc("light or dark mode for application user.")] = Field(nullable=False, default=False)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# define function to control SQL event
|
|
26
|
+
# please follow the procedure below
|
|
27
|
+
# use decorator 'event.listens_for()' for event handling function
|
|
28
|
+
# event handler function must have 3 args: mapper, connection and target)
|
|
29
|
+
# in your model file, import listens_for from 'sqlalchemy.event'
|
|
30
|
+
# event.listens_for(target=TARGET_TABLE_CLASS_NAME, identifier=['before_insert', 'before_update',...])
|
|
31
|
+
def _convert_language_to_alpha2(mapper, connection, target) -> None:
|
|
32
|
+
if target.language is not None:
|
|
33
|
+
target.language = pylanguage.get(name=target.language).alpha_2
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
def _convert_language_to_alpha3(mapper, connection, target) -> None:
|
|
37
|
+
if target.language is not None:
|
|
38
|
+
target.language = pylanguage.get(name=target.language).alpha_3
|
|
39
|
+
return None
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# import packages from default or pip library
|
|
2
|
+
from datetime import datetime, timedelta, timezone
|
|
3
|
+
from typing_extensions import Annotated, Doc
|
|
4
|
+
from sqlmodel import SQLModel, Field
|
|
5
|
+
|
|
6
|
+
# import packages from this framework
|
|
7
|
+
from settings import AUTHENTICATION
|
|
8
|
+
|
|
9
|
+
# settings
|
|
10
|
+
TOKEN_VALID_TIME: int = AUTHENTICATION.get("token").get("token_valid_time")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# define classes
|
|
14
|
+
class AccessTokenMixin(SQLModel):
|
|
15
|
+
access_token: Annotated[str,
|
|
16
|
+
Doc("This is a access token that will be stored in DB or somewhere")] = Field(primary_key=True,
|
|
17
|
+
nullable=False,
|
|
18
|
+
unique=True)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RefreshTokenMixin(SQLModel):
|
|
22
|
+
refresh_token: Annotated[str,
|
|
23
|
+
Doc("This is a refresh token that will be used for issuing access token again.")] = Field(nullable=False,
|
|
24
|
+
unique=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TokenValidDateTimeMixin(SQLModel):
|
|
28
|
+
issued_dt: Annotated[datetime,
|
|
29
|
+
Doc("datetime when the token issued.")] = Field(nullable=False)
|
|
30
|
+
expiration_dt: Annotated[datetime,
|
|
31
|
+
Doc("datetime when the token will be expired.")] = Field(nullable=False)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class JWTTokens(RefreshTokenMixin, AccessTokenMixin):
|
|
35
|
+
token_type: Annotated[str,
|
|
36
|
+
Doc("Token type that used in this class.")] = Field(nullable=False,
|
|
37
|
+
default="Bearer")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# define function to control SQL event
|
|
41
|
+
# please follow the procedure below
|
|
42
|
+
# use decorator 'event.listens_for()' for event handling function
|
|
43
|
+
# event handler function must have 3 args: mapper, connection and target)
|
|
44
|
+
# in your model file, import listens_for from 'sqlalchemy.event'
|
|
45
|
+
# event.listens_for(target=TARGET_TABLE_CLASS_NAME, identifier=['before_insert', 'before_update',...])
|
|
46
|
+
def _fill_token_datetime_field(mapper, connection, target) -> None:
|
|
47
|
+
if target.issued_dt is None:
|
|
48
|
+
target.issued_dt = datetime.now(tz=timezone.utc)
|
|
49
|
+
|
|
50
|
+
if target.expired is None:
|
|
51
|
+
target.expired_dt = target.issued_dt + timedelta(minutes=TOKEN_VALID_TIME)
|
|
52
|
+
|
|
53
|
+
return None
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# import packages from default or pip library
|
|
2
|
+
import hashlib
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing_extensions import Annotated, Doc
|
|
5
|
+
from sqlmodel import SQLModel, Field
|
|
6
|
+
|
|
7
|
+
# import packages from this framework
|
|
8
|
+
from settings import PASSWORD_POLICIES
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# password encryption algorithm
|
|
12
|
+
ENCRYPT_TYPE: str = PASSWORD_POLICIES.get("encrypt_type")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# define Class for Common SQLModel
|
|
16
|
+
class LastSigninDateTimeMixin(SQLModel):
|
|
17
|
+
last_signin_dt: Annotated[datetime,
|
|
18
|
+
Doc("User's last signin datetime.")] = Field(nullable=True, default=None)
|
|
19
|
+
|
|
20
|
+
def update_signin_dt(self) -> None:
|
|
21
|
+
self.last_signin_dt = datetime.now(tz=timezone.utc)
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SigninFailMixin(SQLModel):
|
|
26
|
+
is_active: Annotated[bool,
|
|
27
|
+
Doc("show whether the user is activated or not.")] = Field(nullable=False, default=False)
|
|
28
|
+
signin_fail: Annotated[int,
|
|
29
|
+
Doc("If the user fail to login, this value will be incremented.")] = Field(nullable=False, default=0, ge=0, le=5)
|
|
30
|
+
|
|
31
|
+
def activate_user(self) -> None:
|
|
32
|
+
self.is_active = True
|
|
33
|
+
self.__init_signin_fail_count()
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
def deactivate_user(self) -> None:
|
|
37
|
+
self.is_active = False
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
def add_signin_fail_count(self, limit: int) -> None:
|
|
41
|
+
if self.signin_fail < limit:
|
|
42
|
+
self.signin_fail += 1
|
|
43
|
+
|
|
44
|
+
if self.signin_fail == limit:
|
|
45
|
+
self.deactivate_user()
|
|
46
|
+
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
def __init_signin_fail_count(self) -> None:
|
|
50
|
+
self.signin_fail = 0
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class IsAdminMixin(SQLModel):
|
|
55
|
+
is_admin: Annotated[bool,
|
|
56
|
+
Doc("show whether the user is admin or not.")] = Field(nullable=False, default=False)
|
|
57
|
+
|
|
58
|
+
def grant_admin(self) -> None:
|
|
59
|
+
self.is_admin = True
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
def revoke_admin(self) -> None:
|
|
63
|
+
self.is_admin = False
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class PasswordMixin(SQLModel):
|
|
68
|
+
password: Annotated[str,
|
|
69
|
+
Doc("password for application user. it is recommended to store password after encrypting.")] = Field(nullable=False, unique=False, min_length=32)
|
|
70
|
+
|
|
71
|
+
def encrypt_password(self, enc_type:str=ENCRYPT_TYPE):
|
|
72
|
+
self.password = encrypt_password(password=self.password, enc_type=enc_type)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class UsernameMixin(SQLModel):
|
|
76
|
+
username: Annotated[str,
|
|
77
|
+
Doc("username for application user. if you want to replace it to email, refer to 'contact' module.")] = Field(nullable=False, unique=True, min_length=8)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# define function for users
|
|
81
|
+
def encrypt_password(password:str, enc_type:str=ENCRYPT_TYPE) -> str:
|
|
82
|
+
if enc_type not in hashlib.algorithms_available:
|
|
83
|
+
raise ValueError(f"'{enc_type}' is not supported hash method in this application. Password will be encrypted with default hash.")
|
|
84
|
+
|
|
85
|
+
return getattr(hashlib, enc_type)(string=password.encode("utf-8")).hexdigest()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# define function to control SQL event
|
|
89
|
+
# please follow the procedure below
|
|
90
|
+
# use decorator 'event.listens_for()' for event handling function
|
|
91
|
+
# event handler function must have 3 args: mapper, connection and target)
|
|
92
|
+
# in your model file, import listens_for from 'sqlalchemy.event'
|
|
93
|
+
# event.listens_for(target=TARGET_TABLE_CLASS_NAME, identifier=['before_insert', 'before_update',...])
|
|
94
|
+
|
|
File without changes
|