square-authentication 6.2.2__py3-none-any.whl → 8.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,9 @@
1
1
  import copy
2
+ import io
3
+ import mimetypes
4
+ import random
2
5
  import re
6
+ import uuid
3
7
  from datetime import datetime, timedelta, timezone
4
8
  from typing import Annotated, List
5
9
 
@@ -8,12 +12,19 @@ import jwt
8
12
  from fastapi import APIRouter, Header, HTTPException, status
9
13
  from fastapi.params import Query
10
14
  from fastapi.responses import JSONResponse
15
+ from google.auth.transport import requests as google_requests
16
+ from google.oauth2 import id_token
11
17
  from requests import HTTPError
12
- from square_commons import get_api_output_in_standard_format
18
+ from square_commons import get_api_output_in_standard_format, send_email_using_mailgun
19
+ from square_commons.api_utils import make_request
13
20
  from square_database_helper.pydantic_models import FilterConditionsV0, FiltersV0
14
21
  from square_database_structure.square import global_string_database_name
15
22
  from square_database_structure.square.authentication import global_string_schema_name
16
- from square_database_structure.square.authentication.enums import RecoveryMethodEnum
23
+ from square_database_structure.square.authentication.enums import (
24
+ RecoveryMethodEnum,
25
+ AuthProviderEnum,
26
+ VerificationCodeTypeEnum,
27
+ )
17
28
  from square_database_structure.square.authentication.tables import (
18
29
  User,
19
30
  UserApp,
@@ -21,7 +32,14 @@ from square_database_structure.square.authentication.tables import (
21
32
  UserSession,
22
33
  UserProfile,
23
34
  UserRecoveryMethod,
35
+ UserAuthProvider,
36
+ UserVerificationCode,
37
+ )
38
+ from square_database_structure.square.email import (
39
+ global_string_schema_name as email_schema_name,
24
40
  )
41
+ from square_database_structure.square.email.enums import EmailTypeEnum, EmailStatusEnum
42
+ from square_database_structure.square.email.tables import EmailLog
25
43
  from square_database_structure.square.public import (
26
44
  global_string_schema_name as global_string_public_schema_name,
27
45
  )
@@ -34,6 +52,12 @@ from square_authentication.configuration import (
34
52
  config_str_secret_key_for_refresh_token,
35
53
  global_object_square_logger,
36
54
  global_object_square_database_helper,
55
+ MAIL_GUN_API_KEY,
56
+ GOOGLE_AUTH_PLATFORM_CLIENT_ID,
57
+ NUMBER_OF_RECOVERY_CODES,
58
+ NUMBER_OF_DIGITS_IN_EMAIL_PASSWORD_RESET_CODE,
59
+ EXPIRY_TIME_FOR_EMAIL_PASSWORD_RESET_CODE_IN_SECONDS,
60
+ global_object_square_file_store_helper,
37
61
  )
38
62
  from square_authentication.messages import messages
39
63
  from square_authentication.pydantic_models.core import (
@@ -43,7 +67,12 @@ from square_authentication.pydantic_models.core import (
43
67
  RegisterUsernameV0,
44
68
  TokenType,
45
69
  UpdatePasswordV0,
70
+ ResetPasswordAndLoginUsingBackupCodeV0,
71
+ SendResetPasswordEmailV0,
72
+ ResetPasswordAndLoginUsingResetEmailCodeV0,
73
+ RegisterLoginGoogleV0,
46
74
  )
75
+ from square_authentication.utils.core import generate_default_username_for_google_users
47
76
  from square_authentication.utils.token import get_jwt_payload
48
77
 
49
78
  router = APIRouter(
@@ -86,13 +115,9 @@ async def register_username_v0(
86
115
  global_object_square_database_helper.get_rows_v0(
87
116
  database_name=global_string_database_name,
88
117
  schema_name=global_string_schema_name,
89
- table_name=UserProfile.__tablename__,
118
+ table_name=User.__tablename__,
90
119
  filters=FiltersV0(
91
- root={
92
- UserProfile.user_profile_username.name: FilterConditionsV0(
93
- eq=username
94
- )
95
- }
120
+ root={User.user_username.name: FilterConditionsV0(eq=username)}
96
121
  ),
97
122
  )["data"]["main"]
98
123
  )
@@ -111,13 +136,42 @@ async def register_username_v0(
111
136
  """
112
137
  # entry in user table
113
138
  local_list_response_user = global_object_square_database_helper.insert_rows_v0(
114
- data=[{}],
139
+ data=[
140
+ {
141
+ User.user_username.name: username,
142
+ }
143
+ ],
115
144
  database_name=global_string_database_name,
116
145
  schema_name=global_string_schema_name,
117
146
  table_name=User.__tablename__,
118
147
  )["data"]["main"]
119
148
  local_str_user_id = local_list_response_user[0][User.user_id.name]
120
149
 
150
+ # entry in user auth provider table
151
+ global_object_square_database_helper.insert_rows_v0(
152
+ data=[
153
+ {
154
+ UserAuthProvider.user_id.name: local_str_user_id,
155
+ UserAuthProvider.auth_provider.name: AuthProviderEnum.SELF.value,
156
+ }
157
+ ],
158
+ database_name=global_string_database_name,
159
+ schema_name=global_string_schema_name,
160
+ table_name=UserAuthProvider.__tablename__,
161
+ )
162
+
163
+ # entry in user profile table
164
+ global_object_square_database_helper.insert_rows_v0(
165
+ database_name=global_string_database_name,
166
+ schema_name=global_string_schema_name,
167
+ table_name=UserProfile.__tablename__,
168
+ data=[
169
+ {
170
+ UserProfile.user_id.name: local_str_user_id,
171
+ }
172
+ ],
173
+ )
174
+
121
175
  # entry in credential table
122
176
 
123
177
  # hash password
@@ -136,17 +190,6 @@ async def register_username_v0(
136
190
  schema_name=global_string_schema_name,
137
191
  table_name=UserCredential.__tablename__,
138
192
  )
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
193
  if app_id is not None:
151
194
  # assign app to user
152
195
  global_object_square_database_helper.insert_rows_v0(
@@ -252,6 +295,355 @@ async def register_username_v0(
252
295
  )
253
296
 
254
297
 
298
+ @router.post("/register_login_google/v0")
299
+ async def register_login_google_v0(body: RegisterLoginGoogleV0):
300
+ app_id = body.app_id
301
+ google_id = body.google_id
302
+ assign_app_id_if_missing = body.assign_app_id_if_missing
303
+ was_new_user = False
304
+ try:
305
+ """
306
+ validation
307
+ """
308
+ # verify id token
309
+ id_info = id_token.verify_oauth2_token(
310
+ google_id,
311
+ google_requests.Request(),
312
+ GOOGLE_AUTH_PLATFORM_CLIENT_ID,
313
+ )
314
+ # validate if email is verified
315
+ if id_info.get("email_verified") is not True:
316
+ output_content = get_api_output_in_standard_format(
317
+ message=messages["EMAIL_NOT_VERIFIED"],
318
+ log="Google account email is not verified.",
319
+ )
320
+ raise HTTPException(
321
+ status_code=status.HTTP_400_BAD_REQUEST,
322
+ detail=output_content,
323
+ )
324
+ """
325
+ processing
326
+ """
327
+ google_sub = id_info["sub"]
328
+ email = id_info.get("email")
329
+ given_name = id_info.get("given_name")
330
+ family_name = id_info.get("family_name")
331
+
332
+ profile_picture = id_info.get("picture")
333
+
334
+ # check if user exists
335
+ user_rows = global_object_square_database_helper.get_rows_v0(
336
+ database_name=global_string_database_name,
337
+ schema_name=global_string_schema_name,
338
+ table_name=UserAuthProvider.__tablename__,
339
+ filters=FiltersV0(
340
+ root={
341
+ UserAuthProvider.auth_provider_user_id.name: FilterConditionsV0(
342
+ eq=google_sub
343
+ ),
344
+ UserAuthProvider.auth_provider.name: FilterConditionsV0(
345
+ eq=AuthProviderEnum.GOOGLE.value
346
+ ),
347
+ }
348
+ ),
349
+ )["data"]["main"]
350
+
351
+ if user_rows:
352
+ # login
353
+
354
+ # validate if app_id is assigned to user
355
+ # this will also validate if app_id is valid
356
+ local_str_user_id = user_rows[0][User.user_id.name]
357
+ user_record = global_object_square_database_helper.get_rows_v0(
358
+ database_name=global_string_database_name,
359
+ schema_name=global_string_schema_name,
360
+ table_name=User.__tablename__,
361
+ filters=FiltersV0(
362
+ root={User.user_id.name: FilterConditionsV0(eq=local_str_user_id)}
363
+ ),
364
+ )["data"]["main"][0]
365
+ username = user_record[User.user_username.name]
366
+ local_list_user_app_response = (
367
+ global_object_square_database_helper.get_rows_v0(
368
+ database_name=global_string_database_name,
369
+ schema_name=global_string_schema_name,
370
+ table_name=UserApp.__tablename__,
371
+ filters=FiltersV0(
372
+ root={
373
+ UserApp.user_id.name: FilterConditionsV0(
374
+ eq=local_str_user_id
375
+ ),
376
+ UserApp.app_id.name: FilterConditionsV0(eq=app_id),
377
+ }
378
+ ),
379
+ )["data"]["main"]
380
+ )
381
+ if len(local_list_user_app_response) == 0:
382
+ if assign_app_id_if_missing:
383
+ global_object_square_database_helper.insert_rows_v0(
384
+ database_name=global_string_database_name,
385
+ schema_name=global_string_schema_name,
386
+ table_name=UserApp.__tablename__,
387
+ data=[
388
+ {
389
+ UserApp.user_id.name: local_str_user_id,
390
+ UserApp.app_id.name: app_id,
391
+ }
392
+ ],
393
+ )
394
+ else:
395
+ output_content = get_api_output_in_standard_format(
396
+ message=messages["GENERIC_400"],
397
+ log=f"user_id {local_str_user_id}({username}) not assigned to app {app_id}.",
398
+ )
399
+ raise HTTPException(
400
+ status_code=status.HTTP_400_BAD_REQUEST,
401
+ detail=output_content,
402
+ )
403
+ else:
404
+ # register
405
+
406
+ was_new_user = True
407
+ # check if account with same email address exists
408
+ profile_rows = global_object_square_database_helper.get_rows_v0(
409
+ database_name=global_string_database_name,
410
+ schema_name=global_string_schema_name,
411
+ table_name=UserProfile.__tablename__,
412
+ filters=FiltersV0(
413
+ root={
414
+ UserProfile.user_profile_email.name: FilterConditionsV0(
415
+ eq=email
416
+ )
417
+ }
418
+ ),
419
+ )["data"]["main"]
420
+ if len(profile_rows) > 0:
421
+ output_content = get_api_output_in_standard_format(
422
+ message=messages["ACCOUNT_WITH_EMAIL_ALREADY_EXISTS"],
423
+ log=f"An account with the email {email} already exists.",
424
+ )
425
+ raise HTTPException(
426
+ status_code=status.HTTP_409_CONFLICT,
427
+ detail=output_content,
428
+ )
429
+ # generate a default username
430
+ username = generate_default_username_for_google_users(
431
+ family_name=family_name, given_name=given_name
432
+ )
433
+ # create user
434
+ user_rows = global_object_square_database_helper.insert_rows_v0(
435
+ database_name=global_string_database_name,
436
+ schema_name=global_string_schema_name,
437
+ table_name=User.__tablename__,
438
+ data=[
439
+ {
440
+ User.user_username.name: username,
441
+ }
442
+ ],
443
+ )["data"]["main"]
444
+ local_str_user_id = user_rows[0][User.user_id.name]
445
+
446
+ # link to user_auth_provider
447
+ global_object_square_database_helper.insert_rows_v0(
448
+ database_name=global_string_database_name,
449
+ schema_name=global_string_schema_name,
450
+ table_name=UserAuthProvider.__tablename__,
451
+ data=[
452
+ {
453
+ UserAuthProvider.user_id.name: local_str_user_id,
454
+ UserAuthProvider.auth_provider.name: AuthProviderEnum.GOOGLE.value,
455
+ UserAuthProvider.auth_provider_user_id.name: google_sub,
456
+ }
457
+ ],
458
+ )
459
+ # getting profile picture
460
+ if profile_picture:
461
+ try:
462
+ profile_picture_response = make_request(
463
+ "GET", profile_picture, return_type="response"
464
+ )
465
+
466
+ # finding content type and filename
467
+ headers = profile_picture_response.headers
468
+ content_type = headers.get(
469
+ "Content-Type", "application/octet-stream"
470
+ )
471
+ content_disposition = headers.get("Content-Disposition", "")
472
+
473
+ if content_disposition:
474
+ match = re.search(r'filename="([^"]+)"', content_disposition)
475
+ if match:
476
+ filename = match.group(1)
477
+ else:
478
+ filename = None
479
+ else:
480
+ filename = None
481
+ if filename is None:
482
+ global_object_square_logger.logger.warning(
483
+ f"user_id {local_str_user_id}'s profile picture from Google missing filename; guessing extension from Content-Type: {content_type}."
484
+ )
485
+ ext = (
486
+ mimetypes.guess_extension(
487
+ content_type.split(";")[0].strip()
488
+ )
489
+ or ""
490
+ )
491
+ filename = f"profile_photo{ext}"
492
+ if not ext:
493
+ filename += ".bin"
494
+
495
+ # upload bytes to square_file_storage
496
+ file_upload_response = global_object_square_file_store_helper.upload_file_using_tuple_v0(
497
+ file=(
498
+ filename,
499
+ io.BytesIO(profile_picture_response.content),
500
+ content_type,
501
+ ),
502
+ system_relative_path="global/users/profile_photos",
503
+ )
504
+ user_profile_photo_storage_token = file_upload_response["data"][
505
+ "main"
506
+ ]
507
+ except HTTPError:
508
+ global_object_square_logger.logger.error(
509
+ f"Failed to fetch profile picture for user_id {local_str_user_id} from google account.",
510
+ exc_info=True,
511
+ )
512
+ user_profile_photo_storage_token = None
513
+ except Exception as e:
514
+ global_object_square_logger.logger.error(
515
+ f"Error while fetching profile picture for user_id {local_str_user_id} from google account: {str(e)}",
516
+ exc_info=True,
517
+ )
518
+ user_profile_photo_storage_token = None
519
+ else:
520
+ global_object_square_logger.logger.warning(
521
+ f"user_id {local_str_user_id} has no profile picture in google account."
522
+ )
523
+ user_profile_photo_storage_token = None
524
+ # create user profile
525
+ global_object_square_database_helper.insert_rows_v0(
526
+ database_name=global_string_database_name,
527
+ schema_name=global_string_schema_name,
528
+ table_name=UserProfile.__tablename__,
529
+ data=[
530
+ {
531
+ UserProfile.user_id.name: local_str_user_id,
532
+ UserProfile.user_profile_email.name: email,
533
+ UserProfile.user_profile_email_verified.name: datetime.now(
534
+ timezone.utc
535
+ ).strftime("%Y-%m-%d %H:%M:%S.%f+00"),
536
+ UserProfile.user_profile_first_name.name: given_name,
537
+ UserProfile.user_profile_last_name.name: family_name,
538
+ UserProfile.user_profile_photo_storage_token.name: user_profile_photo_storage_token,
539
+ }
540
+ ],
541
+ )
542
+
543
+ # assign app if provided
544
+ if app_id is not None:
545
+ global_object_square_database_helper.insert_rows_v0(
546
+ database_name=global_string_database_name,
547
+ schema_name=global_string_schema_name,
548
+ table_name=UserApp.__tablename__,
549
+ data=[
550
+ {
551
+ UserApp.user_id.name: local_str_user_id,
552
+ UserApp.app_id.name: app_id,
553
+ }
554
+ ],
555
+ )
556
+
557
+ # generate tokens
558
+ now = datetime.now(timezone.utc)
559
+ access_token_payload = {
560
+ "app_id": app_id,
561
+ "user_id": local_str_user_id,
562
+ "exp": now + timedelta(minutes=config_int_access_token_valid_minutes),
563
+ }
564
+ access_token_str = jwt.encode(
565
+ access_token_payload,
566
+ config_str_secret_key_for_access_token,
567
+ )
568
+
569
+ refresh_token_expiry = now + timedelta(
570
+ minutes=config_int_refresh_token_valid_minutes
571
+ )
572
+ refresh_token_payload = {
573
+ "app_id": app_id,
574
+ "user_id": local_str_user_id,
575
+ "exp": refresh_token_expiry,
576
+ }
577
+ refresh_token_str = jwt.encode(
578
+ refresh_token_payload,
579
+ config_str_secret_key_for_refresh_token,
580
+ )
581
+
582
+ # store refresh token
583
+ global_object_square_database_helper.insert_rows_v0(
584
+ database_name=global_string_database_name,
585
+ schema_name=global_string_schema_name,
586
+ table_name=UserSession.__tablename__,
587
+ data=[
588
+ {
589
+ UserSession.user_id.name: local_str_user_id,
590
+ UserSession.app_id.name: app_id,
591
+ UserSession.user_session_refresh_token.name: refresh_token_str,
592
+ UserSession.user_session_expiry_time.name: refresh_token_expiry.strftime(
593
+ "%Y-%m-%d %H:%M:%S.%f+00"
594
+ ),
595
+ }
596
+ ],
597
+ )
598
+ """
599
+ return value
600
+ """
601
+ if was_new_user:
602
+ message = messages["REGISTRATION_SUCCESSFUL"]
603
+ else:
604
+ message = messages["LOGIN_SUCCESSFUL"]
605
+ output_content = get_api_output_in_standard_format(
606
+ message=message,
607
+ data={
608
+ "main": {
609
+ "user_id": local_str_user_id,
610
+ "username": username,
611
+ "app_id": app_id,
612
+ "access_token": access_token_str,
613
+ "refresh_token": refresh_token_str,
614
+ "refresh_token_expiry_time": refresh_token_expiry.isoformat(),
615
+ "was_new_user": was_new_user,
616
+ },
617
+ },
618
+ )
619
+
620
+ return JSONResponse(status_code=status.HTTP_200_OK, content=output_content)
621
+ except HTTPError as http_error:
622
+ global_object_square_logger.logger.error(http_error, exc_info=True)
623
+ return JSONResponse(
624
+ status_code=http_error.response.status_code,
625
+ content=http_error.response.text,
626
+ )
627
+ except HTTPException as http_exception:
628
+ global_object_square_logger.logger.error(http_exception, exc_info=True)
629
+ return JSONResponse(
630
+ status_code=http_exception.status_code, content=http_exception.detail
631
+ )
632
+ except Exception as e:
633
+ """
634
+ rollback logic
635
+ """
636
+ global_object_square_logger.logger.error(e, exc_info=True)
637
+ output_content = get_api_output_in_standard_format(
638
+ message=messages["GENERIC_500"],
639
+ log=str(e),
640
+ )
641
+ return JSONResponse(
642
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
643
+ content=output_content,
644
+ )
645
+
646
+
255
647
  @router.get("/get_user_details/v0")
256
648
  @global_object_square_logger.auto_logger()
257
649
  async def get_user_details_v0(
@@ -552,29 +944,52 @@ async def login_username_v0(body: LoginUsernameV0):
552
944
  validation
553
945
  """
554
946
  # validation for username
555
- local_list_response_user_profile = (
947
+ # check if user with username exists
948
+ local_list_response_user = global_object_square_database_helper.get_rows_v0(
949
+ database_name=global_string_database_name,
950
+ schema_name=global_string_schema_name,
951
+ table_name=User.__tablename__,
952
+ filters=FiltersV0(
953
+ root={User.user_username.name: FilterConditionsV0(eq=username)}
954
+ ),
955
+ )["data"]["main"]
956
+ if len(local_list_response_user) != 1:
957
+ output_content = get_api_output_in_standard_format(
958
+ message=messages["INCORRECT_USERNAME"],
959
+ log=f"incorrect username {username}",
960
+ )
961
+ raise HTTPException(
962
+ status_code=status.HTTP_400_BAD_REQUEST,
963
+ detail=output_content,
964
+ )
965
+ # check if user has auth provider as SELF
966
+ local_list_user_auth_provider_response = (
556
967
  global_object_square_database_helper.get_rows_v0(
557
968
  database_name=global_string_database_name,
558
969
  schema_name=global_string_schema_name,
559
- table_name=UserProfile.__tablename__,
970
+ table_name=UserAuthProvider.__tablename__,
560
971
  filters=FiltersV0(
561
972
  root={
562
- UserProfile.user_profile_username.name: FilterConditionsV0(
563
- eq=username
564
- )
973
+ UserAuthProvider.user_id.name: FilterConditionsV0(
974
+ eq=local_list_response_user[0][User.user_id.name]
975
+ ),
976
+ UserAuthProvider.auth_provider.name: FilterConditionsV0(
977
+ eq=AuthProviderEnum.SELF.value
978
+ ),
565
979
  }
566
980
  ),
567
981
  )["data"]["main"]
568
982
  )
569
- if len(local_list_response_user_profile) != 1:
983
+ if len(local_list_user_auth_provider_response) != 1:
570
984
  output_content = get_api_output_in_standard_format(
571
- message=messages["INCORRECT_USERNAME"],
572
- log=f"incorrect username {username}",
985
+ message=messages["INCORRECT_AUTH_PROVIDER"],
986
+ log=f"{username} not linked with {AuthProviderEnum.SELF.value} auth provider.",
573
987
  )
574
988
  raise HTTPException(
575
989
  status_code=status.HTTP_400_BAD_REQUEST,
576
990
  detail=output_content,
577
991
  )
992
+ # check if user has credentials (might not be set in case of errors in registration.)
578
993
  local_list_authentication_user_response = (
579
994
  global_object_square_database_helper.get_rows_v0(
580
995
  database_name=global_string_database_name,
@@ -583,9 +998,7 @@ async def login_username_v0(body: LoginUsernameV0):
583
998
  filters=FiltersV0(
584
999
  root={
585
1000
  UserCredential.user_id.name: FilterConditionsV0(
586
- eq=local_list_response_user_profile[0][
587
- UserProfile.user_id.name
588
- ]
1001
+ eq=local_list_response_user[0][User.user_id.name]
589
1002
  )
590
1003
  }
591
1004
  ),
@@ -593,8 +1006,8 @@ async def login_username_v0(body: LoginUsernameV0):
593
1006
  )
594
1007
  if len(local_list_authentication_user_response) != 1:
595
1008
  output_content = get_api_output_in_standard_format(
596
- message=messages["INCORRECT_USERNAME"],
597
- log=f"incorrect username {username}",
1009
+ message=messages["MALFORMED_USER"],
1010
+ log=f"username: {username} does not have credentials set.",
598
1011
  )
599
1012
  raise HTTPException(
600
1013
  status_code=status.HTTP_400_BAD_REQUEST,
@@ -1155,12 +1568,10 @@ async def update_username_v0(
1155
1568
  global_object_square_database_helper.get_rows_v0(
1156
1569
  database_name=global_string_database_name,
1157
1570
  schema_name=global_string_schema_name,
1158
- table_name=UserProfile.__tablename__,
1571
+ table_name=User.__tablename__,
1159
1572
  filters=FiltersV0(
1160
1573
  root={
1161
- UserProfile.user_profile_username.name: FilterConditionsV0(
1162
- eq=new_username
1163
- ),
1574
+ User.user_username.name: FilterConditionsV0(eq=new_username),
1164
1575
  }
1165
1576
  ),
1166
1577
  )["data"]["main"]
@@ -1181,14 +1592,14 @@ async def update_username_v0(
1181
1592
  global_object_square_database_helper.edit_rows_v0(
1182
1593
  database_name=global_string_database_name,
1183
1594
  schema_name=global_string_schema_name,
1184
- table_name=UserProfile.__tablename__,
1595
+ table_name=User.__tablename__,
1185
1596
  filters=FiltersV0(
1186
1597
  root={
1187
- UserProfile.user_id.name: FilterConditionsV0(eq=user_id),
1598
+ User.user_id.name: FilterConditionsV0(eq=user_id),
1188
1599
  }
1189
1600
  ),
1190
1601
  data={
1191
- UserProfile.user_profile_username.name: new_username,
1602
+ User.user_username.name: new_username,
1192
1603
  },
1193
1604
  )
1194
1605
  """
@@ -1218,7 +1629,7 @@ async def update_username_v0(
1218
1629
  )
1219
1630
 
1220
1631
 
1221
- @router.delete("/delete_user/v0")
1632
+ @router.post("/delete_user/v0")
1222
1633
  @global_object_square_logger.auto_logger()
1223
1634
  async def delete_user_v0(
1224
1635
  body: DeleteUserV0,
@@ -1286,8 +1697,19 @@ async def delete_user_v0(
1286
1697
  """
1287
1698
  main process
1288
1699
  """
1289
- # delete the user.
1290
- global_object_square_database_helper.delete_rows_v0(
1700
+ # fetch user profile photo storage token
1701
+ user_profile_storage_token = global_object_square_database_helper.get_rows_v0(
1702
+ database_name=global_string_database_name,
1703
+ schema_name=global_string_schema_name,
1704
+ table_name=UserProfile.__tablename__,
1705
+ filters=FiltersV0(
1706
+ root={UserProfile.user_id.name: FilterConditionsV0(eq=user_id)}
1707
+ ),
1708
+ columns=[UserProfile.user_profile_photo_storage_token.name],
1709
+ )["data"]["main"][0][UserProfile.user_profile_photo_storage_token.name]
1710
+
1711
+ # delete the user.
1712
+ global_object_square_database_helper.delete_rows_v0(
1291
1713
  database_name=global_string_database_name,
1292
1714
  schema_name=global_string_schema_name,
1293
1715
  table_name=User.__tablename__,
@@ -1297,6 +1719,18 @@ async def delete_user_v0(
1297
1719
  }
1298
1720
  ),
1299
1721
  )
1722
+ # delete profile photo if exists
1723
+ if user_profile_storage_token:
1724
+ try:
1725
+ global_object_square_file_store_helper.delete_file_v0(
1726
+ list_file_storage_token=[user_profile_storage_token]
1727
+ )
1728
+ except HTTPError as he:
1729
+ global_object_square_logger.warning(
1730
+ f"Failed to delete user profile photo with storage token {user_profile_storage_token}. "
1731
+ f"Error: {he.response.text}",
1732
+ exc_info=True,
1733
+ )
1300
1734
  """
1301
1735
  return value
1302
1736
  """
@@ -1332,6 +1766,8 @@ async def update_password_v0(
1332
1766
  ):
1333
1767
  old_password = body.old_password
1334
1768
  new_password = body.new_password
1769
+ logout_other_sessions = body.logout_other_sessions
1770
+ preserve_session_refresh_token = body.preserve_session_refresh_token
1335
1771
  try:
1336
1772
  """
1337
1773
  validation
@@ -1371,7 +1807,47 @@ async def update_password_v0(
1371
1807
  status_code=status.HTTP_400_BAD_REQUEST,
1372
1808
  detail=output_content,
1373
1809
  )
1374
-
1810
+ # check if user has SELF auth provider
1811
+ local_list_response_user_auth_provider = (
1812
+ global_object_square_database_helper.get_rows_v0(
1813
+ database_name=global_string_database_name,
1814
+ schema_name=global_string_schema_name,
1815
+ table_name=UserAuthProvider.__tablename__,
1816
+ filters=FiltersV0(
1817
+ root={
1818
+ UserAuthProvider.user_id.name: FilterConditionsV0(eq=user_id),
1819
+ UserAuthProvider.auth_provider.name: FilterConditionsV0(
1820
+ eq=AuthProviderEnum.SELF.value
1821
+ ),
1822
+ }
1823
+ ),
1824
+ )["data"]["main"]
1825
+ )
1826
+ if len(local_list_response_user_auth_provider) != 1:
1827
+ output_content = get_api_output_in_standard_format(
1828
+ message=messages["INCORRECT_AUTH_PROVIDER"],
1829
+ log=f"user_id: {user_id} does not have {AuthProviderEnum.SELF.value} auth provider.",
1830
+ )
1831
+ raise HTTPException(
1832
+ status_code=status.HTTP_400_BAD_REQUEST,
1833
+ detail=output_content,
1834
+ )
1835
+ # check if user has credentials (might not be set in case of errors in registration.)
1836
+ local_list_response_user = global_object_square_database_helper.get_rows_v0(
1837
+ database_name=global_string_database_name,
1838
+ schema_name=global_string_schema_name,
1839
+ table_name=User.__tablename__,
1840
+ filters=FiltersV0(root={User.user_id.name: FilterConditionsV0(eq=user_id)}),
1841
+ )["data"]["main"]
1842
+ if len(local_list_response_user) != 1:
1843
+ output_content = get_api_output_in_standard_format(
1844
+ message=messages["MALFORMED_USER"],
1845
+ log=f"user_id: {user_id} does not have credentials.",
1846
+ )
1847
+ raise HTTPException(
1848
+ status_code=status.HTTP_400_BAD_REQUEST,
1849
+ detail=output_content,
1850
+ )
1375
1851
  # validate password
1376
1852
  local_dict_user = local_list_authentication_user_response[0]
1377
1853
  if not (
@@ -1390,10 +1866,41 @@ async def update_password_v0(
1390
1866
  status_code=status.HTTP_400_BAD_REQUEST,
1391
1867
  detail=output_content,
1392
1868
  )
1869
+ # check if provided refresh token is valid
1870
+ if preserve_session_refresh_token:
1871
+ local_dict_token_payload = get_jwt_payload(
1872
+ preserve_session_refresh_token, config_str_secret_key_for_refresh_token
1873
+ )
1874
+ local_list_response_user_session = global_object_square_database_helper.get_rows_v0(
1875
+ database_name=global_string_database_name,
1876
+ schema_name=global_string_schema_name,
1877
+ table_name=UserSession.__tablename__,
1878
+ filters=FiltersV0(
1879
+ root={
1880
+ UserSession.user_id.name: FilterConditionsV0(eq=user_id),
1881
+ UserSession.user_session_refresh_token.name: FilterConditionsV0(
1882
+ eq=preserve_session_refresh_token
1883
+ ),
1884
+ }
1885
+ ),
1886
+ )[
1887
+ "data"
1888
+ ][
1889
+ "main"
1890
+ ]
1891
+ if len(local_list_response_user_session) != 1:
1892
+ output_content = get_api_output_in_standard_format(
1893
+ message=messages["INCORRECT_REFRESH_TOKEN"],
1894
+ log=f"incorrect refresh token: {preserve_session_refresh_token}.",
1895
+ )
1896
+ raise HTTPException(
1897
+ status_code=status.HTTP_400_BAD_REQUEST,
1898
+ detail=output_content,
1899
+ )
1393
1900
  """
1394
1901
  main process
1395
1902
  """
1396
- # delete the user.
1903
+ # update the password
1397
1904
  local_str_hashed_password = bcrypt.hashpw(
1398
1905
  new_password.encode("utf-8"), bcrypt.gensalt()
1399
1906
  ).decode("utf-8")
@@ -1410,6 +1917,34 @@ async def update_password_v0(
1410
1917
  UserCredential.user_credential_hashed_password.name: local_str_hashed_password,
1411
1918
  },
1412
1919
  )
1920
+ if logout_other_sessions:
1921
+ if preserve_session_refresh_token:
1922
+ # delete all sessions for user except the one with the given refresh token
1923
+ global_object_square_database_helper.delete_rows_v0(
1924
+ database_name=global_string_database_name,
1925
+ schema_name=global_string_schema_name,
1926
+ table_name=UserSession.__tablename__,
1927
+ filters=FiltersV0(
1928
+ root={
1929
+ UserSession.user_id.name: FilterConditionsV0(eq=user_id),
1930
+ UserSession.user_session_refresh_token.name: FilterConditionsV0(
1931
+ ne=preserve_session_refresh_token
1932
+ ),
1933
+ }
1934
+ ),
1935
+ )
1936
+ else:
1937
+ # delete all sessions for user
1938
+ global_object_square_database_helper.delete_rows_v0(
1939
+ database_name=global_string_database_name,
1940
+ schema_name=global_string_schema_name,
1941
+ table_name=UserSession.__tablename__,
1942
+ filters=FiltersV0(
1943
+ root={
1944
+ UserSession.user_id.name: FilterConditionsV0(eq=user_id),
1945
+ }
1946
+ ),
1947
+ )
1413
1948
  """
1414
1949
  return value
1415
1950
  """
@@ -1581,7 +2116,44 @@ async def update_user_recovery_methods_v0(
1581
2116
  status_code=status.HTTP_400_BAD_REQUEST,
1582
2117
  detail=output_content,
1583
2118
  )
1584
-
2119
+ # check if user's email is verified in user profile.
2120
+ # maybe too harsh to reject the request entirely,
2121
+ # but for practical purposes this api call should be used for 1 recovery method at a time, so it's not too bad.
2122
+ if RecoveryMethodEnum.EMAIL.value in recovery_methods_to_add:
2123
+ local_list_response_user_profile = (
2124
+ global_object_square_database_helper.get_rows_v0(
2125
+ database_name=global_string_database_name,
2126
+ schema_name=global_string_schema_name,
2127
+ table_name=UserProfile.__tablename__,
2128
+ filters=FiltersV0(
2129
+ root={
2130
+ UserProfile.user_id.name: FilterConditionsV0(eq=user_id),
2131
+ }
2132
+ ),
2133
+ )["data"]["main"]
2134
+ )
2135
+ if len(local_list_response_user_profile) != 1:
2136
+ # maybe this should raise 500 as this error will not occur if code runs correctly.
2137
+ output_content = get_api_output_in_standard_format(
2138
+ message=messages["GENERIC_400"],
2139
+ log=f"user_id: {user_id} does not have a profile.",
2140
+ )
2141
+ raise HTTPException(
2142
+ status_code=status.HTTP_400_BAD_REQUEST,
2143
+ detail=output_content,
2144
+ )
2145
+ local_dict_user_profile = local_list_response_user_profile[0]
2146
+ if not local_dict_user_profile[
2147
+ UserProfile.user_profile_email_verified.name
2148
+ ]:
2149
+ output_content = get_api_output_in_standard_format(
2150
+ message=messages["EMAIL_NOT_VERIFIED"],
2151
+ log=f"user_id: {user_id} does not have email verified.",
2152
+ )
2153
+ raise HTTPException(
2154
+ status_code=status.HTTP_400_BAD_REQUEST,
2155
+ detail=output_content,
2156
+ )
1585
2157
  """
1586
2158
  main process
1587
2159
  """
@@ -1619,6 +2191,28 @@ async def update_user_recovery_methods_v0(
1619
2191
  )
1620
2192
 
1621
2193
  # logic for removing recovery_methods
2194
+ remove_old_backup_codes = (
2195
+ RecoveryMethodEnum.BACKUP_CODE.value in recovery_methods_to_remove
2196
+ )
2197
+ old_backup_code_hashes = None
2198
+ if remove_old_backup_codes:
2199
+ # delete existing backup codes if any
2200
+ old_backup_code_hashes = global_object_square_database_helper.get_rows_v0(
2201
+ database_name=global_string_database_name,
2202
+ schema_name=global_string_schema_name,
2203
+ table_name=UserVerificationCode.__tablename__,
2204
+ filters=FiltersV0(
2205
+ root={
2206
+ UserVerificationCode.user_id.name: FilterConditionsV0(
2207
+ eq=user_id
2208
+ ),
2209
+ UserVerificationCode.user_verification_code_type.name: FilterConditionsV0(
2210
+ eq=VerificationCodeTypeEnum.BACKUP_CODE_RECOVERY.value
2211
+ ),
2212
+ }
2213
+ ),
2214
+ columns=[UserVerificationCode.user_verification_code_hash.name],
2215
+ )["data"]["main"]
1622
2216
  global_object_square_database_helper.delete_rows_v0(
1623
2217
  database_name=global_string_database_name,
1624
2218
  schema_name=global_string_schema_name,
@@ -1632,6 +2226,22 @@ async def update_user_recovery_methods_v0(
1632
2226
  }
1633
2227
  ),
1634
2228
  )
2229
+ if remove_old_backup_codes and old_backup_code_hashes:
2230
+ global_object_square_database_helper.delete_rows_v0(
2231
+ database_name=global_string_database_name,
2232
+ schema_name=global_string_schema_name,
2233
+ table_name=UserVerificationCode.__tablename__,
2234
+ filters=FiltersV0(
2235
+ root={
2236
+ UserVerificationCode.user_verification_code_hash.name: FilterConditionsV0(
2237
+ in_=[
2238
+ x[UserVerificationCode.user_verification_code_hash.name]
2239
+ for x in old_backup_code_hashes
2240
+ ]
2241
+ ),
2242
+ }
2243
+ ),
2244
+ )
1635
2245
 
1636
2246
  """
1637
2247
  return value
@@ -1680,3 +2290,958 @@ async def update_user_recovery_methods_v0(
1680
2290
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1681
2291
  content=output_content,
1682
2292
  )
2293
+
2294
+
2295
+ @router.post("/generate_account_backup_codes/v0")
2296
+ @global_object_square_logger.auto_logger()
2297
+ async def generate_account_backup_codes_v0(
2298
+ access_token: Annotated[str, Header()],
2299
+ ):
2300
+
2301
+ try:
2302
+ """
2303
+ validation
2304
+ """
2305
+ try:
2306
+ local_dict_access_token_payload = get_jwt_payload(
2307
+ access_token, config_str_secret_key_for_access_token
2308
+ )
2309
+ except Exception as error:
2310
+ output_content = get_api_output_in_standard_format(
2311
+ message=messages["INCORRECT_ACCESS_TOKEN"], log=str(error)
2312
+ )
2313
+ raise HTTPException(
2314
+ status_code=status.HTTP_400_BAD_REQUEST,
2315
+ detail=output_content,
2316
+ )
2317
+ user_id = local_dict_access_token_payload["user_id"]
2318
+ # check if user has recovery method enabled
2319
+ local_list_response_user_recovery_methods = global_object_square_database_helper.get_rows_v0(
2320
+ database_name=global_string_database_name,
2321
+ schema_name=global_string_schema_name,
2322
+ table_name=UserRecoveryMethod.__tablename__,
2323
+ filters=FiltersV0(
2324
+ root={
2325
+ UserRecoveryMethod.user_id.name: FilterConditionsV0(eq=user_id),
2326
+ UserRecoveryMethod.user_recovery_method_name.name: FilterConditionsV0(
2327
+ eq=RecoveryMethodEnum.BACKUP_CODE.value
2328
+ ),
2329
+ }
2330
+ ),
2331
+ )[
2332
+ "data"
2333
+ ][
2334
+ "main"
2335
+ ]
2336
+ if len(local_list_response_user_recovery_methods) != 1:
2337
+ output_content = get_api_output_in_standard_format(
2338
+ message=messages["RECOVERY_METHOD_NOT_ENABLED"],
2339
+ log=f"user_id: {user_id} does not have backup codes recovery method enabled.",
2340
+ )
2341
+ raise HTTPException(
2342
+ status_code=status.HTTP_400_BAD_REQUEST,
2343
+ detail=output_content,
2344
+ )
2345
+ """
2346
+ main process
2347
+ """
2348
+ # delete existing backup codes if any
2349
+ old_backup_code_hashes = global_object_square_database_helper.get_rows_v0(
2350
+ database_name=global_string_database_name,
2351
+ schema_name=global_string_schema_name,
2352
+ table_name=UserVerificationCode.__tablename__,
2353
+ filters=FiltersV0(
2354
+ root={
2355
+ UserVerificationCode.user_id.name: FilterConditionsV0(eq=user_id),
2356
+ UserVerificationCode.user_verification_code_type.name: FilterConditionsV0(
2357
+ eq=VerificationCodeTypeEnum.BACKUP_CODE_RECOVERY.value
2358
+ ),
2359
+ }
2360
+ ),
2361
+ columns=[UserVerificationCode.user_verification_code_hash.name],
2362
+ )["data"]["main"]
2363
+
2364
+ # generate backup codes
2365
+ backup_codes = []
2366
+ db_data = []
2367
+
2368
+ for i in range(NUMBER_OF_RECOVERY_CODES):
2369
+ backup_code = str(uuid.uuid4())
2370
+ backup_codes.append(backup_code)
2371
+ # hash the backup code
2372
+ local_str_hashed_backup_code = bcrypt.hashpw(
2373
+ backup_code.encode("utf-8"), bcrypt.gensalt()
2374
+ ).decode("utf-8")
2375
+
2376
+ db_data.append(
2377
+ {
2378
+ UserVerificationCode.user_id.name: user_id,
2379
+ UserVerificationCode.user_verification_code_type.name: VerificationCodeTypeEnum.BACKUP_CODE_RECOVERY.value,
2380
+ UserVerificationCode.user_verification_code_hash.name: local_str_hashed_backup_code,
2381
+ }
2382
+ )
2383
+ global_object_square_database_helper.insert_rows_v0(
2384
+ database_name=global_string_database_name,
2385
+ schema_name=global_string_schema_name,
2386
+ table_name=UserVerificationCode.__tablename__,
2387
+ data=db_data,
2388
+ )
2389
+ global_object_square_database_helper.delete_rows_v0(
2390
+ database_name=global_string_database_name,
2391
+ schema_name=global_string_schema_name,
2392
+ table_name=UserVerificationCode.__tablename__,
2393
+ filters=FiltersV0(
2394
+ root={
2395
+ UserVerificationCode.user_verification_code_hash.name: FilterConditionsV0(
2396
+ in_=[
2397
+ x[UserVerificationCode.user_verification_code_hash.name]
2398
+ for x in old_backup_code_hashes
2399
+ ]
2400
+ ),
2401
+ }
2402
+ ),
2403
+ )
2404
+ """
2405
+ return value
2406
+ """
2407
+ output_content = get_api_output_in_standard_format(
2408
+ message=messages["GENERIC_CREATION_SUCCESSFUL"],
2409
+ data={
2410
+ "main": {
2411
+ "user_id": user_id,
2412
+ "backup_codes": backup_codes,
2413
+ }
2414
+ },
2415
+ )
2416
+ return JSONResponse(status_code=status.HTTP_200_OK, content=output_content)
2417
+ except HTTPException as http_exception:
2418
+ global_object_square_logger.logger.error(http_exception, exc_info=True)
2419
+ return JSONResponse(
2420
+ status_code=http_exception.status_code, content=http_exception.detail
2421
+ )
2422
+ except Exception as e:
2423
+ """
2424
+ rollback logic
2425
+ """
2426
+ global_object_square_logger.logger.error(e, exc_info=True)
2427
+ output_content = get_api_output_in_standard_format(
2428
+ message=messages["GENERIC_500"],
2429
+ log=str(e),
2430
+ )
2431
+ return JSONResponse(
2432
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=output_content
2433
+ )
2434
+
2435
+
2436
+ @router.post("/reset_password_and_login_using_backup_code/v0")
2437
+ @global_object_square_logger.auto_logger()
2438
+ async def reset_password_and_login_using_backup_code_v0(
2439
+ body: ResetPasswordAndLoginUsingBackupCodeV0,
2440
+ ):
2441
+ backup_code = body.backup_code
2442
+ username = body.username
2443
+ new_password = body.new_password
2444
+ app_id = body.app_id
2445
+ logout_other_sessions = body.logout_other_sessions
2446
+ try:
2447
+ """
2448
+ validation
2449
+ """
2450
+ # validate username
2451
+ local_list_authentication_user_response = (
2452
+ global_object_square_database_helper.get_rows_v0(
2453
+ database_name=global_string_database_name,
2454
+ schema_name=global_string_schema_name,
2455
+ table_name=User.__tablename__,
2456
+ filters=FiltersV0(
2457
+ root={User.user_username.name: FilterConditionsV0(eq=username)}
2458
+ ),
2459
+ )["data"]["main"]
2460
+ )
2461
+ if len(local_list_authentication_user_response) != 1:
2462
+ output_content = get_api_output_in_standard_format(
2463
+ message=messages["INCORRECT_USERNAME"],
2464
+ log=f"incorrect username: {username}.",
2465
+ )
2466
+ raise HTTPException(
2467
+ status_code=status.HTTP_400_BAD_REQUEST,
2468
+ detail=output_content,
2469
+ )
2470
+ user_id = local_list_authentication_user_response[0][User.user_id.name]
2471
+ # check if user has SELF auth provider
2472
+ local_list_response_user_auth_provider = (
2473
+ global_object_square_database_helper.get_rows_v0(
2474
+ database_name=global_string_database_name,
2475
+ schema_name=global_string_schema_name,
2476
+ table_name=UserAuthProvider.__tablename__,
2477
+ filters=FiltersV0(
2478
+ root={
2479
+ UserAuthProvider.user_id.name: FilterConditionsV0(eq=user_id),
2480
+ UserAuthProvider.auth_provider.name: FilterConditionsV0(
2481
+ eq=AuthProviderEnum.SELF.value
2482
+ ),
2483
+ }
2484
+ ),
2485
+ )["data"]["main"]
2486
+ )
2487
+ if len(local_list_response_user_auth_provider) != 1:
2488
+ output_content = get_api_output_in_standard_format(
2489
+ message=messages["INCORRECT_AUTH_PROVIDER"],
2490
+ log=f"user_id: {user_id} does not have {AuthProviderEnum.SELF.value} auth provider.",
2491
+ )
2492
+ raise HTTPException(
2493
+ status_code=status.HTTP_400_BAD_REQUEST,
2494
+ detail=output_content,
2495
+ )
2496
+ # check if user has recovery method enabled
2497
+ local_list_response_user_recovery_methods = global_object_square_database_helper.get_rows_v0(
2498
+ database_name=global_string_database_name,
2499
+ schema_name=global_string_schema_name,
2500
+ table_name=UserRecoveryMethod.__tablename__,
2501
+ filters=FiltersV0(
2502
+ root={
2503
+ UserRecoveryMethod.user_id.name: FilterConditionsV0(eq=user_id),
2504
+ UserRecoveryMethod.user_recovery_method_name.name: FilterConditionsV0(
2505
+ eq=RecoveryMethodEnum.BACKUP_CODE.value
2506
+ ),
2507
+ }
2508
+ ),
2509
+ )[
2510
+ "data"
2511
+ ][
2512
+ "main"
2513
+ ]
2514
+ if len(local_list_response_user_recovery_methods) != 1:
2515
+ output_content = get_api_output_in_standard_format(
2516
+ message=messages["RECOVERY_METHOD_NOT_ENABLED"],
2517
+ log=f"user_id: {user_id} does not have backup codes recovery method enabled.",
2518
+ )
2519
+ raise HTTPException(
2520
+ status_code=status.HTTP_400_BAD_REQUEST,
2521
+ detail=output_content,
2522
+ )
2523
+ # validate if user is assigned to the app.
2524
+ # not checking [skipping] if the app exists, as it is not required for this endpoint.
2525
+ local_list_response_user_app = global_object_square_database_helper.get_rows_v0(
2526
+ database_name=global_string_database_name,
2527
+ schema_name=global_string_schema_name,
2528
+ table_name=UserApp.__tablename__,
2529
+ filters=FiltersV0(
2530
+ root={
2531
+ UserApp.user_id.name: FilterConditionsV0(eq=user_id),
2532
+ UserApp.app_id.name: FilterConditionsV0(eq=app_id),
2533
+ }
2534
+ ),
2535
+ )["data"]["main"]
2536
+ if len(local_list_response_user_app) == 0:
2537
+ output_content = get_api_output_in_standard_format(
2538
+ message=messages["GENERIC_400"],
2539
+ log=f"user_id: {user_id} is not assigned to app_id: {app_id}.",
2540
+ )
2541
+ raise HTTPException(
2542
+ status_code=status.HTTP_400_BAD_REQUEST,
2543
+ detail=output_content,
2544
+ )
2545
+ """
2546
+ main process
2547
+ """
2548
+ # validate backup code
2549
+ local_list_response_user_verification_code = global_object_square_database_helper.get_rows_v0(
2550
+ database_name=global_string_database_name,
2551
+ schema_name=global_string_schema_name,
2552
+ table_name=UserVerificationCode.__tablename__,
2553
+ filters=FiltersV0(
2554
+ root={
2555
+ UserVerificationCode.user_id.name: FilterConditionsV0(eq=user_id),
2556
+ UserVerificationCode.user_verification_code_type.name: FilterConditionsV0(
2557
+ eq=VerificationCodeTypeEnum.BACKUP_CODE_RECOVERY.value
2558
+ ),
2559
+ UserVerificationCode.user_verification_code_expires_at.name: FilterConditionsV0(
2560
+ is_null=True
2561
+ ),
2562
+ UserVerificationCode.user_verification_code_used_at.name: FilterConditionsV0(
2563
+ is_null=True
2564
+ ),
2565
+ }
2566
+ ),
2567
+ columns=[UserVerificationCode.user_verification_code_hash.name],
2568
+ )[
2569
+ "data"
2570
+ ][
2571
+ "main"
2572
+ ]
2573
+ # find the backup code in the list
2574
+ local_list_response_user_verification_code = [
2575
+ x
2576
+ for x in local_list_response_user_verification_code
2577
+ if bcrypt.checkpw(
2578
+ backup_code.encode("utf-8"),
2579
+ x[UserVerificationCode.user_verification_code_hash.name].encode(
2580
+ "utf-8"
2581
+ ),
2582
+ )
2583
+ ]
2584
+ if len(local_list_response_user_verification_code) != 1:
2585
+ output_content = get_api_output_in_standard_format(
2586
+ message=messages["INCORRECT_BACKUP_CODE"],
2587
+ log=f"incorrect backup code: {backup_code} for user_id: {user_id}.",
2588
+ )
2589
+ raise HTTPException(
2590
+ status_code=status.HTTP_400_BAD_REQUEST,
2591
+ detail=output_content,
2592
+ )
2593
+ # hash the new password
2594
+ local_str_hashed_password = bcrypt.hashpw(
2595
+ new_password.encode("utf-8"), bcrypt.gensalt()
2596
+ ).decode("utf-8")
2597
+ # update the password
2598
+ global_object_square_database_helper.edit_rows_v0(
2599
+ database_name=global_string_database_name,
2600
+ schema_name=global_string_schema_name,
2601
+ table_name=UserCredential.__tablename__,
2602
+ filters=FiltersV0(
2603
+ root={
2604
+ UserCredential.user_id.name: FilterConditionsV0(eq=user_id),
2605
+ }
2606
+ ),
2607
+ data={
2608
+ UserCredential.user_credential_hashed_password.name: local_str_hashed_password,
2609
+ },
2610
+ )
2611
+ # mark the backup code as used
2612
+ global_object_square_database_helper.edit_rows_v0(
2613
+ database_name=global_string_database_name,
2614
+ schema_name=global_string_schema_name,
2615
+ table_name=UserVerificationCode.__tablename__,
2616
+ filters=FiltersV0(
2617
+ root={
2618
+ UserVerificationCode.user_id.name: FilterConditionsV0(eq=user_id),
2619
+ UserVerificationCode.user_verification_code_type.name: FilterConditionsV0(
2620
+ eq=VerificationCodeTypeEnum.BACKUP_CODE_RECOVERY.value
2621
+ ),
2622
+ UserVerificationCode.user_verification_code_hash.name: FilterConditionsV0(
2623
+ eq=local_list_response_user_verification_code[0][
2624
+ UserVerificationCode.user_verification_code_hash.name
2625
+ ]
2626
+ ),
2627
+ }
2628
+ ),
2629
+ data={
2630
+ UserVerificationCode.user_verification_code_used_at.name: datetime.now(
2631
+ timezone.utc
2632
+ ).strftime("%Y-%m-%d %H:%M:%S.%f+00"),
2633
+ },
2634
+ )
2635
+ if logout_other_sessions:
2636
+ # delete all sessions for user
2637
+ global_object_square_database_helper.delete_rows_v0(
2638
+ database_name=global_string_database_name,
2639
+ schema_name=global_string_schema_name,
2640
+ table_name=UserSession.__tablename__,
2641
+ filters=FiltersV0(
2642
+ root={
2643
+ UserSession.user_id.name: FilterConditionsV0(eq=user_id),
2644
+ }
2645
+ ),
2646
+ )
2647
+ # generate access token and refresh token
2648
+ local_dict_access_token_payload = {
2649
+ "app_id": app_id,
2650
+ "user_id": user_id,
2651
+ "exp": datetime.now(timezone.utc)
2652
+ + timedelta(minutes=config_int_access_token_valid_minutes),
2653
+ }
2654
+ local_str_access_token = jwt.encode(
2655
+ local_dict_access_token_payload, config_str_secret_key_for_access_token
2656
+ )
2657
+ local_object_refresh_token_expiry_time = datetime.now(timezone.utc) + timedelta(
2658
+ minutes=config_int_refresh_token_valid_minutes
2659
+ )
2660
+ local_dict_refresh_token_payload = {
2661
+ "app_id": app_id,
2662
+ "user_id": user_id,
2663
+ "exp": local_object_refresh_token_expiry_time,
2664
+ }
2665
+ local_str_refresh_token = jwt.encode(
2666
+ local_dict_refresh_token_payload, config_str_secret_key_for_refresh_token
2667
+ )
2668
+ # insert the refresh token in the database
2669
+ global_object_square_database_helper.insert_rows_v0(
2670
+ database_name=global_string_database_name,
2671
+ schema_name=global_string_schema_name,
2672
+ table_name=UserSession.__tablename__,
2673
+ data=[
2674
+ {
2675
+ UserSession.user_id.name: user_id,
2676
+ UserSession.app_id.name: app_id,
2677
+ UserSession.user_session_refresh_token.name: local_str_refresh_token,
2678
+ UserSession.user_session_expiry_time.name: local_object_refresh_token_expiry_time.strftime(
2679
+ "%Y-%m-%d %H:%M:%S.%f+00"
2680
+ ),
2681
+ }
2682
+ ],
2683
+ )
2684
+ """
2685
+ return value
2686
+ """
2687
+ output_content = get_api_output_in_standard_format(
2688
+ message=messages["GENERIC_ACTION_SUCCESSFUL"],
2689
+ data={
2690
+ "main": {
2691
+ "user_id": user_id,
2692
+ "access_token": local_str_access_token,
2693
+ "refresh_token": local_str_refresh_token,
2694
+ }
2695
+ },
2696
+ )
2697
+
2698
+ return JSONResponse(status_code=status.HTTP_200_OK, content=output_content)
2699
+ except HTTPException as http_exception:
2700
+ global_object_square_logger.logger.error(http_exception, exc_info=True)
2701
+ return JSONResponse(
2702
+ status_code=http_exception.status_code, content=http_exception.detail
2703
+ )
2704
+ except Exception as e:
2705
+ """
2706
+ rollback logic
2707
+ """
2708
+ global_object_square_logger.logger.error(e, exc_info=True)
2709
+ output_content = get_api_output_in_standard_format(
2710
+ message=messages["GENERIC_500"],
2711
+ log=str(e),
2712
+ )
2713
+ return JSONResponse(
2714
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=output_content
2715
+ )
2716
+
2717
+
2718
+ @router.post("/send_reset_password_email/v0")
2719
+ @global_object_square_logger.auto_logger()
2720
+ async def send_reset_password_email_v0(
2721
+ body: SendResetPasswordEmailV0,
2722
+ ):
2723
+ username = body.username
2724
+ try:
2725
+ """
2726
+ validation
2727
+ """
2728
+ # validate username
2729
+ local_list_authentication_user_response = (
2730
+ global_object_square_database_helper.get_rows_v0(
2731
+ database_name=global_string_database_name,
2732
+ schema_name=global_string_schema_name,
2733
+ table_name=User.__tablename__,
2734
+ filters=FiltersV0(
2735
+ root={User.user_username.name: FilterConditionsV0(eq=username)}
2736
+ ),
2737
+ )["data"]["main"]
2738
+ )
2739
+ if len(local_list_authentication_user_response) != 1:
2740
+ output_content = get_api_output_in_standard_format(
2741
+ message=messages["INCORRECT_USERNAME"],
2742
+ log=f"incorrect username: {username}.",
2743
+ )
2744
+ raise HTTPException(
2745
+ status_code=status.HTTP_400_BAD_REQUEST,
2746
+ detail=output_content,
2747
+ )
2748
+ user_id = local_list_authentication_user_response[0][User.user_id.name]
2749
+ # check if user has SELF auth provider
2750
+ local_list_response_user_auth_provider = (
2751
+ global_object_square_database_helper.get_rows_v0(
2752
+ database_name=global_string_database_name,
2753
+ schema_name=global_string_schema_name,
2754
+ table_name=UserAuthProvider.__tablename__,
2755
+ filters=FiltersV0(
2756
+ root={
2757
+ UserAuthProvider.user_id.name: FilterConditionsV0(eq=user_id),
2758
+ UserAuthProvider.auth_provider.name: FilterConditionsV0(
2759
+ eq=AuthProviderEnum.SELF.value
2760
+ ),
2761
+ }
2762
+ ),
2763
+ )["data"]["main"]
2764
+ )
2765
+ if len(local_list_response_user_auth_provider) != 1:
2766
+ output_content = get_api_output_in_standard_format(
2767
+ message=messages["INCORRECT_AUTH_PROVIDER"],
2768
+ log=f"user_id: {user_id} does not have {AuthProviderEnum.SELF.value} auth provider.",
2769
+ )
2770
+ raise HTTPException(
2771
+ status_code=status.HTTP_400_BAD_REQUEST,
2772
+ detail=output_content,
2773
+ )
2774
+ # check if user has recovery method enabled
2775
+ local_list_response_user_recovery_methods = global_object_square_database_helper.get_rows_v0(
2776
+ database_name=global_string_database_name,
2777
+ schema_name=global_string_schema_name,
2778
+ table_name=UserRecoveryMethod.__tablename__,
2779
+ filters=FiltersV0(
2780
+ root={
2781
+ UserRecoveryMethod.user_id.name: FilterConditionsV0(eq=user_id),
2782
+ UserRecoveryMethod.user_recovery_method_name.name: FilterConditionsV0(
2783
+ eq=RecoveryMethodEnum.EMAIL.value
2784
+ ),
2785
+ }
2786
+ ),
2787
+ )[
2788
+ "data"
2789
+ ][
2790
+ "main"
2791
+ ]
2792
+ if len(local_list_response_user_recovery_methods) != 1:
2793
+ output_content = get_api_output_in_standard_format(
2794
+ message=messages["RECOVERY_METHOD_NOT_ENABLED"],
2795
+ log=f"user_id: {user_id} does not have email recovery method enabled.",
2796
+ )
2797
+ raise HTTPException(
2798
+ status_code=status.HTTP_400_BAD_REQUEST,
2799
+ detail=output_content,
2800
+ )
2801
+ # validate if user has email in profile
2802
+ user_profile_response = global_object_square_database_helper.get_rows_v0(
2803
+ database_name=global_string_database_name,
2804
+ schema_name=global_string_schema_name,
2805
+ table_name=UserProfile.__tablename__,
2806
+ filters=FiltersV0(
2807
+ root={UserProfile.user_id.name: FilterConditionsV0(eq=user_id)}
2808
+ ),
2809
+ apply_filters=True,
2810
+ )
2811
+ user_profile_data = user_profile_response["data"]["main"][0]
2812
+ if not user_profile_data.get(UserProfile.user_profile_email.name):
2813
+ output_content = get_api_output_in_standard_format(
2814
+ message=messages["GENERIC_MISSING_REQUIRED_FIELD"],
2815
+ log="email is required to send verification email.",
2816
+ )
2817
+ raise HTTPException(
2818
+ status_code=status.HTTP_400_BAD_REQUEST,
2819
+ detail=output_content,
2820
+ )
2821
+ # check if email is not verified
2822
+ if not user_profile_data.get(UserProfile.user_profile_email_verified.name):
2823
+ output_content = get_api_output_in_standard_format(
2824
+ message=messages["EMAIL_NOT_VERIFIED"],
2825
+ log="email is not verified.",
2826
+ )
2827
+ return JSONResponse(status_code=status.HTTP_200_OK, content=output_content)
2828
+
2829
+ """
2830
+ main process
2831
+ """
2832
+ verification_code = random.randint(
2833
+ 10 ** (NUMBER_OF_DIGITS_IN_EMAIL_PASSWORD_RESET_CODE - 1),
2834
+ 10**NUMBER_OF_DIGITS_IN_EMAIL_PASSWORD_RESET_CODE - 1,
2835
+ )
2836
+ # hash the verification code
2837
+ hashed_verification_code = bcrypt.hashpw(
2838
+ str(verification_code).encode("utf-8"), bcrypt.gensalt()
2839
+ ).decode("utf-8")
2840
+ expires_at = datetime.now(timezone.utc) + timedelta(
2841
+ seconds=EXPIRY_TIME_FOR_EMAIL_PASSWORD_RESET_CODE_IN_SECONDS
2842
+ )
2843
+ # add verification code to UserVerification code table
2844
+ global_object_square_database_helper.insert_rows_v0(
2845
+ database_name=global_string_database_name,
2846
+ schema_name=global_string_schema_name,
2847
+ table_name=UserVerificationCode.__tablename__,
2848
+ data=[
2849
+ {
2850
+ UserVerificationCode.user_id.name: user_id,
2851
+ UserVerificationCode.user_verification_code_type.name: VerificationCodeTypeEnum.EMAIL_RECOVERY.value,
2852
+ UserVerificationCode.user_verification_code_hash.name: hashed_verification_code,
2853
+ UserVerificationCode.user_verification_code_expires_at.name: expires_at.strftime(
2854
+ "%Y-%m-%d %H:%M:%S.%f+00"
2855
+ ),
2856
+ }
2857
+ ],
2858
+ )
2859
+ # send verification email
2860
+ if (
2861
+ user_profile_data[UserProfile.user_profile_first_name.name]
2862
+ and user_profile_data[UserProfile.user_profile_last_name.name]
2863
+ ):
2864
+ user_to_name = f"{user_profile_data[UserProfile.user_profile_first_name.name]} {user_profile_data[UserProfile.user_profile_last_name.name]}"
2865
+ elif user_profile_data[UserProfile.user_profile_first_name.name]:
2866
+ user_to_name = user_profile_data[UserProfile.user_profile_first_name.name]
2867
+ elif user_profile_data[UserProfile.user_profile_last_name.name]:
2868
+ user_to_name = user_profile_data[UserProfile.user_profile_last_name.name]
2869
+ else:
2870
+ user_to_name = ""
2871
+
2872
+ mailgun_response = send_email_using_mailgun(
2873
+ from_email="auth@thepmsquare.com",
2874
+ from_name="square_authentication",
2875
+ to_email=user_profile_data[UserProfile.user_profile_email.name],
2876
+ to_name=user_to_name,
2877
+ subject="Password Reset Verification Code",
2878
+ body=f"Your Password Reset verification code is {verification_code}. It will expire in {EXPIRY_TIME_FOR_EMAIL_PASSWORD_RESET_CODE_IN_SECONDS/60} minutes.",
2879
+ api_key=MAIL_GUN_API_KEY,
2880
+ domain_name="thepmsquare.com",
2881
+ )
2882
+ # add log for email sending
2883
+ global_object_square_database_helper.insert_rows_v0(
2884
+ database_name=global_string_database_name,
2885
+ schema_name=email_schema_name,
2886
+ table_name=EmailLog.__tablename__,
2887
+ data=[
2888
+ {
2889
+ EmailLog.user_id.name: user_id,
2890
+ EmailLog.recipient_email.name: user_profile_data[
2891
+ UserProfile.user_profile_email.name
2892
+ ],
2893
+ EmailLog.email_type.name: EmailTypeEnum.VERIFY_EMAIL.value,
2894
+ EmailLog.status.name: EmailStatusEnum.SENT.value,
2895
+ EmailLog.third_party_message_id.name: mailgun_response.get("id"),
2896
+ }
2897
+ ],
2898
+ )
2899
+ """
2900
+ return value
2901
+ """
2902
+ output_content = get_api_output_in_standard_format(
2903
+ data={
2904
+ "expires_at": expires_at.isoformat(),
2905
+ },
2906
+ message=messages["GENERIC_ACTION_SUCCESSFUL"],
2907
+ )
2908
+ return JSONResponse(
2909
+ status_code=status.HTTP_200_OK,
2910
+ content=output_content,
2911
+ )
2912
+ except HTTPException as http_exception:
2913
+ global_object_square_logger.logger.error(http_exception, exc_info=True)
2914
+ return JSONResponse(
2915
+ status_code=http_exception.status_code, content=http_exception.detail
2916
+ )
2917
+ except Exception as e:
2918
+ """
2919
+ rollback logic
2920
+ """
2921
+ global_object_square_logger.logger.error(e, exc_info=True)
2922
+ output_content = get_api_output_in_standard_format(
2923
+ message=messages["GENERIC_500"],
2924
+ log=str(e),
2925
+ )
2926
+ return JSONResponse(
2927
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=output_content
2928
+ )
2929
+
2930
+
2931
+ @router.post("/reset_password_and_login_using_reset_email_code/v0")
2932
+ @global_object_square_logger.auto_logger()
2933
+ async def reset_password_and_login_using_reset_email_code_v0(
2934
+ body: ResetPasswordAndLoginUsingResetEmailCodeV0,
2935
+ ):
2936
+ reset_email_code = body.reset_email_code
2937
+ username = body.username
2938
+ new_password = body.new_password
2939
+ app_id = body.app_id
2940
+ logout_other_sessions = body.logout_other_sessions
2941
+ try:
2942
+ """
2943
+ validation
2944
+ """
2945
+ # validate username
2946
+ local_list_authentication_user_response = (
2947
+ global_object_square_database_helper.get_rows_v0(
2948
+ database_name=global_string_database_name,
2949
+ schema_name=global_string_schema_name,
2950
+ table_name=User.__tablename__,
2951
+ filters=FiltersV0(
2952
+ root={User.user_username.name: FilterConditionsV0(eq=username)}
2953
+ ),
2954
+ )["data"]["main"]
2955
+ )
2956
+ if len(local_list_authentication_user_response) != 1:
2957
+ output_content = get_api_output_in_standard_format(
2958
+ message=messages["INCORRECT_USERNAME"],
2959
+ log=f"incorrect username: {username}.",
2960
+ )
2961
+ raise HTTPException(
2962
+ status_code=status.HTTP_400_BAD_REQUEST,
2963
+ detail=output_content,
2964
+ )
2965
+ user_id = local_list_authentication_user_response[0][User.user_id.name]
2966
+ # check if user has SELF auth provider
2967
+ local_list_response_user_auth_provider = (
2968
+ global_object_square_database_helper.get_rows_v0(
2969
+ database_name=global_string_database_name,
2970
+ schema_name=global_string_schema_name,
2971
+ table_name=UserAuthProvider.__tablename__,
2972
+ filters=FiltersV0(
2973
+ root={
2974
+ UserAuthProvider.user_id.name: FilterConditionsV0(eq=user_id),
2975
+ UserAuthProvider.auth_provider.name: FilterConditionsV0(
2976
+ eq=AuthProviderEnum.SELF.value
2977
+ ),
2978
+ }
2979
+ ),
2980
+ )["data"]["main"]
2981
+ )
2982
+ if len(local_list_response_user_auth_provider) != 1:
2983
+ output_content = get_api_output_in_standard_format(
2984
+ message=messages["INCORRECT_AUTH_PROVIDER"],
2985
+ log=f"user_id: {user_id} does not have {AuthProviderEnum.SELF.value} auth provider.",
2986
+ )
2987
+ raise HTTPException(
2988
+ status_code=status.HTTP_400_BAD_REQUEST,
2989
+ detail=output_content,
2990
+ )
2991
+ # check if user has recovery method enabled
2992
+ local_list_response_user_recovery_methods = global_object_square_database_helper.get_rows_v0(
2993
+ database_name=global_string_database_name,
2994
+ schema_name=global_string_schema_name,
2995
+ table_name=UserRecoveryMethod.__tablename__,
2996
+ filters=FiltersV0(
2997
+ root={
2998
+ UserRecoveryMethod.user_id.name: FilterConditionsV0(eq=user_id),
2999
+ UserRecoveryMethod.user_recovery_method_name.name: FilterConditionsV0(
3000
+ eq=RecoveryMethodEnum.EMAIL.value
3001
+ ),
3002
+ }
3003
+ ),
3004
+ )[
3005
+ "data"
3006
+ ][
3007
+ "main"
3008
+ ]
3009
+ if len(local_list_response_user_recovery_methods) != 1:
3010
+ output_content = get_api_output_in_standard_format(
3011
+ message=messages["RECOVERY_METHOD_NOT_ENABLED"],
3012
+ log=f"user_id: {user_id} does not have email recovery method enabled.",
3013
+ )
3014
+ raise HTTPException(
3015
+ status_code=status.HTTP_400_BAD_REQUEST,
3016
+ detail=output_content,
3017
+ )
3018
+ # check if user has email in profile
3019
+ user_profile_response = global_object_square_database_helper.get_rows_v0(
3020
+ database_name=global_string_database_name,
3021
+ schema_name=global_string_schema_name,
3022
+ table_name=UserProfile.__tablename__,
3023
+ filters=FiltersV0(
3024
+ root={UserProfile.user_id.name: FilterConditionsV0(eq=user_id)}
3025
+ ),
3026
+ apply_filters=True,
3027
+ )
3028
+ user_profile_data = user_profile_response["data"]["main"][0]
3029
+ if not user_profile_data.get(UserProfile.user_profile_email.name):
3030
+ output_content = get_api_output_in_standard_format(
3031
+ message=messages["GENERIC_MISSING_REQUIRED_FIELD"],
3032
+ log="user seems to have email recovery method enabled, but does not have email in profile.",
3033
+ )
3034
+ raise HTTPException(
3035
+ status_code=status.HTTP_400_BAD_REQUEST,
3036
+ detail=output_content,
3037
+ )
3038
+ # check if email is verified.
3039
+ if not user_profile_data.get(UserProfile.user_profile_email_verified.name):
3040
+ output_content = get_api_output_in_standard_format(
3041
+ message=messages["EMAIL_NOT_VERIFIED"],
3042
+ log="email is not verified.",
3043
+ )
3044
+ return JSONResponse(status_code=status.HTTP_200_OK, content=output_content)
3045
+ # validate if user is assigned to the app.
3046
+ # not checking [skipping] if the app exists, as it is not required for this endpoint.
3047
+ local_list_response_user_app = global_object_square_database_helper.get_rows_v0(
3048
+ database_name=global_string_database_name,
3049
+ schema_name=global_string_schema_name,
3050
+ table_name=UserApp.__tablename__,
3051
+ filters=FiltersV0(
3052
+ root={
3053
+ UserApp.user_id.name: FilterConditionsV0(eq=user_id),
3054
+ UserApp.app_id.name: FilterConditionsV0(eq=app_id),
3055
+ }
3056
+ ),
3057
+ )["data"]["main"]
3058
+ if len(local_list_response_user_app) == 0:
3059
+ output_content = get_api_output_in_standard_format(
3060
+ message=messages["GENERIC_400"],
3061
+ log=f"user_id: {user_id} is not assigned to app_id: {app_id}.",
3062
+ )
3063
+ raise HTTPException(
3064
+ status_code=status.HTTP_400_BAD_REQUEST,
3065
+ detail=output_content,
3066
+ )
3067
+ """
3068
+ main process
3069
+ """
3070
+ # validate email reset code
3071
+ local_list_response_user_verification_code = global_object_square_database_helper.get_rows_v0(
3072
+ database_name=global_string_database_name,
3073
+ schema_name=global_string_schema_name,
3074
+ table_name=UserVerificationCode.__tablename__,
3075
+ filters=FiltersV0(
3076
+ root={
3077
+ UserVerificationCode.user_id.name: FilterConditionsV0(eq=user_id),
3078
+ UserVerificationCode.user_verification_code_type.name: FilterConditionsV0(
3079
+ eq=VerificationCodeTypeEnum.EMAIL_RECOVERY.value
3080
+ ),
3081
+ UserVerificationCode.user_verification_code_expires_at.name: FilterConditionsV0(
3082
+ gte=datetime.now(timezone.utc).strftime(
3083
+ "%Y-%m-%d %H:%M:%S.%f+00"
3084
+ )
3085
+ ),
3086
+ UserVerificationCode.user_verification_code_used_at.name: FilterConditionsV0(
3087
+ is_null=True
3088
+ ),
3089
+ }
3090
+ ),
3091
+ columns=[UserVerificationCode.user_verification_code_hash.name],
3092
+ order_by=[
3093
+ "-" + UserVerificationCode.user_verification_code_created_at.name
3094
+ ],
3095
+ limit=1,
3096
+ )[
3097
+ "data"
3098
+ ][
3099
+ "main"
3100
+ ]
3101
+ if len(local_list_response_user_verification_code) != 1:
3102
+ output_content = get_api_output_in_standard_format(
3103
+ message=messages["INCORRECT_VERIFICATION_CODE"],
3104
+ log=f"incorrect reset_email_code: {reset_email_code} for user_id: {user_id}.",
3105
+ )
3106
+ raise HTTPException(
3107
+ status_code=status.HTTP_400_BAD_REQUEST,
3108
+ detail=output_content,
3109
+ )
3110
+ latest_hashed_verification_code = local_list_response_user_verification_code[0][
3111
+ UserVerificationCode.user_verification_code_hash.name
3112
+ ]
3113
+
3114
+ if not bcrypt.checkpw(
3115
+ reset_email_code.encode("utf-8"),
3116
+ latest_hashed_verification_code.encode("utf-8"),
3117
+ ):
3118
+ output_content = get_api_output_in_standard_format(
3119
+ message=messages["INCORRECT_VERIFICATION_CODE"],
3120
+ log=f"incorrect reset_email_code: {reset_email_code} for user_id: {user_id}.",
3121
+ )
3122
+ raise HTTPException(
3123
+ status_code=status.HTTP_400_BAD_REQUEST,
3124
+ detail=output_content,
3125
+ )
3126
+
3127
+ # hash the new password
3128
+ local_str_hashed_password = bcrypt.hashpw(
3129
+ new_password.encode("utf-8"), bcrypt.gensalt()
3130
+ ).decode("utf-8")
3131
+ # update the password
3132
+ global_object_square_database_helper.edit_rows_v0(
3133
+ database_name=global_string_database_name,
3134
+ schema_name=global_string_schema_name,
3135
+ table_name=UserCredential.__tablename__,
3136
+ filters=FiltersV0(
3137
+ root={
3138
+ UserCredential.user_id.name: FilterConditionsV0(eq=user_id),
3139
+ }
3140
+ ),
3141
+ data={
3142
+ UserCredential.user_credential_hashed_password.name: local_str_hashed_password,
3143
+ },
3144
+ )
3145
+ # mark the email code as used
3146
+ global_object_square_database_helper.edit_rows_v0(
3147
+ database_name=global_string_database_name,
3148
+ schema_name=global_string_schema_name,
3149
+ table_name=UserVerificationCode.__tablename__,
3150
+ filters=FiltersV0(
3151
+ root={
3152
+ UserVerificationCode.user_id.name: FilterConditionsV0(eq=user_id),
3153
+ UserVerificationCode.user_verification_code_type.name: FilterConditionsV0(
3154
+ eq=VerificationCodeTypeEnum.EMAIL_RECOVERY.value
3155
+ ),
3156
+ UserVerificationCode.user_verification_code_hash.name: FilterConditionsV0(
3157
+ eq=latest_hashed_verification_code
3158
+ ),
3159
+ }
3160
+ ),
3161
+ data={
3162
+ UserVerificationCode.user_verification_code_used_at.name: datetime.now(
3163
+ timezone.utc
3164
+ ).strftime("%Y-%m-%d %H:%M:%S.%f+00"),
3165
+ },
3166
+ )
3167
+ if logout_other_sessions:
3168
+ # delete all sessions for user
3169
+ global_object_square_database_helper.delete_rows_v0(
3170
+ database_name=global_string_database_name,
3171
+ schema_name=global_string_schema_name,
3172
+ table_name=UserSession.__tablename__,
3173
+ filters=FiltersV0(
3174
+ root={
3175
+ UserSession.user_id.name: FilterConditionsV0(eq=user_id),
3176
+ }
3177
+ ),
3178
+ )
3179
+ # generate access token and refresh token
3180
+ local_dict_access_token_payload = {
3181
+ "app_id": app_id,
3182
+ "user_id": user_id,
3183
+ "exp": datetime.now(timezone.utc)
3184
+ + timedelta(minutes=config_int_access_token_valid_minutes),
3185
+ }
3186
+ local_str_access_token = jwt.encode(
3187
+ local_dict_access_token_payload, config_str_secret_key_for_access_token
3188
+ )
3189
+ local_object_refresh_token_expiry_time = datetime.now(timezone.utc) + timedelta(
3190
+ minutes=config_int_refresh_token_valid_minutes
3191
+ )
3192
+ local_dict_refresh_token_payload = {
3193
+ "app_id": app_id,
3194
+ "user_id": user_id,
3195
+ "exp": local_object_refresh_token_expiry_time,
3196
+ }
3197
+ local_str_refresh_token = jwt.encode(
3198
+ local_dict_refresh_token_payload, config_str_secret_key_for_refresh_token
3199
+ )
3200
+ # insert the refresh token in the database
3201
+ global_object_square_database_helper.insert_rows_v0(
3202
+ database_name=global_string_database_name,
3203
+ schema_name=global_string_schema_name,
3204
+ table_name=UserSession.__tablename__,
3205
+ data=[
3206
+ {
3207
+ UserSession.user_id.name: user_id,
3208
+ UserSession.app_id.name: app_id,
3209
+ UserSession.user_session_refresh_token.name: local_str_refresh_token,
3210
+ UserSession.user_session_expiry_time.name: local_object_refresh_token_expiry_time.strftime(
3211
+ "%Y-%m-%d %H:%M:%S.%f+00"
3212
+ ),
3213
+ }
3214
+ ],
3215
+ )
3216
+ """
3217
+ return value
3218
+ """
3219
+ output_content = get_api_output_in_standard_format(
3220
+ message=messages["GENERIC_ACTION_SUCCESSFUL"],
3221
+ data={
3222
+ "main": {
3223
+ "user_id": user_id,
3224
+ "access_token": local_str_access_token,
3225
+ "refresh_token": local_str_refresh_token,
3226
+ }
3227
+ },
3228
+ )
3229
+
3230
+ return JSONResponse(status_code=status.HTTP_200_OK, content=output_content)
3231
+ except HTTPException as http_exception:
3232
+ global_object_square_logger.logger.error(http_exception, exc_info=True)
3233
+ return JSONResponse(
3234
+ status_code=http_exception.status_code, content=http_exception.detail
3235
+ )
3236
+ except Exception as e:
3237
+ """
3238
+ rollback logic
3239
+ """
3240
+ global_object_square_logger.logger.error(e, exc_info=True)
3241
+ output_content = get_api_output_in_standard_format(
3242
+ message=messages["GENERIC_500"],
3243
+ log=str(e),
3244
+ )
3245
+ return JSONResponse(
3246
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=output_content
3247
+ )