square-authentication 6.2.1__py3-none-any.whl → 7.0.0__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.
- square_authentication/configuration.py +12 -1
- square_authentication/data/{config.ini → config.sample.ini} +4 -1
- square_authentication/data/{config.testing.ini → config.testing.sample.ini} +4 -1
- square_authentication/messages.py +10 -0
- square_authentication/pydantic_models/core.py +12 -0
- square_authentication/pydantic_models/profile.py +5 -0
- square_authentication/routes/core.py +673 -41
- square_authentication/routes/profile.py +448 -4
- {square_authentication-6.2.1.dist-info → square_authentication-7.0.0.dist-info}/METADATA +32 -3
- square_authentication-7.0.0.dist-info/RECORD +20 -0
- {square_authentication-6.2.1.dist-info → square_authentication-7.0.0.dist-info}/WHEEL +1 -1
- square_authentication-6.2.1.dist-info/RECORD +0 -19
- {square_authentication-6.2.1.dist-info → square_authentication-7.0.0.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,7 @@
|
|
1
1
|
import copy
|
2
|
+
import random
|
2
3
|
import re
|
4
|
+
import uuid
|
3
5
|
from datetime import datetime, timedelta, timezone
|
4
6
|
from typing import Annotated, List
|
5
7
|
|
@@ -9,11 +11,15 @@ from fastapi import APIRouter, Header, HTTPException, status
|
|
9
11
|
from fastapi.params import Query
|
10
12
|
from fastapi.responses import JSONResponse
|
11
13
|
from requests import HTTPError
|
12
|
-
from square_commons import get_api_output_in_standard_format
|
14
|
+
from square_commons import get_api_output_in_standard_format, send_email_using_mailgun
|
13
15
|
from square_database_helper.pydantic_models import FilterConditionsV0, FiltersV0
|
14
16
|
from square_database_structure.square import global_string_database_name
|
15
17
|
from square_database_structure.square.authentication import global_string_schema_name
|
16
|
-
from square_database_structure.square.authentication.enums import
|
18
|
+
from square_database_structure.square.authentication.enums import (
|
19
|
+
RecoveryMethodEnum,
|
20
|
+
AuthProviderEnum,
|
21
|
+
VerificationCodeTypeEnum,
|
22
|
+
)
|
17
23
|
from square_database_structure.square.authentication.tables import (
|
18
24
|
User,
|
19
25
|
UserApp,
|
@@ -21,7 +27,14 @@ from square_database_structure.square.authentication.tables import (
|
|
21
27
|
UserSession,
|
22
28
|
UserProfile,
|
23
29
|
UserRecoveryMethod,
|
30
|
+
UserAuthProvider,
|
31
|
+
UserVerificationCode,
|
32
|
+
)
|
33
|
+
from square_database_structure.square.email import (
|
34
|
+
global_string_schema_name as email_schema_name,
|
24
35
|
)
|
36
|
+
from square_database_structure.square.email.enums import EmailTypeEnum, EmailStatusEnum
|
37
|
+
from square_database_structure.square.email.tables import EmailLog
|
25
38
|
from square_database_structure.square.public import (
|
26
39
|
global_string_schema_name as global_string_public_schema_name,
|
27
40
|
)
|
@@ -34,6 +47,7 @@ from square_authentication.configuration import (
|
|
34
47
|
config_str_secret_key_for_refresh_token,
|
35
48
|
global_object_square_logger,
|
36
49
|
global_object_square_database_helper,
|
50
|
+
MAIL_GUN_API_KEY,
|
37
51
|
)
|
38
52
|
from square_authentication.messages import messages
|
39
53
|
from square_authentication.pydantic_models.core import (
|
@@ -43,6 +57,8 @@ from square_authentication.pydantic_models.core import (
|
|
43
57
|
RegisterUsernameV0,
|
44
58
|
TokenType,
|
45
59
|
UpdatePasswordV0,
|
60
|
+
ResetPasswordAndLoginUsingBackupCodeV0,
|
61
|
+
SendResetPasswordEmailV0,
|
46
62
|
)
|
47
63
|
from square_authentication.utils.token import get_jwt_payload
|
48
64
|
|
@@ -86,13 +102,9 @@ async def register_username_v0(
|
|
86
102
|
global_object_square_database_helper.get_rows_v0(
|
87
103
|
database_name=global_string_database_name,
|
88
104
|
schema_name=global_string_schema_name,
|
89
|
-
table_name=
|
105
|
+
table_name=User.__tablename__,
|
90
106
|
filters=FiltersV0(
|
91
|
-
root={
|
92
|
-
UserProfile.user_profile_username.name: FilterConditionsV0(
|
93
|
-
eq=username
|
94
|
-
)
|
95
|
-
}
|
107
|
+
root={User.user_username.name: FilterConditionsV0(eq=username)}
|
96
108
|
),
|
97
109
|
)["data"]["main"]
|
98
110
|
)
|
@@ -111,13 +123,47 @@ async def register_username_v0(
|
|
111
123
|
"""
|
112
124
|
# entry in user table
|
113
125
|
local_list_response_user = global_object_square_database_helper.insert_rows_v0(
|
114
|
-
data=[
|
126
|
+
data=[
|
127
|
+
{
|
128
|
+
User.user_username.name: username,
|
129
|
+
}
|
130
|
+
],
|
115
131
|
database_name=global_string_database_name,
|
116
132
|
schema_name=global_string_schema_name,
|
117
133
|
table_name=User.__tablename__,
|
118
134
|
)["data"]["main"]
|
119
135
|
local_str_user_id = local_list_response_user[0][User.user_id.name]
|
120
136
|
|
137
|
+
# entry in user auth provider table
|
138
|
+
local_list_response_user_auth_provider = global_object_square_database_helper.insert_rows_v0(
|
139
|
+
data=[
|
140
|
+
{
|
141
|
+
UserAuthProvider.user_id.name: local_str_user_id,
|
142
|
+
UserAuthProvider.auth_provider.name: AuthProviderEnum.SELF.value,
|
143
|
+
}
|
144
|
+
],
|
145
|
+
database_name=global_string_database_name,
|
146
|
+
schema_name=global_string_schema_name,
|
147
|
+
table_name=UserAuthProvider.__tablename__,
|
148
|
+
)[
|
149
|
+
"data"
|
150
|
+
][
|
151
|
+
"main"
|
152
|
+
]
|
153
|
+
local_str_user_id = local_list_response_user[0][User.user_id.name]
|
154
|
+
|
155
|
+
# entry in user profile table
|
156
|
+
global_object_square_database_helper.insert_rows_v0(
|
157
|
+
database_name=global_string_database_name,
|
158
|
+
schema_name=global_string_schema_name,
|
159
|
+
table_name=UserProfile.__tablename__,
|
160
|
+
data=[
|
161
|
+
{
|
162
|
+
UserProfile.user_id.name: local_str_user_id,
|
163
|
+
}
|
164
|
+
],
|
165
|
+
)
|
166
|
+
|
121
167
|
# entry in credential table
|
122
168
|
|
123
169
|
# hash password
|
@@ -136,17 +182,6 @@ async def register_username_v0(
|
|
136
182
|
schema_name=global_string_schema_name,
|
137
183
|
table_name=UserCredential.__tablename__,
|
138
184
|
)
|
139
|
-
global_object_square_database_helper.insert_rows_v0(
|
140
|
-
data=[
|
141
|
-
{
|
142
|
-
UserProfile.user_id.name: local_str_user_id,
|
143
|
-
UserProfile.user_profile_username.name: username,
|
144
|
-
}
|
145
|
-
],
|
146
|
-
database_name=global_string_database_name,
|
147
|
-
schema_name=global_string_schema_name,
|
148
|
-
table_name=UserProfile.__tablename__,
|
149
|
-
)
|
150
185
|
if app_id is not None:
|
151
186
|
# assign app to user
|
152
187
|
global_object_square_database_helper.insert_rows_v0(
|
@@ -552,29 +587,52 @@ async def login_username_v0(body: LoginUsernameV0):
|
|
552
587
|
validation
|
553
588
|
"""
|
554
589
|
# validation for username
|
555
|
-
|
590
|
+
# check if user with username exists
|
591
|
+
local_list_response_user = global_object_square_database_helper.get_rows_v0(
|
592
|
+
database_name=global_string_database_name,
|
593
|
+
schema_name=global_string_schema_name,
|
594
|
+
table_name=User.__tablename__,
|
595
|
+
filters=FiltersV0(
|
596
|
+
root={User.user_username.name: FilterConditionsV0(eq=username)}
|
597
|
+
),
|
598
|
+
)["data"]["main"]
|
599
|
+
if len(local_list_response_user) != 1:
|
600
|
+
output_content = get_api_output_in_standard_format(
|
601
|
+
message=messages["INCORRECT_USERNAME"],
|
602
|
+
log=f"incorrect username {username}",
|
603
|
+
)
|
604
|
+
raise HTTPException(
|
605
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
606
|
+
detail=output_content,
|
607
|
+
)
|
608
|
+
# check if user has auth provider as SELF
|
609
|
+
local_list_user_auth_provider_response = (
|
556
610
|
global_object_square_database_helper.get_rows_v0(
|
557
611
|
database_name=global_string_database_name,
|
558
612
|
schema_name=global_string_schema_name,
|
559
|
-
table_name=
|
613
|
+
table_name=UserAuthProvider.__tablename__,
|
560
614
|
filters=FiltersV0(
|
561
615
|
root={
|
562
|
-
|
563
|
-
eq=
|
564
|
-
)
|
616
|
+
UserAuthProvider.user_id.name: FilterConditionsV0(
|
617
|
+
eq=local_list_response_user[0][User.user_id.name]
|
618
|
+
),
|
619
|
+
UserAuthProvider.auth_provider.name: FilterConditionsV0(
|
620
|
+
eq=AuthProviderEnum.SELF.value
|
621
|
+
),
|
565
622
|
}
|
566
623
|
),
|
567
624
|
)["data"]["main"]
|
568
625
|
)
|
569
|
-
if len(
|
626
|
+
if len(local_list_user_auth_provider_response) != 1:
|
570
627
|
output_content = get_api_output_in_standard_format(
|
571
|
-
message=messages["
|
572
|
-
log=f"
|
628
|
+
message=messages["INCORRECT_AUTH_PROVIDER"],
|
629
|
+
log=f"{username} not linked with {AuthProviderEnum.SELF.value} auth provider.",
|
573
630
|
)
|
574
631
|
raise HTTPException(
|
575
632
|
status_code=status.HTTP_400_BAD_REQUEST,
|
576
633
|
detail=output_content,
|
577
634
|
)
|
635
|
+
# check if user has credentials (might not be set in case of errors in registration.)
|
578
636
|
local_list_authentication_user_response = (
|
579
637
|
global_object_square_database_helper.get_rows_v0(
|
580
638
|
database_name=global_string_database_name,
|
@@ -583,9 +641,7 @@ async def login_username_v0(body: LoginUsernameV0):
|
|
583
641
|
filters=FiltersV0(
|
584
642
|
root={
|
585
643
|
UserCredential.user_id.name: FilterConditionsV0(
|
586
|
-
eq=
|
587
|
-
UserProfile.user_id.name
|
588
|
-
]
|
644
|
+
eq=local_list_response_user[0][User.user_id.name]
|
589
645
|
)
|
590
646
|
}
|
591
647
|
),
|
@@ -1155,12 +1211,10 @@ async def update_username_v0(
|
|
1155
1211
|
global_object_square_database_helper.get_rows_v0(
|
1156
1212
|
database_name=global_string_database_name,
|
1157
1213
|
schema_name=global_string_schema_name,
|
1158
|
-
table_name=
|
1214
|
+
table_name=User.__tablename__,
|
1159
1215
|
filters=FiltersV0(
|
1160
1216
|
root={
|
1161
|
-
|
1162
|
-
eq=new_username
|
1163
|
-
),
|
1217
|
+
User.user_username.name: FilterConditionsV0(eq=new_username),
|
1164
1218
|
}
|
1165
1219
|
),
|
1166
1220
|
)["data"]["main"]
|
@@ -1181,14 +1235,14 @@ async def update_username_v0(
|
|
1181
1235
|
global_object_square_database_helper.edit_rows_v0(
|
1182
1236
|
database_name=global_string_database_name,
|
1183
1237
|
schema_name=global_string_schema_name,
|
1184
|
-
table_name=
|
1238
|
+
table_name=User.__tablename__,
|
1185
1239
|
filters=FiltersV0(
|
1186
1240
|
root={
|
1187
|
-
|
1241
|
+
User.user_id.name: FilterConditionsV0(eq=user_id),
|
1188
1242
|
}
|
1189
1243
|
),
|
1190
1244
|
data={
|
1191
|
-
|
1245
|
+
User.user_username.name: new_username,
|
1192
1246
|
},
|
1193
1247
|
)
|
1194
1248
|
"""
|
@@ -1218,7 +1272,7 @@ async def update_username_v0(
|
|
1218
1272
|
)
|
1219
1273
|
|
1220
1274
|
|
1221
|
-
@router.
|
1275
|
+
@router.post("/delete_user/v0")
|
1222
1276
|
@global_object_square_logger.auto_logger()
|
1223
1277
|
async def delete_user_v0(
|
1224
1278
|
body: DeleteUserV0,
|
@@ -1393,7 +1447,7 @@ async def update_password_v0(
|
|
1393
1447
|
"""
|
1394
1448
|
main process
|
1395
1449
|
"""
|
1396
|
-
#
|
1450
|
+
# update the password
|
1397
1451
|
local_str_hashed_password = bcrypt.hashpw(
|
1398
1452
|
new_password.encode("utf-8"), bcrypt.gensalt()
|
1399
1453
|
).decode("utf-8")
|
@@ -1581,7 +1635,44 @@ async def update_user_recovery_methods_v0(
|
|
1581
1635
|
status_code=status.HTTP_400_BAD_REQUEST,
|
1582
1636
|
detail=output_content,
|
1583
1637
|
)
|
1584
|
-
|
1638
|
+
# check if user's email is verified in user profile.
|
1639
|
+
# maybe too harsh to reject the request entirely,
|
1640
|
+
# but for practical purposes this api call should be used for 1 recovery method at a time, so it's not too bad.
|
1641
|
+
if RecoveryMethodEnum.EMAIL.value in recovery_methods_to_add:
|
1642
|
+
local_list_response_user_profile = (
|
1643
|
+
global_object_square_database_helper.get_rows_v0(
|
1644
|
+
database_name=global_string_database_name,
|
1645
|
+
schema_name=global_string_schema_name,
|
1646
|
+
table_name=UserProfile.__tablename__,
|
1647
|
+
filters=FiltersV0(
|
1648
|
+
root={
|
1649
|
+
UserProfile.user_id.name: FilterConditionsV0(eq=user_id),
|
1650
|
+
}
|
1651
|
+
),
|
1652
|
+
)["data"]["main"]
|
1653
|
+
)
|
1654
|
+
if len(local_list_response_user_profile) != 1:
|
1655
|
+
# maybe this should raise 500 as this error will not occur if code runs correctly.
|
1656
|
+
output_content = get_api_output_in_standard_format(
|
1657
|
+
message=messages["GENERIC_400"],
|
1658
|
+
log=f"user_id: {user_id} does not have a profile.",
|
1659
|
+
)
|
1660
|
+
raise HTTPException(
|
1661
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
1662
|
+
detail=output_content,
|
1663
|
+
)
|
1664
|
+
local_dict_user_profile = local_list_response_user_profile[0]
|
1665
|
+
if not local_dict_user_profile[
|
1666
|
+
UserProfile.user_profile_email_verified.name
|
1667
|
+
]:
|
1668
|
+
output_content = get_api_output_in_standard_format(
|
1669
|
+
message=messages["EMAIL_NOT_VERIFIED"],
|
1670
|
+
log=f"user_id: {user_id} does not have email verified.",
|
1671
|
+
)
|
1672
|
+
raise HTTPException(
|
1673
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
1674
|
+
detail=output_content,
|
1675
|
+
)
|
1585
1676
|
"""
|
1586
1677
|
main process
|
1587
1678
|
"""
|
@@ -1680,3 +1771,544 @@ async def update_user_recovery_methods_v0(
|
|
1680
1771
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
1681
1772
|
content=output_content,
|
1682
1773
|
)
|
1774
|
+
|
1775
|
+
|
1776
|
+
@router.post("/generate_account_backup_codes/v0")
|
1777
|
+
@global_object_square_logger.auto_logger()
|
1778
|
+
async def generate_account_backup_codes_v0(
|
1779
|
+
access_token: Annotated[str, Header()],
|
1780
|
+
):
|
1781
|
+
|
1782
|
+
try:
|
1783
|
+
"""
|
1784
|
+
validation
|
1785
|
+
"""
|
1786
|
+
try:
|
1787
|
+
local_dict_access_token_payload = get_jwt_payload(
|
1788
|
+
access_token, config_str_secret_key_for_access_token
|
1789
|
+
)
|
1790
|
+
except Exception as error:
|
1791
|
+
output_content = get_api_output_in_standard_format(
|
1792
|
+
message=messages["INCORRECT_ACCESS_TOKEN"], log=str(error)
|
1793
|
+
)
|
1794
|
+
raise HTTPException(
|
1795
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
1796
|
+
detail=output_content,
|
1797
|
+
)
|
1798
|
+
user_id = local_dict_access_token_payload["user_id"]
|
1799
|
+
# check if user has recovery method enabled
|
1800
|
+
local_list_response_user_recovery_methods = global_object_square_database_helper.get_rows_v0(
|
1801
|
+
database_name=global_string_database_name,
|
1802
|
+
schema_name=global_string_schema_name,
|
1803
|
+
table_name=UserRecoveryMethod.__tablename__,
|
1804
|
+
filters=FiltersV0(
|
1805
|
+
root={
|
1806
|
+
UserRecoveryMethod.user_id.name: FilterConditionsV0(eq=user_id),
|
1807
|
+
UserRecoveryMethod.user_recovery_method_name.name: FilterConditionsV0(
|
1808
|
+
eq=RecoveryMethodEnum.BACKUP_CODE.value
|
1809
|
+
),
|
1810
|
+
}
|
1811
|
+
),
|
1812
|
+
)[
|
1813
|
+
"data"
|
1814
|
+
][
|
1815
|
+
"main"
|
1816
|
+
]
|
1817
|
+
if len(local_list_response_user_recovery_methods) != 1:
|
1818
|
+
output_content = get_api_output_in_standard_format(
|
1819
|
+
message=messages["RECOVERY_METHOD_NOT_ENABLED"],
|
1820
|
+
log=f"user_id: {user_id} does not have backup codes recovery method enabled.",
|
1821
|
+
)
|
1822
|
+
raise HTTPException(
|
1823
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
1824
|
+
detail=output_content,
|
1825
|
+
)
|
1826
|
+
"""
|
1827
|
+
main process
|
1828
|
+
"""
|
1829
|
+
# generate backup codes
|
1830
|
+
backup_codes = []
|
1831
|
+
db_data = []
|
1832
|
+
|
1833
|
+
for i in range(10):
|
1834
|
+
backup_code = str(uuid.uuid4())
|
1835
|
+
backup_codes.append(backup_code)
|
1836
|
+
# hash the backup code
|
1837
|
+
local_str_hashed_backup_code = bcrypt.hashpw(
|
1838
|
+
backup_code.encode("utf-8"), bcrypt.gensalt()
|
1839
|
+
).decode("utf-8")
|
1840
|
+
|
1841
|
+
db_data.append(
|
1842
|
+
{
|
1843
|
+
UserVerificationCode.user_id.name: user_id,
|
1844
|
+
UserVerificationCode.user_verification_code_type.name: VerificationCodeTypeEnum.BACKUP_CODE_RECOVERY.value,
|
1845
|
+
UserVerificationCode.user_verification_code_hash.name: local_str_hashed_backup_code,
|
1846
|
+
}
|
1847
|
+
)
|
1848
|
+
global_object_square_database_helper.insert_rows_v0(
|
1849
|
+
database_name=global_string_database_name,
|
1850
|
+
schema_name=global_string_schema_name,
|
1851
|
+
table_name=UserVerificationCode.__tablename__,
|
1852
|
+
data=db_data,
|
1853
|
+
)
|
1854
|
+
|
1855
|
+
"""
|
1856
|
+
return value
|
1857
|
+
"""
|
1858
|
+
output_content = get_api_output_in_standard_format(
|
1859
|
+
message=messages["GENERIC_CREATION_SUCCESSFUL"],
|
1860
|
+
data={
|
1861
|
+
"main": {
|
1862
|
+
"user_id": user_id,
|
1863
|
+
"backup_codes": backup_codes,
|
1864
|
+
}
|
1865
|
+
},
|
1866
|
+
)
|
1867
|
+
return JSONResponse(status_code=status.HTTP_200_OK, content=output_content)
|
1868
|
+
except HTTPException as http_exception:
|
1869
|
+
global_object_square_logger.logger.error(http_exception, exc_info=True)
|
1870
|
+
return JSONResponse(
|
1871
|
+
status_code=http_exception.status_code, content=http_exception.detail
|
1872
|
+
)
|
1873
|
+
except Exception as e:
|
1874
|
+
"""
|
1875
|
+
rollback logic
|
1876
|
+
"""
|
1877
|
+
global_object_square_logger.logger.error(e, exc_info=True)
|
1878
|
+
output_content = get_api_output_in_standard_format(
|
1879
|
+
message=messages["GENERIC_500"],
|
1880
|
+
log=str(e),
|
1881
|
+
)
|
1882
|
+
return JSONResponse(
|
1883
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=output_content
|
1884
|
+
)
|
1885
|
+
|
1886
|
+
|
1887
|
+
@router.post("/reset_password_and_login_using_backup_code/v0")
|
1888
|
+
@global_object_square_logger.auto_logger()
|
1889
|
+
async def reset_password_and_login_using_backup_code_v0(
|
1890
|
+
body: ResetPasswordAndLoginUsingBackupCodeV0,
|
1891
|
+
):
|
1892
|
+
backup_code = body.backup_code
|
1893
|
+
username = body.username
|
1894
|
+
new_password = body.new_password
|
1895
|
+
app_id = body.app_id
|
1896
|
+
try:
|
1897
|
+
"""
|
1898
|
+
validation
|
1899
|
+
"""
|
1900
|
+
# validate username
|
1901
|
+
local_list_authentication_user_response = (
|
1902
|
+
global_object_square_database_helper.get_rows_v0(
|
1903
|
+
database_name=global_string_database_name,
|
1904
|
+
schema_name=global_string_schema_name,
|
1905
|
+
table_name=User.__tablename__,
|
1906
|
+
filters=FiltersV0(
|
1907
|
+
root={User.user_username.name: FilterConditionsV0(eq=username)}
|
1908
|
+
),
|
1909
|
+
)["data"]["main"]
|
1910
|
+
)
|
1911
|
+
if len(local_list_authentication_user_response) != 1:
|
1912
|
+
output_content = get_api_output_in_standard_format(
|
1913
|
+
message=messages["INCORRECT_USERNAME"],
|
1914
|
+
log=f"incorrect username: {username}.",
|
1915
|
+
)
|
1916
|
+
raise HTTPException(
|
1917
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
1918
|
+
detail=output_content,
|
1919
|
+
)
|
1920
|
+
user_id = local_list_authentication_user_response[0][User.user_id.name]
|
1921
|
+
# check if user has recovery method enabled
|
1922
|
+
local_list_response_user_recovery_methods = global_object_square_database_helper.get_rows_v0(
|
1923
|
+
database_name=global_string_database_name,
|
1924
|
+
schema_name=global_string_schema_name,
|
1925
|
+
table_name=UserRecoveryMethod.__tablename__,
|
1926
|
+
filters=FiltersV0(
|
1927
|
+
root={
|
1928
|
+
UserRecoveryMethod.user_id.name: FilterConditionsV0(eq=user_id),
|
1929
|
+
UserRecoveryMethod.user_recovery_method_name.name: FilterConditionsV0(
|
1930
|
+
eq=RecoveryMethodEnum.BACKUP_CODE.value
|
1931
|
+
),
|
1932
|
+
}
|
1933
|
+
),
|
1934
|
+
)[
|
1935
|
+
"data"
|
1936
|
+
][
|
1937
|
+
"main"
|
1938
|
+
]
|
1939
|
+
if len(local_list_response_user_recovery_methods) != 1:
|
1940
|
+
output_content = get_api_output_in_standard_format(
|
1941
|
+
message=messages["RECOVERY_METHOD_NOT_ENABLED"],
|
1942
|
+
log=f"user_id: {user_id} does not have backup codes recovery method enabled.",
|
1943
|
+
)
|
1944
|
+
raise HTTPException(
|
1945
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
1946
|
+
detail=output_content,
|
1947
|
+
)
|
1948
|
+
# validate if user is assigned to the app.
|
1949
|
+
# not checking [skipping] if the app exists, as it is not required for this endpoint.
|
1950
|
+
local_list_response_user_app = global_object_square_database_helper.get_rows_v0(
|
1951
|
+
database_name=global_string_database_name,
|
1952
|
+
schema_name=global_string_schema_name,
|
1953
|
+
table_name=UserApp.__tablename__,
|
1954
|
+
filters=FiltersV0(
|
1955
|
+
root={
|
1956
|
+
UserApp.user_id.name: FilterConditionsV0(eq=user_id),
|
1957
|
+
UserApp.app_id.name: FilterConditionsV0(eq=app_id),
|
1958
|
+
}
|
1959
|
+
),
|
1960
|
+
)["data"]["main"]
|
1961
|
+
if len(local_list_response_user_app) == 0:
|
1962
|
+
output_content = get_api_output_in_standard_format(
|
1963
|
+
message=messages["GENERIC_400"],
|
1964
|
+
log=f"user_id: {user_id} is not assigned to app_id: {app_id}.",
|
1965
|
+
)
|
1966
|
+
raise HTTPException(
|
1967
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
1968
|
+
detail=output_content,
|
1969
|
+
)
|
1970
|
+
"""
|
1971
|
+
main process
|
1972
|
+
"""
|
1973
|
+
# validate backup code
|
1974
|
+
local_list_response_user_verification_code = global_object_square_database_helper.get_rows_v0(
|
1975
|
+
database_name=global_string_database_name,
|
1976
|
+
schema_name=global_string_schema_name,
|
1977
|
+
table_name=UserVerificationCode.__tablename__,
|
1978
|
+
filters=FiltersV0(
|
1979
|
+
root={
|
1980
|
+
UserVerificationCode.user_id.name: FilterConditionsV0(eq=user_id),
|
1981
|
+
UserVerificationCode.user_verification_code_type.name: FilterConditionsV0(
|
1982
|
+
eq=VerificationCodeTypeEnum.BACKUP_CODE_RECOVERY.value
|
1983
|
+
),
|
1984
|
+
UserVerificationCode.user_verification_code_expires_at.name: FilterConditionsV0(
|
1985
|
+
is_null=True
|
1986
|
+
),
|
1987
|
+
UserVerificationCode.user_verification_code_used_at.name: FilterConditionsV0(
|
1988
|
+
is_null=True
|
1989
|
+
),
|
1990
|
+
}
|
1991
|
+
),
|
1992
|
+
columns=[UserVerificationCode.user_verification_code_hash.name],
|
1993
|
+
)[
|
1994
|
+
"data"
|
1995
|
+
][
|
1996
|
+
"main"
|
1997
|
+
]
|
1998
|
+
# find the backup code in the list
|
1999
|
+
local_list_response_user_verification_code = [
|
2000
|
+
x
|
2001
|
+
for x in local_list_response_user_verification_code
|
2002
|
+
if bcrypt.checkpw(
|
2003
|
+
backup_code.encode("utf-8"),
|
2004
|
+
x[UserVerificationCode.user_verification_code_hash.name].encode(
|
2005
|
+
"utf-8"
|
2006
|
+
),
|
2007
|
+
)
|
2008
|
+
]
|
2009
|
+
if len(local_list_response_user_verification_code) != 1:
|
2010
|
+
output_content = get_api_output_in_standard_format(
|
2011
|
+
message=messages["INCORRECT_BACKUP_CODE"],
|
2012
|
+
log=f"incorrect backup code: {backup_code} for user_id: {user_id}.",
|
2013
|
+
)
|
2014
|
+
raise HTTPException(
|
2015
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
2016
|
+
detail=output_content,
|
2017
|
+
)
|
2018
|
+
# hash the new password
|
2019
|
+
local_str_hashed_password = bcrypt.hashpw(
|
2020
|
+
new_password.encode("utf-8"), bcrypt.gensalt()
|
2021
|
+
).decode("utf-8")
|
2022
|
+
# update the password
|
2023
|
+
global_object_square_database_helper.edit_rows_v0(
|
2024
|
+
database_name=global_string_database_name,
|
2025
|
+
schema_name=global_string_schema_name,
|
2026
|
+
table_name=UserCredential.__tablename__,
|
2027
|
+
filters=FiltersV0(
|
2028
|
+
root={
|
2029
|
+
UserCredential.user_id.name: FilterConditionsV0(eq=user_id),
|
2030
|
+
}
|
2031
|
+
),
|
2032
|
+
data={
|
2033
|
+
UserCredential.user_credential_hashed_password.name: local_str_hashed_password,
|
2034
|
+
},
|
2035
|
+
)
|
2036
|
+
# mark the backup code as used
|
2037
|
+
global_object_square_database_helper.edit_rows_v0(
|
2038
|
+
database_name=global_string_database_name,
|
2039
|
+
schema_name=global_string_schema_name,
|
2040
|
+
table_name=UserVerificationCode.__tablename__,
|
2041
|
+
filters=FiltersV0(
|
2042
|
+
root={
|
2043
|
+
UserVerificationCode.user_id.name: FilterConditionsV0(eq=user_id),
|
2044
|
+
UserVerificationCode.user_verification_code_type.name: FilterConditionsV0(
|
2045
|
+
eq=VerificationCodeTypeEnum.BACKUP_CODE_RECOVERY.value
|
2046
|
+
),
|
2047
|
+
UserVerificationCode.user_verification_code_hash.name: FilterConditionsV0(
|
2048
|
+
eq=local_list_response_user_verification_code[0][
|
2049
|
+
UserVerificationCode.user_verification_code_hash.name
|
2050
|
+
]
|
2051
|
+
),
|
2052
|
+
}
|
2053
|
+
),
|
2054
|
+
data={
|
2055
|
+
UserVerificationCode.user_verification_code_used_at.name: datetime.now(
|
2056
|
+
timezone.utc
|
2057
|
+
).strftime("%Y-%m-%d %H:%M:%S.%f+00"),
|
2058
|
+
},
|
2059
|
+
)
|
2060
|
+
# generate access token and refresh token
|
2061
|
+
local_dict_access_token_payload = {
|
2062
|
+
"app_id": app_id,
|
2063
|
+
"user_id": user_id,
|
2064
|
+
"exp": datetime.now(timezone.utc)
|
2065
|
+
+ timedelta(minutes=config_int_access_token_valid_minutes),
|
2066
|
+
}
|
2067
|
+
local_str_access_token = jwt.encode(
|
2068
|
+
local_dict_access_token_payload, config_str_secret_key_for_access_token
|
2069
|
+
)
|
2070
|
+
local_object_refresh_token_expiry_time = datetime.now(timezone.utc) + timedelta(
|
2071
|
+
minutes=config_int_refresh_token_valid_minutes
|
2072
|
+
)
|
2073
|
+
local_dict_refresh_token_payload = {
|
2074
|
+
"app_id": app_id,
|
2075
|
+
"user_id": user_id,
|
2076
|
+
"exp": local_object_refresh_token_expiry_time,
|
2077
|
+
}
|
2078
|
+
local_str_refresh_token = jwt.encode(
|
2079
|
+
local_dict_refresh_token_payload, config_str_secret_key_for_refresh_token
|
2080
|
+
)
|
2081
|
+
# insert the refresh token in the database
|
2082
|
+
global_object_square_database_helper.insert_rows_v0(
|
2083
|
+
database_name=global_string_database_name,
|
2084
|
+
schema_name=global_string_schema_name,
|
2085
|
+
table_name=UserSession.__tablename__,
|
2086
|
+
data=[
|
2087
|
+
{
|
2088
|
+
UserSession.user_id.name: user_id,
|
2089
|
+
UserSession.app_id.name: app_id,
|
2090
|
+
UserSession.user_session_refresh_token.name: local_str_refresh_token,
|
2091
|
+
UserSession.user_session_expiry_time.name: local_object_refresh_token_expiry_time.strftime(
|
2092
|
+
"%Y-%m-%d %H:%M:%S.%f+00"
|
2093
|
+
),
|
2094
|
+
}
|
2095
|
+
],
|
2096
|
+
)
|
2097
|
+
"""
|
2098
|
+
return value
|
2099
|
+
"""
|
2100
|
+
output_content = get_api_output_in_standard_format(
|
2101
|
+
message=messages["GENERIC_CREATION_SUCCESSFUL"],
|
2102
|
+
data={
|
2103
|
+
"main": {
|
2104
|
+
"user_id": user_id,
|
2105
|
+
"access_token": local_str_access_token,
|
2106
|
+
"refresh_token": local_str_refresh_token,
|
2107
|
+
}
|
2108
|
+
},
|
2109
|
+
)
|
2110
|
+
|
2111
|
+
return JSONResponse(status_code=status.HTTP_200_OK, content=output_content)
|
2112
|
+
except HTTPException as http_exception:
|
2113
|
+
global_object_square_logger.logger.error(http_exception, exc_info=True)
|
2114
|
+
return JSONResponse(
|
2115
|
+
status_code=http_exception.status_code, content=http_exception.detail
|
2116
|
+
)
|
2117
|
+
except Exception as e:
|
2118
|
+
"""
|
2119
|
+
rollback logic
|
2120
|
+
"""
|
2121
|
+
global_object_square_logger.logger.error(e, exc_info=True)
|
2122
|
+
output_content = get_api_output_in_standard_format(
|
2123
|
+
message=messages["GENERIC_500"],
|
2124
|
+
log=str(e),
|
2125
|
+
)
|
2126
|
+
return JSONResponse(
|
2127
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=output_content
|
2128
|
+
)
|
2129
|
+
|
2130
|
+
|
2131
|
+
@router.post("/send_reset_password_email/v0")
|
2132
|
+
@global_object_square_logger.auto_logger()
|
2133
|
+
async def send_reset_password_email_v0(
|
2134
|
+
body: SendResetPasswordEmailV0,
|
2135
|
+
):
|
2136
|
+
username = body.username
|
2137
|
+
app_id = body.app_id
|
2138
|
+
try:
|
2139
|
+
"""
|
2140
|
+
validation
|
2141
|
+
"""
|
2142
|
+
# validate username
|
2143
|
+
local_list_authentication_user_response = (
|
2144
|
+
global_object_square_database_helper.get_rows_v0(
|
2145
|
+
database_name=global_string_database_name,
|
2146
|
+
schema_name=global_string_schema_name,
|
2147
|
+
table_name=User.__tablename__,
|
2148
|
+
filters=FiltersV0(
|
2149
|
+
root={User.user_username.name: FilterConditionsV0(eq=username)}
|
2150
|
+
),
|
2151
|
+
)["data"]["main"]
|
2152
|
+
)
|
2153
|
+
if len(local_list_authentication_user_response) != 1:
|
2154
|
+
output_content = get_api_output_in_standard_format(
|
2155
|
+
message=messages["INCORRECT_USERNAME"],
|
2156
|
+
log=f"incorrect username: {username}.",
|
2157
|
+
)
|
2158
|
+
raise HTTPException(
|
2159
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
2160
|
+
detail=output_content,
|
2161
|
+
)
|
2162
|
+
user_id = local_list_authentication_user_response[0][User.user_id.name]
|
2163
|
+
# check if user has recovery method enabled
|
2164
|
+
local_list_response_user_recovery_methods = global_object_square_database_helper.get_rows_v0(
|
2165
|
+
database_name=global_string_database_name,
|
2166
|
+
schema_name=global_string_schema_name,
|
2167
|
+
table_name=UserRecoveryMethod.__tablename__,
|
2168
|
+
filters=FiltersV0(
|
2169
|
+
root={
|
2170
|
+
UserRecoveryMethod.user_id.name: FilterConditionsV0(eq=user_id),
|
2171
|
+
UserRecoveryMethod.user_recovery_method_name.name: FilterConditionsV0(
|
2172
|
+
eq=RecoveryMethodEnum.EMAIL.value
|
2173
|
+
),
|
2174
|
+
}
|
2175
|
+
),
|
2176
|
+
)[
|
2177
|
+
"data"
|
2178
|
+
][
|
2179
|
+
"main"
|
2180
|
+
]
|
2181
|
+
if len(local_list_response_user_recovery_methods) != 1:
|
2182
|
+
output_content = get_api_output_in_standard_format(
|
2183
|
+
message=messages["RECOVERY_METHOD_NOT_ENABLED"],
|
2184
|
+
log=f"user_id: {user_id} does not have email recovery method enabled.",
|
2185
|
+
)
|
2186
|
+
raise HTTPException(
|
2187
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
2188
|
+
detail=output_content,
|
2189
|
+
)
|
2190
|
+
# validate if user has email in profile
|
2191
|
+
user_profile_response = global_object_square_database_helper.get_rows_v0(
|
2192
|
+
database_name=global_string_database_name,
|
2193
|
+
schema_name=global_string_schema_name,
|
2194
|
+
table_name=UserProfile.__tablename__,
|
2195
|
+
filters=FiltersV0(
|
2196
|
+
root={UserProfile.user_id.name: FilterConditionsV0(eq=user_id)}
|
2197
|
+
),
|
2198
|
+
apply_filters=True,
|
2199
|
+
)
|
2200
|
+
user_profile_data = user_profile_response["data"]["main"][0]
|
2201
|
+
if not user_profile_data.get(UserProfile.user_profile_email.name):
|
2202
|
+
output_content = get_api_output_in_standard_format(
|
2203
|
+
message=messages["GENERIC_MISSING_REQUIRED_FIELD"],
|
2204
|
+
log="email is required to send verification email.",
|
2205
|
+
)
|
2206
|
+
raise HTTPException(
|
2207
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
2208
|
+
detail=output_content,
|
2209
|
+
)
|
2210
|
+
# check if email is not verified
|
2211
|
+
if not user_profile_data.get(UserProfile.user_profile_email_verified.name):
|
2212
|
+
output_content = get_api_output_in_standard_format(
|
2213
|
+
message=messages["EMAIL_NOT_VERIFIED"],
|
2214
|
+
log="email is not verified.",
|
2215
|
+
)
|
2216
|
+
return JSONResponse(status_code=status.HTTP_200_OK, content=output_content)
|
2217
|
+
|
2218
|
+
"""
|
2219
|
+
main process
|
2220
|
+
"""
|
2221
|
+
# create 6 digit verification code
|
2222
|
+
verification_code = random.randint(100000, 999999)
|
2223
|
+
# hash the verification code
|
2224
|
+
hashed_verification_code = bcrypt.hashpw(
|
2225
|
+
str(verification_code).encode("utf-8"), bcrypt.gensalt()
|
2226
|
+
).decode("utf-8")
|
2227
|
+
# expire the verification code after 10 minutes
|
2228
|
+
expires_at = datetime.now(timezone.utc) + timedelta(minutes=10)
|
2229
|
+
# add verification code to UserVerification code table
|
2230
|
+
global_object_square_database_helper.insert_rows_v0(
|
2231
|
+
database_name=global_string_database_name,
|
2232
|
+
schema_name=global_string_schema_name,
|
2233
|
+
table_name=UserVerificationCode.__tablename__,
|
2234
|
+
data=[
|
2235
|
+
{
|
2236
|
+
UserVerificationCode.user_id.name: user_id,
|
2237
|
+
UserVerificationCode.user_verification_code_type.name: VerificationCodeTypeEnum.EMAIL_RECOVERY.value,
|
2238
|
+
UserVerificationCode.user_verification_code_hash.name: hashed_verification_code,
|
2239
|
+
UserVerificationCode.user_verification_code_expires_at.name: expires_at.strftime(
|
2240
|
+
"%Y-%m-%d %H:%M:%S.%f+00"
|
2241
|
+
),
|
2242
|
+
}
|
2243
|
+
],
|
2244
|
+
)
|
2245
|
+
# send verification email
|
2246
|
+
if (
|
2247
|
+
user_profile_data[UserProfile.user_profile_first_name.name]
|
2248
|
+
and user_profile_data[UserProfile.user_profile_last_name.name]
|
2249
|
+
):
|
2250
|
+
user_to_name = f"{user_profile_data[UserProfile.user_profile_first_name.name]} {user_profile_data[UserProfile.user_profile_last_name.name]}"
|
2251
|
+
elif user_profile_data[UserProfile.user_profile_first_name.name]:
|
2252
|
+
user_to_name = user_profile_data[UserProfile.user_profile_first_name.name]
|
2253
|
+
elif user_profile_data[UserProfile.user_profile_last_name.name]:
|
2254
|
+
user_to_name = user_profile_data[UserProfile.user_profile_last_name.name]
|
2255
|
+
else:
|
2256
|
+
user_to_name = ""
|
2257
|
+
|
2258
|
+
mailgun_response = send_email_using_mailgun(
|
2259
|
+
from_email="auth@thepmsquare.com",
|
2260
|
+
from_name="square_authentication",
|
2261
|
+
to_email=user_profile_data[UserProfile.user_profile_email.name],
|
2262
|
+
to_name=user_to_name,
|
2263
|
+
subject="Password Reset Verification Code",
|
2264
|
+
body=f"Your Password Reset verification code is {verification_code}. It will expire in 10 minutes.",
|
2265
|
+
api_key=MAIL_GUN_API_KEY,
|
2266
|
+
domain_name="thepmsquare.com",
|
2267
|
+
)
|
2268
|
+
# add log for email sending
|
2269
|
+
global_object_square_database_helper.insert_rows_v0(
|
2270
|
+
database_name=global_string_database_name,
|
2271
|
+
schema_name=email_schema_name,
|
2272
|
+
table_name=EmailLog.__tablename__,
|
2273
|
+
data=[
|
2274
|
+
{
|
2275
|
+
EmailLog.user_id.name: user_id,
|
2276
|
+
EmailLog.recipient_email.name: user_profile_data[
|
2277
|
+
UserProfile.user_profile_email.name
|
2278
|
+
],
|
2279
|
+
EmailLog.email_type.name: EmailTypeEnum.VERIFY_EMAIL.value,
|
2280
|
+
EmailLog.status.name: EmailStatusEnum.SENT.value,
|
2281
|
+
EmailLog.third_party_message_id.name: mailgun_response.get("id"),
|
2282
|
+
}
|
2283
|
+
],
|
2284
|
+
)
|
2285
|
+
"""
|
2286
|
+
return value
|
2287
|
+
"""
|
2288
|
+
output_content = get_api_output_in_standard_format(
|
2289
|
+
data={
|
2290
|
+
"expires_at": expires_at.isoformat(),
|
2291
|
+
},
|
2292
|
+
message=messages["GENERIC_ACTION_SUCCESSFUL"],
|
2293
|
+
)
|
2294
|
+
return JSONResponse(
|
2295
|
+
status_code=status.HTTP_200_OK,
|
2296
|
+
content=output_content,
|
2297
|
+
)
|
2298
|
+
except HTTPException as http_exception:
|
2299
|
+
global_object_square_logger.logger.error(http_exception, exc_info=True)
|
2300
|
+
return JSONResponse(
|
2301
|
+
status_code=http_exception.status_code, content=http_exception.detail
|
2302
|
+
)
|
2303
|
+
except Exception as e:
|
2304
|
+
"""
|
2305
|
+
rollback logic
|
2306
|
+
"""
|
2307
|
+
global_object_square_logger.logger.error(e, exc_info=True)
|
2308
|
+
output_content = get_api_output_in_standard_format(
|
2309
|
+
message=messages["GENERIC_500"],
|
2310
|
+
log=str(e),
|
2311
|
+
)
|
2312
|
+
return JSONResponse(
|
2313
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=output_content
|
2314
|
+
)
|