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.
- shaapi/__init__.py +3 -0
- shaapi/cli.py +97 -0
- shaapi/generator.py +114 -0
- shaapi/template/.dockerignore +37 -0
- shaapi/template/.env.template +59 -0
- shaapi/template/.gitattributes +12 -0
- shaapi/template/.gitignore +170 -0
- shaapi/template/.gitlab-ci.yml +89 -0
- shaapi/template/Dockerfile +59 -0
- shaapi/template/LICENSE +21 -0
- shaapi/template/README.md +206 -0
- shaapi/template/backend/.gitignore +164 -0
- shaapi/template/backend/__init__.py +0 -0
- shaapi/template/backend/alembic/README +1 -0
- shaapi/template/backend/alembic/env.py +102 -0
- shaapi/template/backend/alembic/script.py.mako +26 -0
- shaapi/template/backend/alembic/versions/2026_06_08_1024-64524c63b666_initial.py +143 -0
- shaapi/template/backend/alembic.ini +117 -0
- shaapi/template/backend/app/__init__.py +55 -0
- shaapi/template/backend/app/admin/__init__.py +1 -0
- shaapi/template/backend/app/admin/api/v1/__init__.py +0 -0
- shaapi/template/backend/app/admin/api/v1/auth.py +59 -0
- shaapi/template/backend/app/admin/api/v1/casbin.py +218 -0
- shaapi/template/backend/app/admin/api/v1/login_log.py +63 -0
- shaapi/template/backend/app/admin/api/v1/opera_log.py +61 -0
- shaapi/template/backend/app/admin/api/v1/role.py +108 -0
- shaapi/template/backend/app/admin/api/v1/user.py +47 -0
- shaapi/template/backend/app/admin/schema/casbin_rule.py +45 -0
- shaapi/template/backend/app/admin/schema/login_log.py +36 -0
- shaapi/template/backend/app/admin/schema/opera_log.py +43 -0
- shaapi/template/backend/app/admin/schema/role.py +36 -0
- shaapi/template/backend/app/admin/schema/sso.py +37 -0
- shaapi/template/backend/app/admin/schema/token.py +74 -0
- shaapi/template/backend/app/admin/schema/user.py +93 -0
- shaapi/template/backend/app/admin/service/auth_service.py +233 -0
- shaapi/template/backend/app/admin/service/casbin_service.py +135 -0
- shaapi/template/backend/app/admin/service/login_log_service.py +62 -0
- shaapi/template/backend/app/admin/service/opera_log_service.py +31 -0
- shaapi/template/backend/app/admin/service/role_service.py +79 -0
- shaapi/template/backend/app/admin/service/secure_token_service.py +60 -0
- shaapi/template/backend/app/admin/service/user_service.py +153 -0
- shaapi/template/backend/app/api.py +11 -0
- shaapi/template/backend/common/__init__.py +0 -0
- shaapi/template/backend/common/cloud_storage/__init__.py +11 -0
- shaapi/template/backend/common/cloud_storage/cloud_storage.py +180 -0
- shaapi/template/backend/common/dataclasses.py +52 -0
- shaapi/template/backend/common/email_conf/email.py +105 -0
- shaapi/template/backend/common/enums.py +144 -0
- shaapi/template/backend/common/exception/__init__.py +0 -0
- shaapi/template/backend/common/exception/errors.py +87 -0
- shaapi/template/backend/common/exception/exception_handler.py +280 -0
- shaapi/template/backend/common/log.py +123 -0
- shaapi/template/backend/common/model.py +68 -0
- shaapi/template/backend/common/pagination.py +83 -0
- shaapi/template/backend/common/response/__init__.py +0 -0
- shaapi/template/backend/common/response/response_code.py +158 -0
- shaapi/template/backend/common/response/response_schema.py +110 -0
- shaapi/template/backend/common/schema.py +144 -0
- shaapi/template/backend/common/security/jwt.py +203 -0
- shaapi/template/backend/common/security/rbac.py +98 -0
- shaapi/template/backend/common/security/sec_token.py +6 -0
- shaapi/template/backend/common/socketio/action.py +11 -0
- shaapi/template/backend/common/socketio/server.py +50 -0
- shaapi/template/backend/common/sso/base.py +69 -0
- shaapi/template/backend/common/sso/google.py +127 -0
- shaapi/template/backend/core/conf.py +208 -0
- shaapi/template/backend/core/path_conf.py +24 -0
- shaapi/template/backend/core/registrar.py +195 -0
- shaapi/template/backend/crud/__init__.py +1 -0
- shaapi/template/backend/crud/crud_base.py +35 -0
- shaapi/template/backend/crud/crud_casbin.py +46 -0
- shaapi/template/backend/crud/crud_login_log.py +58 -0
- shaapi/template/backend/crud/crud_opera_log.py +58 -0
- shaapi/template/backend/crud/crud_role.py +128 -0
- shaapi/template/backend/crud/crud_user.py +267 -0
- shaapi/template/backend/database/__init__.py +0 -0
- shaapi/template/backend/database/db_postgres.py +125 -0
- shaapi/template/backend/database/db_redis.py +62 -0
- shaapi/template/backend/entrypoint-api.sh +19 -0
- shaapi/template/backend/lang/en/app.py +18 -0
- shaapi/template/backend/lang/en/auth.py +10 -0
- shaapi/template/backend/lang/fr/app.py +18 -0
- shaapi/template/backend/lang/fr/auth.py +10 -0
- shaapi/template/backend/main.py +54 -0
- shaapi/template/backend/middleware/__init__.py +1 -0
- shaapi/template/backend/middleware/access_middleware.py +19 -0
- shaapi/template/backend/middleware/i18n_middleware.py +19 -0
- shaapi/template/backend/middleware/jwt_auth_middleware.py +73 -0
- shaapi/template/backend/middleware/opera_log_middleware.py +179 -0
- shaapi/template/backend/middleware/state_middleware.py +26 -0
- shaapi/template/backend/models/__init__.py +10 -0
- shaapi/template/backend/models/associations.py +20 -0
- shaapi/template/backend/models/casbin_rule.py +30 -0
- shaapi/template/backend/models/login_log.py +28 -0
- shaapi/template/backend/models/opera_log.py +36 -0
- shaapi/template/backend/models/role.py +27 -0
- shaapi/template/backend/models/user.py +30 -0
- shaapi/template/backend/seeder/json/admin.json +15 -0
- shaapi/template/backend/seeder/json/user.json +15 -0
- shaapi/template/backend/seeder/run.py +34 -0
- shaapi/template/backend/static/ip2region.xdb +0 -0
- shaapi/template/backend/templates/build/meet.html +169 -0
- shaapi/template/backend/templates/build/new_account.html +373 -0
- shaapi/template/backend/templates/build/reset-password.html +170 -0
- shaapi/template/backend/templates/build/test_email.html +25 -0
- shaapi/template/backend/templates/build/welcome-one-1.html +160 -0
- shaapi/template/backend/templates/build/welcome-one.html +178 -0
- shaapi/template/backend/templates/build/welcome-two.html +234 -0
- shaapi/template/backend/templates/index.html +0 -0
- shaapi/template/backend/templates/src/new_account.mjml +15 -0
- shaapi/template/backend/templates/src/reset_password.mjml +19 -0
- shaapi/template/backend/templates/src/test_email.mjml +11 -0
- shaapi/template/backend/templates/ws/ws.html +70 -0
- shaapi/template/backend/utils/demo_site.py +18 -0
- shaapi/template/backend/utils/encrypt.py +108 -0
- shaapi/template/backend/utils/health_check.py +34 -0
- shaapi/template/backend/utils/prometheus.py +135 -0
- shaapi/template/backend/utils/request_parse.py +110 -0
- shaapi/template/backend/utils/serializers.py +75 -0
- shaapi/template/backend/utils/timezone.py +51 -0
- shaapi/template/backend/utils/trace_id.py +7 -0
- shaapi/template/backend/utils/translator.py +28 -0
- shaapi/template/devops/scripts/deploy.sh +7 -0
- shaapi/template/devops/scripts/setup_env.sh +62 -0
- shaapi/template/docker-compose.monitoring.yml +63 -0
- shaapi/template/docker-compose.override.yml +12 -0
- shaapi/template/docker-compose.yml +90 -0
- shaapi/template/docker-run.sh +99 -0
- shaapi/template/etc/dashboards/fastapi-observability.json +1044 -0
- shaapi/template/etc/dashboards.yaml +10 -0
- shaapi/template/etc/grafana/datasource.yml +79 -0
- shaapi/template/etc/prometheus/prometheus.yml +52 -0
- shaapi/template/package-lock.json +2102 -0
- shaapi/template/package.json +16 -0
- shaapi/template/pyproject.toml +78 -0
- shaapi/template/uv.lock +2866 -0
- shaapi-0.1.0.dist-info/METADATA +92 -0
- shaapi-0.1.0.dist-info/RECORD +141 -0
- shaapi-0.1.0.dist-info/WHEEL +4 -0
- shaapi-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|