square-authentication 6.0.5__tar.gz → 6.2.0__tar.gz

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.
Files changed (26) hide show
  1. {square_authentication-6.0.5 → square_authentication-6.2.0}/PKG-INFO +11 -1
  2. {square_authentication-6.0.5 → square_authentication-6.2.0}/README.md +10 -0
  3. {square_authentication-6.0.5 → square_authentication-6.2.0}/setup.py +1 -1
  4. {square_authentication-6.0.5 → square_authentication-6.2.0}/square_authentication/messages.py +1 -0
  5. {square_authentication-6.0.5 → square_authentication-6.2.0}/square_authentication/routes/core.py +184 -0
  6. {square_authentication-6.0.5 → square_authentication-6.2.0}/square_authentication.egg-info/PKG-INFO +11 -1
  7. {square_authentication-6.0.5 → square_authentication-6.2.0}/square_authentication.egg-info/SOURCES.txt +2 -1
  8. square_authentication-6.2.0/tests/test_username.py +111 -0
  9. {square_authentication-6.0.5 → square_authentication-6.2.0}/setup.cfg +0 -0
  10. {square_authentication-6.0.5 → square_authentication-6.2.0}/square_authentication/__init__.py +0 -0
  11. {square_authentication-6.0.5 → square_authentication-6.2.0}/square_authentication/configuration.py +0 -0
  12. {square_authentication-6.0.5 → square_authentication-6.2.0}/square_authentication/data/config.ini +0 -0
  13. {square_authentication-6.0.5 → square_authentication-6.2.0}/square_authentication/data/config.testing.ini +0 -0
  14. {square_authentication-6.0.5 → square_authentication-6.2.0}/square_authentication/main.py +0 -0
  15. {square_authentication-6.0.5 → square_authentication-6.2.0}/square_authentication/pydantic_models/__init__.py +0 -0
  16. {square_authentication-6.0.5 → square_authentication-6.2.0}/square_authentication/pydantic_models/core.py +0 -0
  17. {square_authentication-6.0.5 → square_authentication-6.2.0}/square_authentication/routes/__init__.py +0 -0
  18. {square_authentication-6.0.5 → square_authentication-6.2.0}/square_authentication/routes/profile.py +0 -0
  19. {square_authentication-6.0.5 → square_authentication-6.2.0}/square_authentication/routes/utility.py +0 -0
  20. {square_authentication-6.0.5 → square_authentication-6.2.0}/square_authentication/utils/__init__.py +0 -0
  21. {square_authentication-6.0.5 → square_authentication-6.2.0}/square_authentication/utils/encryption.py +0 -0
  22. {square_authentication-6.0.5 → square_authentication-6.2.0}/square_authentication/utils/token.py +0 -0
  23. {square_authentication-6.0.5 → square_authentication-6.2.0}/square_authentication.egg-info/dependency_links.txt +0 -0
  24. {square_authentication-6.0.5 → square_authentication-6.2.0}/square_authentication.egg-info/requires.txt +0 -0
  25. {square_authentication-6.0.5 → square_authentication-6.2.0}/square_authentication.egg-info/top_level.txt +0 -0
  26. {square_authentication-6.0.5 → square_authentication-6.2.0}/tests/test_1.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: square_authentication
3
- Version: 6.0.5
3
+ Version: 6.2.0
4
4
  Summary: authentication layer for my personal server.
5
5
  Home-page: https://github.com/thepmsquare/square_authentication
6
6
  Author: thePmSquare
@@ -53,6 +53,16 @@ pip install square_authentication
53
53
 
54
54
  ## changelog
55
55
 
56
+ ### v6.2.0
57
+
58
+ - core
59
+ - add update_user_recovery_methods_v0.
60
+
61
+ ### v6.1.0
62
+
63
+ - add validation to username in register_username_v0 and update_username_v0.
64
+ - add test cases for register_username_v0.
65
+
56
66
  ### v6.0.5
57
67
 
58
68
  - env
@@ -16,6 +16,16 @@ pip install square_authentication
16
16
 
17
17
  ## changelog
18
18
 
19
+ ### v6.2.0
20
+
21
+ - core
22
+ - add update_user_recovery_methods_v0.
23
+
24
+ ### v6.1.0
25
+
26
+ - add validation to username in register_username_v0 and update_username_v0.
27
+ - add test cases for register_username_v0.
28
+
19
29
  ### v6.0.5
20
30
 
21
31
  - env
@@ -4,7 +4,7 @@ package_name = "square_authentication"
4
4
 
5
5
  setup(
6
6
  name=package_name,
7
- version="6.0.5",
7
+ version="6.2.0",
8
8
  packages=find_packages(),
9
9
  package_data={
10
10
  package_name: ["data/*"],
@@ -5,6 +5,7 @@ messages = {
5
5
  "INCORRECT_USERNAME": "the username you entered does not exist.",
6
6
  "INCORRECT_PASSWORD": "the password you entered is incorrect. please try again.",
7
7
  "INCORRECT_USER_ID": "the user ID you provided does not exist or is invalid.",
8
+ "USERNAME_INVALID": "username must start and end with a lowercase letter and can include only lowercase letters, digits, underscores, or hyphens. no spaces, no dots, and no consecutive special characters.",
8
9
  "USERNAME_ALREADY_EXISTS": "the username you entered is already taken. please choose a different one.",
9
10
  "INCORRECT_ACCESS_TOKEN": "the access token provided is invalid or expired.",
10
11
  "INCORRECT_REFRESH_TOKEN": "the refresh token provided is invalid or expired.",
@@ -1,4 +1,5 @@
1
1
  import copy
2
+ import re
2
3
  from datetime import datetime, timedelta, timezone
3
4
  from typing import Annotated, List
4
5
 
@@ -12,12 +13,14 @@ from square_commons import get_api_output_in_standard_format
12
13
  from square_database_helper.pydantic_models import FilterConditionsV0, FiltersV0
13
14
  from square_database_structure.square import global_string_database_name
14
15
  from square_database_structure.square.authentication import global_string_schema_name
16
+ from square_database_structure.square.authentication.enums import RecoveryMethodEnum
15
17
  from square_database_structure.square.authentication.tables import (
16
18
  User,
17
19
  UserApp,
18
20
  UserCredential,
19
21
  UserSession,
20
22
  UserProfile,
23
+ UserRecoveryMethod,
21
24
  )
22
25
  from square_database_structure.square.public import (
23
26
  global_string_schema_name as global_string_public_schema_name,
@@ -67,6 +70,22 @@ async def register_username_v0(
67
70
  validation
68
71
  """
69
72
  # validation for username
73
+ # ^(?!.*[_-]{2}) # no consecutive _ or -
74
+ # [a-z] # must start with a lowercase letter
75
+ # (?:[a-z0-9_-]{1,18}) # 1–18 of lowercase, digits, _ or -
76
+ # [a-z]$ # must end with a lowercase letter
77
+ username_pattern = re.compile(r"^(?!.*[._-]{2})[a-z][a-z0-9_-]{1,18}[a-z]$")
78
+ if not username_pattern.match(username):
79
+ output_content = get_api_output_in_standard_format(
80
+ message=messages["USERNAME_INVALID"],
81
+ log=f"username '{username}' is invalid. it must start and end with a letter, "
82
+ f"contain only lowercase letters, numbers, underscores, or hyphens, "
83
+ f"and not have consecutive separators.",
84
+ )
85
+ raise HTTPException(
86
+ status_code=status.HTTP_400_BAD_REQUEST,
87
+ detail=output_content,
88
+ )
70
89
  local_list_response_user_creds = (
71
90
  global_object_square_database_helper.get_rows_v0(
72
91
  database_name=global_string_database_name,
@@ -1098,6 +1117,25 @@ async def update_username_v0(
1098
1117
  )
1099
1118
  user_id = local_dict_access_token_payload["user_id"]
1100
1119
 
1120
+ # validation for username
1121
+ # ^(?!.*[_-]{2}) # no consecutive _ or -
1122
+ # [a-z] # must start with a lowercase letter
1123
+ # (?:[a-z0-9_-]{1,18}) # 1–18 of lowercase, digits, _ or -
1124
+ # [a-z]$ # must end with a lowercase letter
1125
+ new_username = new_username.lower()
1126
+ username_pattern = re.compile(r"^(?!.*[._-]{2})[a-z][a-z0-9_-]{1,18}[a-z]$")
1127
+ if not username_pattern.match(new_username):
1128
+ output_content = get_api_output_in_standard_format(
1129
+ message=messages["USERNAME_INVALID"],
1130
+ log=f"username '{new_username}' is invalid. it must start and end with a letter, "
1131
+ f"contain only lowercase letters, numbers, underscores, or hyphens, "
1132
+ f"and not have consecutive separators.",
1133
+ )
1134
+ raise HTTPException(
1135
+ status_code=status.HTTP_400_BAD_REQUEST,
1136
+ detail=output_content,
1137
+ )
1138
+
1101
1139
  # validate user_id
1102
1140
  local_list_user_response = global_object_square_database_helper.get_rows_v0(
1103
1141
  database_name=global_string_database_name,
@@ -1504,3 +1542,149 @@ async def validate_and_get_payload_from_token_v0(
1504
1542
  return JSONResponse(
1505
1543
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=output_content
1506
1544
  )
1545
+
1546
+
1547
+ @router.patch("/update_user_recovery_methods/v0")
1548
+ @global_object_square_logger.auto_logger()
1549
+ async def update_user_recovery_methods_v0(
1550
+ access_token: Annotated[str, Header()],
1551
+ recovery_methods_to_add: List[RecoveryMethodEnum],
1552
+ recovery_methods_to_remove: List[RecoveryMethodEnum],
1553
+ ):
1554
+ try:
1555
+
1556
+ """
1557
+ validation
1558
+ """
1559
+ # validate access token
1560
+ try:
1561
+ local_dict_access_token_payload = get_jwt_payload(
1562
+ access_token, config_str_secret_key_for_access_token
1563
+ )
1564
+ except Exception as error:
1565
+ output_content = get_api_output_in_standard_format(
1566
+ message=messages["INCORRECT_ACCESS_TOKEN"], log=str(error)
1567
+ )
1568
+ raise HTTPException(
1569
+ status_code=status.HTTP_400_BAD_REQUEST,
1570
+ detail=output_content,
1571
+ )
1572
+ user_id = local_dict_access_token_payload["user_id"]
1573
+
1574
+ recovery_methods_to_add = list(set(x.value for x in recovery_methods_to_add))
1575
+ recovery_methods_to_remove = list(
1576
+ set(x.value for x in recovery_methods_to_remove)
1577
+ )
1578
+
1579
+ # check if recovery_methods_to_add and recovery_methods_to_remove don't have common ids.
1580
+ local_list_common_recovery_methods = set(recovery_methods_to_add) & set(
1581
+ recovery_methods_to_remove
1582
+ )
1583
+ if len(local_list_common_recovery_methods) > 0:
1584
+ output_content = get_api_output_in_standard_format(
1585
+ message=messages["GENERIC_400"],
1586
+ log=f"invalid recovery_methods: {list(local_list_common_recovery_methods)}, present in both add list and remove list.",
1587
+ )
1588
+ raise HTTPException(
1589
+ status_code=status.HTTP_400_BAD_REQUEST,
1590
+ detail=output_content,
1591
+ )
1592
+
1593
+ """
1594
+ main process
1595
+ """
1596
+ # logic for adding new recovery_methods
1597
+ local_list_response_user_recovery_methods = (
1598
+ global_object_square_database_helper.get_rows_v0(
1599
+ database_name=global_string_database_name,
1600
+ schema_name=global_string_schema_name,
1601
+ table_name=UserRecoveryMethod.__tablename__,
1602
+ filters=FiltersV0(
1603
+ root={
1604
+ UserRecoveryMethod.user_id.name: FilterConditionsV0(eq=user_id)
1605
+ }
1606
+ ),
1607
+ )["data"]["main"]
1608
+ )
1609
+ local_list_new_recovery_methods = [
1610
+ {
1611
+ UserRecoveryMethod.user_id.name: user_id,
1612
+ UserRecoveryMethod.user_recovery_method_name.name: x,
1613
+ }
1614
+ for x in recovery_methods_to_add
1615
+ if x
1616
+ not in [
1617
+ y[UserRecoveryMethod.user_recovery_method_name.name]
1618
+ for y in local_list_response_user_recovery_methods
1619
+ ]
1620
+ ]
1621
+ if len(local_list_new_recovery_methods) > 0:
1622
+ global_object_square_database_helper.insert_rows_v0(
1623
+ database_name=global_string_database_name,
1624
+ schema_name=global_string_schema_name,
1625
+ table_name=UserRecoveryMethod.__tablename__,
1626
+ data=local_list_new_recovery_methods,
1627
+ )
1628
+
1629
+ # logic for removing recovery_methods
1630
+ global_object_square_database_helper.delete_rows_v0(
1631
+ database_name=global_string_database_name,
1632
+ schema_name=global_string_schema_name,
1633
+ table_name=UserRecoveryMethod.__tablename__,
1634
+ filters=FiltersV0(
1635
+ root={
1636
+ UserRecoveryMethod.user_id.name: FilterConditionsV0(eq=user_id),
1637
+ UserRecoveryMethod.user_recovery_method_name.name: FilterConditionsV0(
1638
+ in_=recovery_methods_to_remove
1639
+ ),
1640
+ }
1641
+ ),
1642
+ )
1643
+
1644
+ """
1645
+ return value
1646
+ """
1647
+ # get latest recovery_methods
1648
+ local_list_response_user_recovery_methods = (
1649
+ global_object_square_database_helper.get_rows_v0(
1650
+ database_name=global_string_database_name,
1651
+ schema_name=global_string_schema_name,
1652
+ table_name=UserRecoveryMethod.__tablename__,
1653
+ filters=FiltersV0(
1654
+ root={
1655
+ UserRecoveryMethod.user_id.name: FilterConditionsV0(eq=user_id)
1656
+ }
1657
+ ),
1658
+ )["data"]["main"]
1659
+ )
1660
+ output_content = get_api_output_in_standard_format(
1661
+ message=messages["GENERIC_UPDATE_SUCCESSFUL"],
1662
+ data={
1663
+ "main": [
1664
+ x[UserRecoveryMethod.user_recovery_method_name.name]
1665
+ for x in local_list_response_user_recovery_methods
1666
+ ]
1667
+ },
1668
+ )
1669
+ return JSONResponse(
1670
+ status_code=status.HTTP_200_OK,
1671
+ content=output_content,
1672
+ )
1673
+ except HTTPException as http_exception:
1674
+ global_object_square_logger.logger.error(http_exception, exc_info=True)
1675
+ return JSONResponse(
1676
+ status_code=http_exception.status_code, content=http_exception.detail
1677
+ )
1678
+ except Exception as e:
1679
+ """
1680
+ rollback logic
1681
+ """
1682
+ global_object_square_logger.logger.error(e, exc_info=True)
1683
+ output_content = get_api_output_in_standard_format(
1684
+ message=messages["GENERIC_500"],
1685
+ log=str(e),
1686
+ )
1687
+ return JSONResponse(
1688
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1689
+ content=output_content,
1690
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: square_authentication
3
- Version: 6.0.5
3
+ Version: 6.2.0
4
4
  Summary: authentication layer for my personal server.
5
5
  Home-page: https://github.com/thepmsquare/square_authentication
6
6
  Author: thePmSquare
@@ -53,6 +53,16 @@ pip install square_authentication
53
53
 
54
54
  ## changelog
55
55
 
56
+ ### v6.2.0
57
+
58
+ - core
59
+ - add update_user_recovery_methods_v0.
60
+
61
+ ### v6.1.0
62
+
63
+ - add validation to username in register_username_v0 and update_username_v0.
64
+ - add test cases for register_username_v0.
65
+
56
66
  ### v6.0.5
57
67
 
58
68
  - env
@@ -20,4 +20,5 @@ square_authentication/routes/utility.py
20
20
  square_authentication/utils/__init__.py
21
21
  square_authentication/utils/encryption.py
22
22
  square_authentication/utils/token.py
23
- tests/test_1.py
23
+ tests/test_1.py
24
+ tests/test_username.py
@@ -0,0 +1,111 @@
1
+ from square_authentication.messages import messages
2
+
3
+
4
+ def test_register_username_invalid(create_client_and_cleanup):
5
+ payload = {
6
+ "username": "invalid..name",
7
+ "password": "testpass123",
8
+ "app_id": 1,
9
+ }
10
+ response = create_client_and_cleanup.post("/register_username/v0", json=payload)
11
+ assert response.status_code == 400
12
+ assert "username" in response.json()["log"]
13
+
14
+
15
+ def test_username_starts_with_number(create_client_and_cleanup):
16
+ payload = {
17
+ "username": "1username",
18
+ "password": "testpass123",
19
+ "app_id": 1,
20
+ }
21
+ response = create_client_and_cleanup.post("/register_username/v0", json=payload)
22
+ assert response.status_code == 400
23
+ assert "username" in response.json()["log"]
24
+
25
+
26
+ def test_username_ends_with_number(create_client_and_cleanup):
27
+ payload = {
28
+ "username": "username1",
29
+ "password": "testpass123",
30
+ "app_id": 1,
31
+ }
32
+ response = create_client_and_cleanup.post("/register_username/v0", json=payload)
33
+ assert response.status_code == 400
34
+ assert "username" in response.json()["log"]
35
+
36
+
37
+ def test_username_with_space(create_client_and_cleanup):
38
+ payload = {
39
+ "username": "user name",
40
+ "password": "testpass123",
41
+ "app_id": 1,
42
+ }
43
+ response = create_client_and_cleanup.post("/register_username/v0", json=payload)
44
+ assert response.status_code == 400
45
+ assert "username" in response.json()["log"]
46
+
47
+
48
+ def test_username_too_short(create_client_and_cleanup):
49
+ payload = {
50
+ "username": "a",
51
+ "password": "testpass123",
52
+ "app_id": 1,
53
+ }
54
+ response = create_client_and_cleanup.post("/register_username/v0", json=payload)
55
+ assert response.status_code == 400
56
+ assert "username" in response.json()["log"]
57
+
58
+
59
+ def test_username_simple_valid(create_client_and_cleanup):
60
+ payload = {
61
+ "username": "johnsmith",
62
+ "password": "testpass123",
63
+ "app_id": 1,
64
+ }
65
+ response = create_client_and_cleanup.post("/register_username/v0", json=payload)
66
+ assert response.status_code == 201
67
+ assert response.json()["message"] == messages["REGISTRATION_SUCCESSFUL"]
68
+
69
+
70
+ def test_username_with_uppercase(create_client_and_cleanup):
71
+ payload = {
72
+ "username": "User_Name",
73
+ "password": "testpass123",
74
+ "app_id": 1,
75
+ }
76
+ response = create_client_and_cleanup.post("/register_username/v0", json=payload)
77
+ assert response.status_code == 201
78
+ assert response.json()["message"] == messages["REGISTRATION_SUCCESSFUL"]
79
+
80
+
81
+ def test_username_with_digits(create_client_and_cleanup):
82
+ payload = {
83
+ "username": "john123smith",
84
+ "password": "testpass123",
85
+ "app_id": 1,
86
+ }
87
+ response = create_client_and_cleanup.post("/register_username/v0", json=payload)
88
+ assert response.status_code == 201
89
+ assert response.json()["message"] == messages["REGISTRATION_SUCCESSFUL"]
90
+
91
+
92
+ def test_username_with_underscore_hyphen(create_client_and_cleanup):
93
+ payload = {
94
+ "username": "john_smith-doe",
95
+ "password": "testpass123",
96
+ "app_id": 1,
97
+ }
98
+ response = create_client_and_cleanup.post("/register_username/v0", json=payload)
99
+ assert response.status_code == 201
100
+ assert response.json()["message"] == messages["REGISTRATION_SUCCESSFUL"]
101
+
102
+
103
+ def test_username_max_length(create_client_and_cleanup):
104
+ payload = {
105
+ "username": "a1234567890_b-cdefgh",
106
+ "password": "testpass123",
107
+ "app_id": 1,
108
+ }
109
+ response = create_client_and_cleanup.post("/register_username/v0", json=payload)
110
+ assert response.status_code == 201
111
+ assert response.json()["message"] == messages["REGISTRATION_SUCCESSFUL"]