shaapi 0.1.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.
Files changed (141) hide show
  1. shaapi/__init__.py +3 -0
  2. shaapi/cli.py +97 -0
  3. shaapi/generator.py +114 -0
  4. shaapi/template/.dockerignore +37 -0
  5. shaapi/template/.env.template +59 -0
  6. shaapi/template/.gitattributes +12 -0
  7. shaapi/template/.gitignore +170 -0
  8. shaapi/template/.gitlab-ci.yml +89 -0
  9. shaapi/template/Dockerfile +59 -0
  10. shaapi/template/LICENSE +21 -0
  11. shaapi/template/README.md +206 -0
  12. shaapi/template/backend/.gitignore +164 -0
  13. shaapi/template/backend/__init__.py +0 -0
  14. shaapi/template/backend/alembic/README +1 -0
  15. shaapi/template/backend/alembic/env.py +102 -0
  16. shaapi/template/backend/alembic/script.py.mako +26 -0
  17. shaapi/template/backend/alembic/versions/2026_06_08_1024-64524c63b666_initial.py +143 -0
  18. shaapi/template/backend/alembic.ini +117 -0
  19. shaapi/template/backend/app/__init__.py +55 -0
  20. shaapi/template/backend/app/admin/__init__.py +1 -0
  21. shaapi/template/backend/app/admin/api/v1/__init__.py +0 -0
  22. shaapi/template/backend/app/admin/api/v1/auth.py +59 -0
  23. shaapi/template/backend/app/admin/api/v1/casbin.py +218 -0
  24. shaapi/template/backend/app/admin/api/v1/login_log.py +63 -0
  25. shaapi/template/backend/app/admin/api/v1/opera_log.py +61 -0
  26. shaapi/template/backend/app/admin/api/v1/role.py +108 -0
  27. shaapi/template/backend/app/admin/api/v1/user.py +47 -0
  28. shaapi/template/backend/app/admin/schema/casbin_rule.py +45 -0
  29. shaapi/template/backend/app/admin/schema/login_log.py +36 -0
  30. shaapi/template/backend/app/admin/schema/opera_log.py +43 -0
  31. shaapi/template/backend/app/admin/schema/role.py +36 -0
  32. shaapi/template/backend/app/admin/schema/sso.py +37 -0
  33. shaapi/template/backend/app/admin/schema/token.py +74 -0
  34. shaapi/template/backend/app/admin/schema/user.py +93 -0
  35. shaapi/template/backend/app/admin/service/auth_service.py +233 -0
  36. shaapi/template/backend/app/admin/service/casbin_service.py +135 -0
  37. shaapi/template/backend/app/admin/service/login_log_service.py +62 -0
  38. shaapi/template/backend/app/admin/service/opera_log_service.py +31 -0
  39. shaapi/template/backend/app/admin/service/role_service.py +79 -0
  40. shaapi/template/backend/app/admin/service/secure_token_service.py +60 -0
  41. shaapi/template/backend/app/admin/service/user_service.py +153 -0
  42. shaapi/template/backend/app/api.py +11 -0
  43. shaapi/template/backend/common/__init__.py +0 -0
  44. shaapi/template/backend/common/cloud_storage/__init__.py +11 -0
  45. shaapi/template/backend/common/cloud_storage/cloud_storage.py +180 -0
  46. shaapi/template/backend/common/dataclasses.py +52 -0
  47. shaapi/template/backend/common/email_conf/email.py +105 -0
  48. shaapi/template/backend/common/enums.py +144 -0
  49. shaapi/template/backend/common/exception/__init__.py +0 -0
  50. shaapi/template/backend/common/exception/errors.py +87 -0
  51. shaapi/template/backend/common/exception/exception_handler.py +280 -0
  52. shaapi/template/backend/common/log.py +123 -0
  53. shaapi/template/backend/common/model.py +68 -0
  54. shaapi/template/backend/common/pagination.py +83 -0
  55. shaapi/template/backend/common/response/__init__.py +0 -0
  56. shaapi/template/backend/common/response/response_code.py +158 -0
  57. shaapi/template/backend/common/response/response_schema.py +110 -0
  58. shaapi/template/backend/common/schema.py +144 -0
  59. shaapi/template/backend/common/security/jwt.py +203 -0
  60. shaapi/template/backend/common/security/rbac.py +98 -0
  61. shaapi/template/backend/common/security/sec_token.py +6 -0
  62. shaapi/template/backend/common/socketio/action.py +11 -0
  63. shaapi/template/backend/common/socketio/server.py +50 -0
  64. shaapi/template/backend/common/sso/base.py +69 -0
  65. shaapi/template/backend/common/sso/google.py +127 -0
  66. shaapi/template/backend/core/conf.py +208 -0
  67. shaapi/template/backend/core/path_conf.py +24 -0
  68. shaapi/template/backend/core/registrar.py +195 -0
  69. shaapi/template/backend/crud/__init__.py +1 -0
  70. shaapi/template/backend/crud/crud_base.py +35 -0
  71. shaapi/template/backend/crud/crud_casbin.py +46 -0
  72. shaapi/template/backend/crud/crud_login_log.py +58 -0
  73. shaapi/template/backend/crud/crud_opera_log.py +58 -0
  74. shaapi/template/backend/crud/crud_role.py +128 -0
  75. shaapi/template/backend/crud/crud_user.py +267 -0
  76. shaapi/template/backend/database/__init__.py +0 -0
  77. shaapi/template/backend/database/db_postgres.py +125 -0
  78. shaapi/template/backend/database/db_redis.py +62 -0
  79. shaapi/template/backend/entrypoint-api.sh +19 -0
  80. shaapi/template/backend/lang/en/app.py +18 -0
  81. shaapi/template/backend/lang/en/auth.py +10 -0
  82. shaapi/template/backend/lang/fr/app.py +18 -0
  83. shaapi/template/backend/lang/fr/auth.py +10 -0
  84. shaapi/template/backend/main.py +54 -0
  85. shaapi/template/backend/middleware/__init__.py +1 -0
  86. shaapi/template/backend/middleware/access_middleware.py +19 -0
  87. shaapi/template/backend/middleware/i18n_middleware.py +19 -0
  88. shaapi/template/backend/middleware/jwt_auth_middleware.py +73 -0
  89. shaapi/template/backend/middleware/opera_log_middleware.py +179 -0
  90. shaapi/template/backend/middleware/state_middleware.py +26 -0
  91. shaapi/template/backend/models/__init__.py +10 -0
  92. shaapi/template/backend/models/associations.py +20 -0
  93. shaapi/template/backend/models/casbin_rule.py +30 -0
  94. shaapi/template/backend/models/login_log.py +28 -0
  95. shaapi/template/backend/models/opera_log.py +36 -0
  96. shaapi/template/backend/models/role.py +27 -0
  97. shaapi/template/backend/models/user.py +30 -0
  98. shaapi/template/backend/seeder/json/admin.json +15 -0
  99. shaapi/template/backend/seeder/json/user.json +15 -0
  100. shaapi/template/backend/seeder/run.py +34 -0
  101. shaapi/template/backend/static/ip2region.xdb +0 -0
  102. shaapi/template/backend/templates/build/meet.html +169 -0
  103. shaapi/template/backend/templates/build/new_account.html +373 -0
  104. shaapi/template/backend/templates/build/reset-password.html +170 -0
  105. shaapi/template/backend/templates/build/test_email.html +25 -0
  106. shaapi/template/backend/templates/build/welcome-one-1.html +160 -0
  107. shaapi/template/backend/templates/build/welcome-one.html +178 -0
  108. shaapi/template/backend/templates/build/welcome-two.html +234 -0
  109. shaapi/template/backend/templates/index.html +0 -0
  110. shaapi/template/backend/templates/src/new_account.mjml +15 -0
  111. shaapi/template/backend/templates/src/reset_password.mjml +19 -0
  112. shaapi/template/backend/templates/src/test_email.mjml +11 -0
  113. shaapi/template/backend/templates/ws/ws.html +70 -0
  114. shaapi/template/backend/utils/demo_site.py +18 -0
  115. shaapi/template/backend/utils/encrypt.py +108 -0
  116. shaapi/template/backend/utils/health_check.py +34 -0
  117. shaapi/template/backend/utils/prometheus.py +135 -0
  118. shaapi/template/backend/utils/request_parse.py +110 -0
  119. shaapi/template/backend/utils/serializers.py +75 -0
  120. shaapi/template/backend/utils/timezone.py +51 -0
  121. shaapi/template/backend/utils/trace_id.py +7 -0
  122. shaapi/template/backend/utils/translator.py +28 -0
  123. shaapi/template/devops/scripts/deploy.sh +7 -0
  124. shaapi/template/devops/scripts/setup_env.sh +62 -0
  125. shaapi/template/docker-compose.monitoring.yml +63 -0
  126. shaapi/template/docker-compose.override.yml +12 -0
  127. shaapi/template/docker-compose.yml +90 -0
  128. shaapi/template/docker-run.sh +99 -0
  129. shaapi/template/etc/dashboards/fastapi-observability.json +1044 -0
  130. shaapi/template/etc/dashboards.yaml +10 -0
  131. shaapi/template/etc/grafana/datasource.yml +79 -0
  132. shaapi/template/etc/prometheus/prometheus.yml +52 -0
  133. shaapi/template/package-lock.json +2102 -0
  134. shaapi/template/package.json +16 -0
  135. shaapi/template/pyproject.toml +78 -0
  136. shaapi/template/uv.lock +2866 -0
  137. shaapi-0.1.0.dist-info/METADATA +92 -0
  138. shaapi-0.1.0.dist-info/RECORD +141 -0
  139. shaapi-0.1.0.dist-info/WHEEL +4 -0
  140. shaapi-0.1.0.dist-info/entry_points.txt +2 -0
  141. shaapi-0.1.0.dist-info/licenses/LICENCE +21 -0
@@ -0,0 +1,158 @@
1
+ import dataclasses
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class CustomCodeBase(Enum):
7
+ """Custom base status code"""
8
+
9
+ @property
10
+ def code(self):
11
+ """
12
+ Get status code
13
+ """
14
+ return self.value[0]
15
+
16
+ @property
17
+ def msg(self):
18
+ """
19
+ Get status code message
20
+ """
21
+ return self.value[1]
22
+
23
+
24
+ class CustomResponseCode(CustomCodeBase):
25
+ """Custom response status codes"""
26
+
27
+ HTTP_200 = (200, 'http_200')
28
+ HTTP_201 = (201, 'http_201')
29
+ HTTP_202 = (202, 'http_202')
30
+ HTTP_204 = (204, 'http_204')
31
+ HTTP_400 = (400, 'http_400')
32
+ HTTP_401 = (401, 'http_401')
33
+ HTTP_403 = (403, 'http_403')
34
+ HTTP_404 = (404, 'http_404')
35
+ HTTP_410 = (410, 'http_410')
36
+ HTTP_422 = (422, 'http_422')
37
+ HTTP_425 = (425, 'http_425')
38
+ HTTP_429 = (429, 'http_429')
39
+ HTTP_500 = (500, 'http_500')
40
+ HTTP_502 = (502, 'http_502')
41
+ HTTP_503 = (503, 'http_503')
42
+ HTTP_504 = (504, 'http_504')
43
+
44
+
45
+ class CustomErrorCode(CustomCodeBase):
46
+ """Custom error codes"""
47
+
48
+ CAPTCHA_ERROR = (40001, 'Captcha error')
49
+
50
+
51
+ @dataclasses.dataclass
52
+ class CustomResponse:
53
+ """
54
+ Provides open response status codes instead of enums, useful when you want to customize response messages
55
+ """
56
+
57
+ code: int
58
+ msg: str
59
+
60
+
61
+ class StandardResponseCode:
62
+ """Standard response status codes"""
63
+
64
+ """
65
+ HTTP codes
66
+ See HTTP Status Code Registry:
67
+ https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
68
+
69
+ And RFC 2324 - https://tools.ietf.org/html/rfc2324
70
+ """
71
+ HTTP_100 = 100 # CONTINUE
72
+ HTTP_101 = 101 # SWITCHING_PROTOCOLS
73
+ HTTP_102 = 102 # PROCESSING
74
+ HTTP_103 = 103 # EARLY_HINTS
75
+ HTTP_200 = 200 # OK
76
+ HTTP_201 = 201 # CREATED
77
+ HTTP_202 = 202 # ACCEPTED
78
+ HTTP_203 = 203 # NON_AUTHORITATIVE_INFORMATION
79
+ HTTP_204 = 204 # NO_CONTENT
80
+ HTTP_205 = 205 # RESET_CONTENT
81
+ HTTP_206 = 206 # PARTIAL_CONTENT
82
+ HTTP_207 = 207 # MULTI_STATUS
83
+ HTTP_208 = 208 # ALREADY_REPORTED
84
+ HTTP_226 = 226 # IM_USED
85
+ HTTP_300 = 300 # MULTIPLE_CHOICES
86
+ HTTP_301 = 301 # MOVED_PERMANENTLY
87
+ HTTP_302 = 302 # FOUND
88
+ HTTP_303 = 303 # SEE_OTHER
89
+ HTTP_304 = 304 # NOT_MODIFIED
90
+ HTTP_305 = 305 # USE_PROXY
91
+ HTTP_307 = 307 # TEMPORARY_REDIRECT
92
+ HTTP_308 = 308 # PERMANENT_REDIRECT
93
+ HTTP_400 = 400 # BAD_REQUEST
94
+ HTTP_401 = 401 # UNAUTHORIZED
95
+ HTTP_402 = 402 # PAYMENT_REQUIRED
96
+ HTTP_403 = 403 # FORBIDDEN
97
+ HTTP_404 = 404 # NOT_FOUND
98
+ HTTP_405 = 405 # METHOD_NOT_ALLOWED
99
+ HTTP_406 = 406 # NOT_ACCEPTABLE
100
+ HTTP_407 = 407 # PROXY_AUTHENTICATION_REQUIRED
101
+ HTTP_408 = 408 # REQUEST_TIMEOUT
102
+ HTTP_409 = 409 # CONFLICT
103
+ HTTP_410 = 410 # GONE
104
+ HTTP_411 = 411 # LENGTH_REQUIRED
105
+ HTTP_412 = 412 # PRECONDITION_FAILED
106
+ HTTP_413 = 413 # REQUEST_ENTITY_TOO_LARGE
107
+ HTTP_414 = 414 # REQUEST_URI_TOO_LONG
108
+ HTTP_415 = 415 # UNSUPPORTED_MEDIA_TYPE
109
+ HTTP_416 = 416 # REQUESTED_RANGE_NOT_SATISFIABLE
110
+ HTTP_417 = 417 # EXPECTATION_FAILED
111
+ HTTP_418 = 418 # UNUSED
112
+ HTTP_421 = 421 # MISDIRECTED_REQUEST
113
+ HTTP_422 = 422 # UNPROCESSABLE_CONTENT
114
+ HTTP_423 = 423 # LOCKED
115
+ HTTP_424 = 424 # FAILED_DEPENDENCY
116
+ HTTP_425 = 425 # TOO_EARLY
117
+ HTTP_426 = 426 # UPGRADE_REQUIRED
118
+ HTTP_427 = 427 # UNASSIGNED
119
+ HTTP_428 = 428 # PRECONDITION_REQUIRED
120
+ HTTP_429 = 429 # TOO_MANY_REQUESTS
121
+ HTTP_430 = 430 # UNASSIGNED
122
+ HTTP_431 = 431 # REQUEST_HEADER_FIELDS_TOO_LARGE
123
+ HTTP_451 = 451 # UNAVAILABLE_FOR_LEGAL_REASONS
124
+ HTTP_500 = 500 # INTERNAL_SERVER_ERROR
125
+ HTTP_501 = 501 # NOT_IMPLEMENTED
126
+ HTTP_502 = 502 # BAD_GATEWAY
127
+ HTTP_503 = 503 # SERVICE_UNAVAILABLE
128
+ HTTP_504 = 504 # GATEWAY_TIMEOUT
129
+ HTTP_505 = 505 # HTTP_VERSION_NOT_SUPPORTED
130
+ HTTP_506 = 506 # VARIANT_ALSO_NEGOTIATES
131
+ HTTP_507 = 507 # INSUFFICIENT_STORAGE
132
+ HTTP_508 = 508 # LOOP_DETECTED
133
+ HTTP_509 = 509 # UNASSIGNED
134
+ HTTP_510 = 510 # NOT_EXTENDED
135
+ HTTP_511 = 511 # NETWORK_AUTHENTICATION_REQUIRED
136
+
137
+ """
138
+ WebSocket codes
139
+ https://www.iana.org/assignments/websocket/websocket.xml#close-code-number
140
+ https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
141
+ """
142
+ WS_1000 = 1000 # NORMAL_CLOSURE
143
+ WS_1001 = 1001 # GOING_AWAY
144
+ WS_1002 = 1002 # PROTOCOL_ERROR
145
+ WS_1003 = 1003 # UNSUPPORTED_DATA
146
+ WS_1005 = 1005 # NO_STATUS_RCVD
147
+ WS_1006 = 1006 # ABNORMAL_CLOSURE
148
+ WS_1007 = 1007 # INVALID_FRAME_PAYLOAD_DATA
149
+ WS_1008 = 1008 # POLICY_VIOLATION
150
+ WS_1009 = 1009 # MESSAGE_TOO_BIG
151
+ WS_1010 = 1010 # MANDATORY_EXT
152
+ WS_1011 = 1011 # INTERNAL_ERROR
153
+ WS_1012 = 1012 # SERVICE_RESTART
154
+ WS_1013 = 1013 # TRY_AGAIN_LATER
155
+ WS_1014 = 1014 # BAD_GATEWAY
156
+ WS_1015 = 1015 # TLS_HANDSHAKE_ERROR
157
+ WS_3000 = 3000 # UNAUTHORIZED
158
+ WS_3003 = 3003 # FORBIDDEN
@@ -0,0 +1,110 @@
1
+ from datetime import datetime
2
+ from typing import Any, Optional
3
+
4
+ from fastapi import Response, Request
5
+ from pydantic import BaseModel, ConfigDict
6
+
7
+ from backend.common.response.response_code import CustomResponse, CustomResponseCode
8
+ from backend.core.conf import settings
9
+ from backend.utils.serializers import MsgSpecJSONResponse
10
+ from backend.utils.translator import Translator
11
+
12
+ _ExcludeData = set[int | str] | dict[int | str, Any]
13
+
14
+ __all__ = ['ResponseModel', 'response_base']
15
+
16
+
17
+ class ResponseModel(BaseModel):
18
+ """
19
+ Unified response model
20
+
21
+ E.g. ::
22
+
23
+ @router.get('/test', response_model=ResponseModel)
24
+ def test():
25
+ return ResponseModel(data={'test': 'test'})
26
+
27
+ @router.get('/test')
28
+ def test() -> ResponseModel:
29
+ return ResponseModel(data={'test': 'test'})
30
+
31
+ @router.get('/test')
32
+ def test() -> ResponseModel:
33
+ res = CustomResponseCode.HTTP_200
34
+ return ResponseModel(code=res.code, msg=res.msg, data={'test': 'test'})
35
+ """
36
+
37
+ # TODO: json_encoders configuration not working: https://github.com/tiangolo/fastapi/discussions/10252
38
+ model_config = ConfigDict(json_encoders={datetime: lambda x: x.strftime(settings.DATETIME_FORMAT)})
39
+
40
+ code: int = CustomResponseCode.HTTP_200.code
41
+ msg: str = CustomResponseCode.HTTP_200.msg
42
+ data: Optional[Any] = None
43
+
44
+
45
+ class ResponseBase:
46
+ """
47
+ Unified response method
48
+
49
+ .. tip::
50
+
51
+ The methods in this class will return the ResponseModel model, which serves as a coding style convention.
52
+
53
+ E.g. ::
54
+
55
+ @router.get('/test')
56
+ def test() -> ResponseModel:
57
+ return response_base.success(data={'test': 'test'})
58
+ """
59
+
60
+ @staticmethod
61
+ def __response(*, request: Request, res: CustomResponseCode | CustomResponse = None, data: Any | None = None) -> ResponseModel:
62
+ """
63
+ General method for successful request responses
64
+
65
+ :param res: Response information
66
+ :param data: Response data
67
+ :return: ResponseModel instance
68
+ """
69
+ translator = Translator(request.state.locale)
70
+ return ResponseModel(code=res.code, msg=translator.t(f'app.{res.msg}'), data=data)
71
+
72
+ def success(
73
+ self,
74
+ *,
75
+ request: Request,
76
+ res: CustomResponseCode | CustomResponse = CustomResponseCode.HTTP_200,
77
+ data: Any | None = None,
78
+ ) -> ResponseModel:
79
+ return self.__response(request=request, res=res, data=data)
80
+
81
+ def fail(
82
+ self,
83
+ *,
84
+ request: Request,
85
+ res: CustomResponseCode | CustomResponse = CustomResponseCode.HTTP_400,
86
+ data: Any = None,
87
+ ) -> ResponseModel:
88
+ return self.__response(request=request, res=res, data=data)
89
+
90
+ @staticmethod
91
+ def fast_success(
92
+ *,
93
+ res: CustomResponseCode | CustomResponse = CustomResponseCode.HTTP_200,
94
+ data: Any | None = None,
95
+ ) -> Response:
96
+ """
97
+ This method was created to improve response speed. If the return data doesn't require parsing and validation by Pydantic, it is recommended to use this. Otherwise, avoid it.
98
+
99
+ .. warning::
100
+
101
+ When using this return method, do not specify the response_model parameter in the route, and do not add a return type annotation to the function.
102
+
103
+ :param res: Response information
104
+ :param data: Response data
105
+ :return: FastAPI Response object
106
+ """
107
+ return MsgSpecJSONResponse({'code': res.code, 'msg': res.msg, 'data': data})
108
+
109
+
110
+ response_base = ResponseBase()
@@ -0,0 +1,144 @@
1
+ from pydantic import BaseModel, ConfigDict, EmailStr, validate_email
2
+
3
+ # Custom validation error messages without validation expected content (i.e., input content).
4
+ # The supported expected content fields can be found at the link below:
5
+ # https://github.com/pydantic/pydantic-core/blob/a5cb7382643415b716b1a7a5392914e50f726528/tests/test_errors.py#L266
6
+ # To replace the expected content fields, refer to the link below:
7
+ # https://github.com/pydantic/pydantic/blob/caa78016433ec9b16a973f92f187a7b6bfde6cb5/docs/errors/errors.md?plain=1#L232
8
+ CUSTOM_VALIDATION_ERROR_MESSAGES = {
9
+ 'arguments_type': 'Incorrect argument type input',
10
+ 'assertion_error': 'Assertion execution error',
11
+ 'bool_parsing': 'Boolean value parsing error',
12
+ 'bool_type': 'Boolean type input error',
13
+ 'bytes_too_long': 'Byte length input is too long',
14
+ 'bytes_too_short': 'Byte length input is too short',
15
+ 'bytes_type': 'Byte type input error',
16
+ 'callable_type': 'Callable object type input error',
17
+ 'dataclass_exact_type': 'Dataclass instance type input error',
18
+ 'dataclass_type': 'Dataclass type input error',
19
+ 'date_from_datetime_inexact': 'Non-zero date component input',
20
+ 'date_from_datetime_parsing': 'Date input parsing error',
21
+ 'date_future': 'Date input is not in the future',
22
+ 'date_parsing': 'Date input validation error',
23
+ 'date_past': 'Date input is not in the past',
24
+ 'date_type': 'Date type input error',
25
+ 'datetime_future': 'Datetime input is not in the future',
26
+ 'datetime_object_invalid': 'Invalid datetime object input',
27
+ 'datetime_parsing': 'Datetime input parsing error',
28
+ 'datetime_past': 'Datetime input is not in the past',
29
+ 'datetime_type': 'Datetime type input error',
30
+ 'decimal_max_digits': 'Too many decimal places input',
31
+ 'decimal_max_places': 'Incorrect decimal places input',
32
+ 'decimal_parsing': 'Decimal input parsing error',
33
+ 'decimal_type': 'Decimal type input error',
34
+ 'decimal_whole_digits': 'Incorrect decimal places input',
35
+ 'dict_type': 'Dictionary type input error',
36
+ 'enum': 'Enum member input error, allowed {expected}',
37
+ 'extra_forbidden': 'Extra field input is forbidden',
38
+ 'finite_number': 'Finite number input error',
39
+ 'float_parsing': 'Float input parsing error',
40
+ 'float_type': 'Float type input error',
41
+ 'frozen_field': 'Frozen field input error',
42
+ 'frozen_instance': 'Modification of frozen instance is forbidden',
43
+ 'frozen_set_type': 'Frozen type input is forbidden',
44
+ 'get_attribute_error': 'Error retrieving attribute',
45
+ 'greater_than': 'Input value is too large',
46
+ 'greater_than_equal': 'Input value is too large or equal',
47
+ 'int_from_float': 'Integer type input error',
48
+ 'int_parsing': 'Integer input parsing error',
49
+ 'int_parsing_size': 'Integer input parsing size error',
50
+ 'int_type': 'Integer type input error',
51
+ 'invalid_key': 'Invalid key input',
52
+ 'is_instance_of': 'Instance type input error',
53
+ 'is_subclass_of': 'Subclass type input error',
54
+ 'iterable_type': 'Iterable type input error',
55
+ 'iteration_error': 'Iteration value input error',
56
+ 'json_invalid': 'Invalid JSON string input',
57
+ 'json_type': 'JSON type input error',
58
+ 'less_than': 'Input value is too small',
59
+ 'less_than_equal': 'Input value is too small or equal',
60
+ 'list_type': 'List type input error',
61
+ 'literal_error': 'Literal value input error',
62
+ 'mapping_type': 'Mapping type input error',
63
+ 'missing': 'Missing required field',
64
+ 'missing_argument': 'Missing argument',
65
+ 'missing_keyword_only_argument': 'Missing keyword-only argument',
66
+ 'missing_positional_only_argument': 'Missing positional-only argument',
67
+ 'model_attributes_type': 'Model attributes type input error',
68
+ 'model_type': 'Model instance input error',
69
+ 'multiple_argument_values': 'Too many argument values input',
70
+ 'multiple_of': 'Input value is not a multiple',
71
+ 'no_such_attribute': 'Invalid attribute assignment',
72
+ 'none_required': 'Input value must be None',
73
+ 'recursion_loop': 'Input recursion loop',
74
+ 'set_type': 'Set type input error',
75
+ 'string_pattern_mismatch': 'String pattern input mismatch',
76
+ 'string_sub_type': 'String subtype input error',
77
+ 'string_too_long': 'String input is too long',
78
+ 'string_too_short': 'String input is too short',
79
+ 'string_type': 'String type input error',
80
+ 'string_unicode': 'String input is not Unicode',
81
+ 'time_delta_parsing': 'Timedelta input parsing error',
82
+ 'time_delta_type': 'Timedelta type input error',
83
+ 'time_parsing': 'Time input parsing error',
84
+ 'time_type': 'Time type input error',
85
+ 'timezone_aware': 'Missing timezone input information',
86
+ 'timezone_naive': 'Timezone input information is forbidden',
87
+ 'too_long': 'Input is too long',
88
+ 'too_short': 'Input is too short',
89
+ 'tuple_type': 'Tuple type input error',
90
+ 'unexpected_keyword_argument': 'Unexpected keyword argument input',
91
+ 'unexpected_positional_argument': 'Unexpected positional argument input',
92
+ 'union_tag_invalid': 'Union tag input error',
93
+ 'union_tag_not_found': 'Union tag input not found',
94
+ 'url_parsing': 'URL input parsing error',
95
+ 'url_scheme': 'URL input scheme error',
96
+ 'url_syntax_violation': 'URL input syntax violation',
97
+ 'url_too_long': 'URL input is too long',
98
+ 'url_type': 'URL type input error',
99
+ 'uuid_parsing': 'UUID input parsing error',
100
+ 'uuid_type': 'UUID type input error',
101
+ 'uuid_version': 'UUID version type input error',
102
+ 'value_error': 'Value input error',
103
+ }
104
+
105
+ CUSTOM_USAGE_ERROR_MESSAGES = {
106
+ 'class-not-fully-defined': 'Class property type is not fully defined',
107
+ 'custom-json-schema': '__modify_schema__ method has been deprecated in V2',
108
+ 'decorator-missing-field': 'Invalid field validator defined',
109
+ 'discriminator-no-field': 'Discriminator field not fully defined',
110
+ 'discriminator-alias-type': 'Discriminator field uses non-string type definition',
111
+ 'discriminator-needs-literal': 'Discriminator field needs to use literal definition',
112
+ 'discriminator-alias': 'Discriminator field alias definition inconsistency',
113
+ 'discriminator-validator': 'Discriminator field cannot define field validators',
114
+ 'model-field-overridden': 'Field without type definition cannot be overridden',
115
+ 'model-field-missing-annotation': 'Field type definition is missing',
116
+ 'config-both': 'Duplicate configuration item definition',
117
+ 'removed-kwargs': 'Call to removed keyword configuration parameters',
118
+ 'invalid-for-json-schema': 'Invalid JSON type present',
119
+ 'base-model-instantiated': 'Instantiation of base model is forbidden',
120
+ 'undefined-annotation': 'Type definition is missing',
121
+ 'schema-for-unknown-type': 'Unknown type definition',
122
+ 'create-model-field-definitions': 'Field definition error',
123
+ 'create-model-config-base': 'Configuration item definition error',
124
+ 'validator-no-fields': 'Field validator did not specify fields',
125
+ 'validator-invalid-fields': 'Field validator fields definition error',
126
+ 'validator-instance-method': 'Field validator must be a class method',
127
+ 'model-serializer-instance-method': 'Serializer must be an instance method',
128
+ 'validator-v1-signature': 'V1 field validator signature has been deprecated',
129
+ 'validator-signature': 'Field validator signature error',
130
+ 'field-serializer-signature': 'Field serializer signature unrecognized',
131
+ 'model-serializer-signature': 'Model serializer signature unrecognized',
132
+ 'multiple-field-serializers': 'Multiple field serializers defined',
133
+ 'invalid_annotated_type': 'Invalid type definition',
134
+ 'type-adapter-config-unused': 'Type adapter configuration item definition error',
135
+ 'root-model-extra': 'Root model cannot define extra fields',
136
+ }
137
+
138
+ class CustomEmailStr(EmailStr):
139
+ @classmethod
140
+ def _validate(cls, __input_value: str) -> str:
141
+ return None if __input_value == '' else validate_email(__input_value)[1]
142
+
143
+ class SchemaBase(BaseModel):
144
+ model_config = ConfigDict(use_enum_values=True)
@@ -0,0 +1,203 @@
1
+ from datetime import timedelta
2
+
3
+ from fastapi import Depends, Request
4
+ from fastapi.security import HTTPBearer
5
+ from fastapi.security.utils import get_authorization_scheme_param
6
+ from jose import ExpiredSignatureError, JWTError, jwt
7
+ from passlib.context import CryptContext
8
+ from sqlalchemy.ext.asyncio import AsyncSession
9
+
10
+ from backend.models import User
11
+ from backend.common.dataclasses import AccessToken, NewToken, RefreshToken
12
+ from backend.common.exception.errors import AuthorizationError, TokenError
13
+ from backend.core.conf import settings
14
+ from backend.database.db_redis import redis_client
15
+ from backend.utils.timezone import timezone
16
+
17
+ pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
18
+
19
+
20
+ # JWT authorizes dependency injection
21
+ DependsJwtAuth = Depends(HTTPBearer())
22
+
23
+
24
+ def get_hash_password(password: str) -> str:
25
+ """
26
+ Encrypt passwords using the hash algorithm
27
+
28
+ :param password:
29
+ :return:
30
+ """
31
+ return pwd_context.hash(password)
32
+
33
+
34
+ def password_verify(plain_password: str, hashed_password: str) -> bool:
35
+ """
36
+ Password verification
37
+
38
+ :param plain_password: The password to verify
39
+ :param hashed_password: The hash ciphers to compare
40
+ :return:
41
+ """
42
+ return pwd_context.verify(plain_password, hashed_password)
43
+
44
+
45
+ async def create_access_token(sub: str, multi_login: bool) -> AccessToken:
46
+ """
47
+ Generate encryption token
48
+
49
+ :param sub: The subject/userid of the JWT
50
+ :param multi_login: multipoint login for user
51
+ :return:
52
+ """
53
+ expire = timezone.now() + timedelta(seconds=settings.TOKEN_EXPIRE_SECONDS)
54
+ expire_seconds = settings.TOKEN_EXPIRE_SECONDS
55
+
56
+ to_encode = {'exp': expire, 'sub': sub}
57
+ access_token = jwt.encode(to_encode, settings.TOKEN_SECRET_KEY, settings.TOKEN_ALGORITHM)
58
+
59
+ if multi_login is False:
60
+ key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{sub}'
61
+ await redis_client.delete_prefix(key_prefix)
62
+
63
+ key = f'{settings.TOKEN_REDIS_PREFIX}:{sub}:{access_token}'
64
+ await redis_client.setex(key, expire_seconds, access_token)
65
+ return AccessToken(access_token=access_token, access_token_expire_time=expire)
66
+
67
+
68
+ async def create_refresh_token(sub: str, multi_login: bool) -> RefreshToken:
69
+ """
70
+ Generate encryption refresh token, only used to create a new token
71
+
72
+ :param sub: The subject/userid of the JWT
73
+ :param multi_login: multipoint login for user
74
+ :return:
75
+ """
76
+ expire = timezone.now() + timedelta(seconds=settings.TOKEN_REFRESH_EXPIRE_SECONDS)
77
+ expire_seconds = settings.TOKEN_REFRESH_EXPIRE_SECONDS
78
+
79
+ to_encode = {'exp': expire, 'sub': sub}
80
+ refresh_token = jwt.encode(to_encode, settings.TOKEN_SECRET_KEY, settings.TOKEN_ALGORITHM)
81
+
82
+ if multi_login is False:
83
+ key_prefix = f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{sub}'
84
+ await redis_client.delete_prefix(key_prefix)
85
+
86
+ key = f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{sub}:{refresh_token}'
87
+ await redis_client.setex(key, expire_seconds, refresh_token)
88
+ return RefreshToken(refresh_token=refresh_token, refresh_token_expire_time=expire)
89
+
90
+
91
+ async def create_new_token(sub: str, token: str, refresh_token: str, multi_login: bool) -> NewToken:
92
+ """
93
+ Generate new token
94
+
95
+ :param sub:
96
+ :param token
97
+ :param refresh_token:
98
+ :param multi_login:
99
+ :return:
100
+ """
101
+ redis_refresh_token = await redis_client.get(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{sub}:{refresh_token}')
102
+ if not redis_refresh_token or redis_refresh_token != refresh_token:
103
+ raise TokenError(msg='Refresh Token has expired')
104
+
105
+ new_access_token = await create_access_token(sub, multi_login)
106
+ new_refresh_token = await create_refresh_token(sub, multi_login)
107
+
108
+ token_key = f'{settings.TOKEN_REDIS_PREFIX}:{sub}:{token}'
109
+ refresh_token_key = f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{sub}:{refresh_token}'
110
+ await redis_client.delete(token_key)
111
+ await redis_client.delete(refresh_token_key)
112
+ return NewToken(
113
+ new_access_token=new_access_token.access_token,
114
+ new_access_token_expire_time=new_access_token.access_token_expire_time,
115
+ new_refresh_token=new_refresh_token.refresh_token,
116
+ new_refresh_token_expire_time=new_refresh_token.refresh_token_expire_time,
117
+ )
118
+
119
+
120
+ def get_token(request: Request) -> str:
121
+ """
122
+ Get token for request header
123
+
124
+ :return:
125
+ """
126
+ authorization = request.headers.get('Authorization')
127
+ scheme, token = get_authorization_scheme_param(authorization)
128
+ if not authorization or scheme.lower() != 'bearer':
129
+ raise TokenError(msg='Token is invalid')
130
+ return token
131
+
132
+
133
+ def jwt_decode(token: str) -> int:
134
+ """
135
+ Decode token
136
+
137
+ :param token:
138
+ :return:
139
+ """
140
+ try:
141
+ payload = jwt.decode(token, settings.TOKEN_SECRET_KEY, algorithms=[settings.TOKEN_ALGORITHM])
142
+ user_id = payload.get('sub')
143
+ if not user_id:
144
+ raise TokenError(msg='Token is invalid')
145
+ except ExpiredSignatureError:
146
+ raise TokenError(msg='Token has expired')
147
+ except (JWTError, Exception):
148
+ raise TokenError(msg='Token is invalid')
149
+ return user_id
150
+
151
+
152
+ async def jwt_authentication(token: str) -> str:
153
+ """
154
+ JWT authentication
155
+
156
+ :param token:
157
+ :return:
158
+ """
159
+ user_id = jwt_decode(token)
160
+ key = f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:{token}'
161
+ token_verify = await redis_client.get(key)
162
+ if not token_verify:
163
+ raise TokenError(msg='Token has expired')
164
+ return user_id
165
+
166
+
167
+ async def get_current_user(db: AsyncSession, sub: str) -> User:
168
+ """
169
+ Get the current user through token
170
+
171
+ :param db:
172
+ :param pk:
173
+ :return:
174
+ """
175
+ from backend.crud.crud_user import user_dao
176
+
177
+ user = await user_dao.get_with_relation(db, x_id=sub)
178
+ if not user:
179
+ raise TokenError(msg='Token is invalid')
180
+ if not user.status:
181
+ raise AuthorizationError(msg='The user has been locked out, please contact the administrator')
182
+ # if user.dept_id:
183
+ # if not user.dept.status:
184
+ # raise AuthorizationError(msg='User\'s department is locked')
185
+ # if user.dept.del_flag:
186
+ # raise AuthorizationError(msg='The department to which the user belongs has been deleted')
187
+ if user.roles:
188
+ role_status = [role.status for role in user.roles]
189
+ if all(status == 0 for status in role_status):
190
+ raise AuthorizationError(msg='User\'s role is locked')
191
+ return user
192
+
193
+ def superuser_verify(request: Request) -> bool:
194
+ """
195
+ Verify the current user permissions through token
196
+
197
+ :param request:
198
+ :return:
199
+ """
200
+ superuser = request.user.is_superuser
201
+ if not superuser or not request.user.is_staff:
202
+ raise AuthorizationError
203
+ return superuser