square-authentication 6.2.2__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.
@@ -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 RecoveryMethodEnum
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=UserProfile.__tablename__,
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
- local_list_response_user_profile = (
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=UserProfile.__tablename__,
613
+ table_name=UserAuthProvider.__tablename__,
560
614
  filters=FiltersV0(
561
615
  root={
562
- UserProfile.user_profile_username.name: FilterConditionsV0(
563
- eq=username
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(local_list_response_user_profile) != 1:
626
+ if len(local_list_user_auth_provider_response) != 1:
570
627
  output_content = get_api_output_in_standard_format(
571
- message=messages["INCORRECT_USERNAME"],
572
- log=f"incorrect username {username}",
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=local_list_response_user_profile[0][
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=UserProfile.__tablename__,
1214
+ table_name=User.__tablename__,
1159
1215
  filters=FiltersV0(
1160
1216
  root={
1161
- UserProfile.user_profile_username.name: FilterConditionsV0(
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=UserProfile.__tablename__,
1238
+ table_name=User.__tablename__,
1185
1239
  filters=FiltersV0(
1186
1240
  root={
1187
- UserProfile.user_id.name: FilterConditionsV0(eq=user_id),
1241
+ User.user_id.name: FilterConditionsV0(eq=user_id),
1188
1242
  }
1189
1243
  ),
1190
1244
  data={
1191
- UserProfile.user_profile_username.name: new_username,
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.delete("/delete_user/v0")
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
- # delete the user.
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
+ )