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,73 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from fastapi import Request, Response
|
|
4
|
+
from fastapi.security.utils import get_authorization_scheme_param
|
|
5
|
+
from pydantic_core import from_json
|
|
6
|
+
from starlette.authentication import AuthCredentials, AuthenticationBackend, AuthenticationError
|
|
7
|
+
from starlette.requests import HTTPConnection
|
|
8
|
+
|
|
9
|
+
from backend.app.admin.schema.user import CurrentUserIns
|
|
10
|
+
from backend.common.exception.errors import TokenError
|
|
11
|
+
from backend.common.log import log
|
|
12
|
+
from backend.common.security import jwt
|
|
13
|
+
from backend.core.conf import settings
|
|
14
|
+
from backend.database.db_postgres import async_db_session
|
|
15
|
+
from backend.database.db_redis import redis_client
|
|
16
|
+
from backend.utils.serializers import MsgSpecJSONResponse, select_as_dict
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _AuthenticationError(AuthenticationError):
|
|
20
|
+
"""Rewrite internal authentication error class"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, *, code: int = None, msg: str = None, headers: dict[str, Any] | None = None):
|
|
23
|
+
self.code = code
|
|
24
|
+
self.msg = msg
|
|
25
|
+
self.headers = headers
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class JwtAuthMiddleware(AuthenticationBackend):
|
|
29
|
+
"""JWT Authentication Middleware"""
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def auth_exception_handler(conn: HTTPConnection, exc: _AuthenticationError) -> Response:
|
|
33
|
+
"""Override internal authentication error handling"""
|
|
34
|
+
return MsgSpecJSONResponse(content={'code': exc.code, 'msg': exc.msg, 'data': None}, status_code=exc.code)
|
|
35
|
+
|
|
36
|
+
async def authenticate(self, request: Request) -> tuple[AuthCredentials, CurrentUserIns] | None:
|
|
37
|
+
token = request.headers.get('Authorization')
|
|
38
|
+
if not token:
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
if request.url.path in settings.TOKEN_REQUEST_PATH_EXCLUDE:
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
scheme, token = get_authorization_scheme_param(token)
|
|
45
|
+
if scheme.lower() != 'bearer':
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
sub = await jwt.jwt_authentication(token)
|
|
50
|
+
cache_user = await redis_client.get(f'{settings.JWT_USER_REDIS_PREFIX}:{sub}')
|
|
51
|
+
if not cache_user:
|
|
52
|
+
async with async_db_session() as db:
|
|
53
|
+
current_user = await jwt.get_current_user(db, sub)
|
|
54
|
+
user = CurrentUserIns(**select_as_dict(current_user))
|
|
55
|
+
await redis_client.setex(
|
|
56
|
+
f'{settings.JWT_USER_REDIS_PREFIX}:{sub}',
|
|
57
|
+
settings.JWT_USER_REDIS_EXPIRE_SECONDS,
|
|
58
|
+
user.model_dump_json(),
|
|
59
|
+
)
|
|
60
|
+
else:
|
|
61
|
+
# TODO: it should be replaced with the use of model_validate_json
|
|
62
|
+
# https://docs.pydantic.dev/latest/concepts/json/#partial-json-parsing
|
|
63
|
+
user = CurrentUserIns.model_validate(from_json(cache_user, allow_partial=True))
|
|
64
|
+
except TokenError as exc:
|
|
65
|
+
raise _AuthenticationError(code=exc.code, msg=exc.detail, headers=exc.headers)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
log.error(f'JWT Authorization Exception:{e}')
|
|
68
|
+
raise _AuthenticationError(code=getattr(e, 'code', 500), msg=getattr(e, 'msg', 'Internal Server Error'))
|
|
69
|
+
|
|
70
|
+
# Note that this return uses a non-standard mode, so some of the standard features will be lost when authentication is passed.
|
|
71
|
+
# For standard return modes see: https://www.starlette.io/authentication/
|
|
72
|
+
return AuthCredentials(['authenticated']), user
|
|
73
|
+
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from asyncio import create_task
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
from asgiref.sync import sync_to_async
|
|
5
|
+
from fastapi import Response
|
|
6
|
+
from starlette.datastructures import UploadFile
|
|
7
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
8
|
+
from starlette.requests import Request
|
|
9
|
+
|
|
10
|
+
from backend.app.admin.schema.opera_log import CreateOperaLogParam
|
|
11
|
+
from backend.app.admin.service.opera_log_service import OperaLogService
|
|
12
|
+
from backend.common.dataclasses import RequestCallNext
|
|
13
|
+
from backend.common.enums import OperaLogCipherType, StatusType
|
|
14
|
+
from backend.common.log import log
|
|
15
|
+
from backend.core.conf import settings
|
|
16
|
+
from backend.utils.encrypt import AESCipher, ItsDCipher, Md5Cipher
|
|
17
|
+
from backend.utils.trace_id import get_request_trace_id
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OperaLogMiddleware(BaseHTTPMiddleware):
|
|
21
|
+
"""Operation Logging Middleware"""
|
|
22
|
+
|
|
23
|
+
async def dispatch(self, request: Request, call_next) -> Response:
|
|
24
|
+
# Whitelisting of excluded records
|
|
25
|
+
path = request.url.path
|
|
26
|
+
if path in settings.OPERA_LOG_PATH_EXCLUDE or not path.startswith((
|
|
27
|
+
f'/client{settings.FASTAPI_API_V1_PATH}',
|
|
28
|
+
f'/admin{settings.FASTAPI_API_V1_PATH}',
|
|
29
|
+
f'/company{settings.FASTAPI_API_V1_PATH}',
|
|
30
|
+
f'/mentor{settings.FASTAPI_API_V1_PATH}')):
|
|
31
|
+
return await call_next(request)
|
|
32
|
+
|
|
33
|
+
# request resolution
|
|
34
|
+
try:
|
|
35
|
+
# This information is dependent on the jwt middleware
|
|
36
|
+
user_email = request.user.email
|
|
37
|
+
except AttributeError:
|
|
38
|
+
user_email = None
|
|
39
|
+
method = request.method
|
|
40
|
+
args = await self.get_request_args(request)
|
|
41
|
+
args = await self.desensitization(args)
|
|
42
|
+
|
|
43
|
+
# execute a request
|
|
44
|
+
start_time = datetime.now()
|
|
45
|
+
request_next = await self.execute_request(request, call_next)
|
|
46
|
+
end_time = datetime.now()
|
|
47
|
+
cost_time = (end_time - start_time).total_seconds() * 1000.0
|
|
48
|
+
|
|
49
|
+
# This information can only be obtained after a request
|
|
50
|
+
_route = request.scope.get('route')
|
|
51
|
+
summary = getattr(_route, 'summary', None) or ''
|
|
52
|
+
|
|
53
|
+
# Log Creation
|
|
54
|
+
opera_log_in = CreateOperaLogParam(
|
|
55
|
+
trace_id=get_request_trace_id(request),
|
|
56
|
+
user_email=user_email,
|
|
57
|
+
method=method,
|
|
58
|
+
title=summary,
|
|
59
|
+
path=path,
|
|
60
|
+
ip=request.state.ip,
|
|
61
|
+
country=request.state.country,
|
|
62
|
+
region=request.state.region,
|
|
63
|
+
city=request.state.city,
|
|
64
|
+
user_agent=request.state.user_agent,
|
|
65
|
+
os=request.state.os,
|
|
66
|
+
browser=request.state.browser,
|
|
67
|
+
device=request.state.device,
|
|
68
|
+
args=args,
|
|
69
|
+
status=request_next.status,
|
|
70
|
+
code=request_next.code,
|
|
71
|
+
msg=request_next.msg,
|
|
72
|
+
cost_time=cost_time,
|
|
73
|
+
opera_time=start_time,
|
|
74
|
+
)
|
|
75
|
+
create_task(OperaLogService.create(obj_in=opera_log_in)) # noqa: ignore
|
|
76
|
+
|
|
77
|
+
# error throwing
|
|
78
|
+
err = request_next.err
|
|
79
|
+
if err:
|
|
80
|
+
raise err from None
|
|
81
|
+
|
|
82
|
+
return request_next.response
|
|
83
|
+
|
|
84
|
+
async def execute_request(self, request: Request, call_next) -> RequestCallNext:
|
|
85
|
+
"""execute a request"""
|
|
86
|
+
code = 200
|
|
87
|
+
msg = 'Success'
|
|
88
|
+
status = StatusType.enable
|
|
89
|
+
err = None
|
|
90
|
+
response = None
|
|
91
|
+
try:
|
|
92
|
+
response = await call_next(request)
|
|
93
|
+
code, msg = self.request_exception_handler(request, code, msg)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
log.error(f'Request Exception: {e}')
|
|
96
|
+
# code processing with SQLAlchemy and Pydantic.
|
|
97
|
+
code = getattr(e, 'code', None) or code
|
|
98
|
+
msg = getattr(e, 'msg', None) or msg
|
|
99
|
+
status = StatusType.disable
|
|
100
|
+
err = e
|
|
101
|
+
|
|
102
|
+
return RequestCallNext(code=str(code), msg=msg, status=status, err=err, response=response)
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def request_exception_handler(request: Request, code: int, msg: str) -> tuple[str, str]:
|
|
106
|
+
"""Request Exception Handler"""
|
|
107
|
+
exception_states = [
|
|
108
|
+
'__request_http_exception__',
|
|
109
|
+
'__request_validation_exception__',
|
|
110
|
+
'__request_pydantic_user_error__',
|
|
111
|
+
'__request_assertion_error__',
|
|
112
|
+
'__request_custom_exception__',
|
|
113
|
+
'__request_all_unknown_exception__',
|
|
114
|
+
'__request_cors_500_exception__',
|
|
115
|
+
]
|
|
116
|
+
for state in exception_states:
|
|
117
|
+
exception = getattr(request.state, state, None)
|
|
118
|
+
if exception:
|
|
119
|
+
code = exception.get('code')
|
|
120
|
+
msg = exception.get('msg')
|
|
121
|
+
log.error(f'Exception: {msg}')
|
|
122
|
+
break
|
|
123
|
+
return code, msg
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
async def get_request_args(request: Request) -> dict:
|
|
127
|
+
"""Request Exception"""
|
|
128
|
+
args = dict(request.query_params)
|
|
129
|
+
args.update(request.path_params)
|
|
130
|
+
# Tip: .body() must be fetched before .form().
|
|
131
|
+
# https://github.com/encode/starlette/discussions/1933
|
|
132
|
+
body_data = await request.body()
|
|
133
|
+
form_data = await request.form()
|
|
134
|
+
if len(form_data) > 0:
|
|
135
|
+
args.update({k: v.filename if isinstance(v, UploadFile) else v for k, v in form_data.items()})
|
|
136
|
+
else:
|
|
137
|
+
if body_data:
|
|
138
|
+
json_data = await request.json()
|
|
139
|
+
if not isinstance(json_data, dict):
|
|
140
|
+
json_data = {
|
|
141
|
+
f'{type(json_data)}_to_dict_data': json_data.decode('utf-8')
|
|
142
|
+
if isinstance(json_data, bytes)
|
|
143
|
+
else json_data
|
|
144
|
+
}
|
|
145
|
+
args.update(json_data)
|
|
146
|
+
return args
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
@sync_to_async
|
|
150
|
+
def desensitization(args: dict) -> dict | None:
|
|
151
|
+
"""
|
|
152
|
+
desensitization
|
|
153
|
+
|
|
154
|
+
:param args:
|
|
155
|
+
:return:
|
|
156
|
+
"""
|
|
157
|
+
if not args:
|
|
158
|
+
args = None
|
|
159
|
+
else:
|
|
160
|
+
match settings.OPERA_LOG_ENCRYPT_TYPE:
|
|
161
|
+
case OperaLogCipherType.aes:
|
|
162
|
+
for key in args.keys():
|
|
163
|
+
if key in settings.OPERA_LOG_ENCRYPT_KEY_INCLUDE:
|
|
164
|
+
args[key] = (AESCipher(settings.OPERA_LOG_ENCRYPT_SECRET_KEY).encrypt(args[key])).hex()
|
|
165
|
+
case OperaLogCipherType.md5:
|
|
166
|
+
for key in args.keys():
|
|
167
|
+
if key in settings.OPERA_LOG_ENCRYPT_KEY_INCLUDE:
|
|
168
|
+
args[key] = Md5Cipher.encrypt(args[key])
|
|
169
|
+
case OperaLogCipherType.itsdangerous:
|
|
170
|
+
for key in args.keys():
|
|
171
|
+
if key in settings.OPERA_LOG_ENCRYPT_KEY_INCLUDE:
|
|
172
|
+
args[key] = ItsDCipher(settings.OPERA_LOG_ENCRYPT_SECRET_KEY).encrypt(args[key])
|
|
173
|
+
case OperaLogCipherType.plan:
|
|
174
|
+
pass
|
|
175
|
+
case _:
|
|
176
|
+
for key in args.keys():
|
|
177
|
+
if key in settings.OPERA_LOG_ENCRYPT_KEY_INCLUDE:
|
|
178
|
+
args[key] = '******'
|
|
179
|
+
return args
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from fastapi import Request, Response
|
|
2
|
+
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
|
3
|
+
|
|
4
|
+
from backend.utils.request_parse import parse_ip_info, parse_user_agent_info
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class StateMiddleware(BaseHTTPMiddleware):
|
|
8
|
+
"""Request state middleware"""
|
|
9
|
+
|
|
10
|
+
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
|
|
11
|
+
ip_info = await parse_ip_info(request)
|
|
12
|
+
ua_info = parse_user_agent_info(request)
|
|
13
|
+
|
|
14
|
+
# Setting up additional request information
|
|
15
|
+
request.state.ip = ip_info.ip
|
|
16
|
+
request.state.country = ip_info.country
|
|
17
|
+
request.state.region = ip_info.region
|
|
18
|
+
request.state.city = ip_info.city
|
|
19
|
+
request.state.user_agent = ua_info.user_agent
|
|
20
|
+
request.state.os = ua_info.os
|
|
21
|
+
request.state.browser = ua_info.browser
|
|
22
|
+
request.state.device = ua_info.device
|
|
23
|
+
|
|
24
|
+
response = await call_next(request)
|
|
25
|
+
|
|
26
|
+
return response
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from backend.models.casbin_rule import CasbinRule
|
|
2
|
+
from backend.models.role import Role
|
|
3
|
+
from backend.models.user import User
|
|
4
|
+
from backend.models.opera_log import OperaLog
|
|
5
|
+
from backend.models.login_log import LoginLog
|
|
6
|
+
|
|
7
|
+
import pkgutil
|
|
8
|
+
import importlib
|
|
9
|
+
import inspect
|
|
10
|
+
from backend.common.model import Base
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import sqlalchemy as sa
|
|
2
|
+
from sqlalchemy.orm import Mapped
|
|
3
|
+
|
|
4
|
+
from backend.common.model import MappedBase
|
|
5
|
+
|
|
6
|
+
user_role = sa.Table(
|
|
7
|
+
"user_role",
|
|
8
|
+
MappedBase.metadata,
|
|
9
|
+
sa.Column(
|
|
10
|
+
"id",
|
|
11
|
+
sa.Integer,
|
|
12
|
+
primary_key=True,
|
|
13
|
+
unique=True,
|
|
14
|
+
index=True,
|
|
15
|
+
autoincrement=True,
|
|
16
|
+
comment="Primary Key ID",
|
|
17
|
+
),
|
|
18
|
+
sa.Column("user_id", sa.Integer, sa.ForeignKey("user.id", ondelete="CASCADE")),
|
|
19
|
+
sa.Column("role_id", sa.Integer, sa.ForeignKey("role.id", ondelete="CASCADE")),
|
|
20
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from sqlalchemy import String, TEXT
|
|
2
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
3
|
+
|
|
4
|
+
from backend.common.model import MappedBase, id_key
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CasbinRule(MappedBase):
|
|
8
|
+
"""Rewrite the CasbinRule model class in Casbin, using a custom Base to avoid alembic migration issues"""
|
|
9
|
+
|
|
10
|
+
__tablename__ = 'casbin_rule'
|
|
11
|
+
|
|
12
|
+
id: Mapped[id_key]
|
|
13
|
+
ptype: Mapped[str] = mapped_column(String(255), comment='Policy type: p / g')
|
|
14
|
+
v0: Mapped[str] = mapped_column(String(255), comment='Role ID / User x_id')
|
|
15
|
+
v1: Mapped[str] = mapped_column(TEXT, comment='API path / Role name')
|
|
16
|
+
v2: Mapped[str | None] = mapped_column(String(255), comment='Request method')
|
|
17
|
+
v3: Mapped[str | None] = mapped_column(String(255))
|
|
18
|
+
v4: Mapped[str | None] = mapped_column(String(255))
|
|
19
|
+
v5: Mapped[str | None] = mapped_column(String(255))
|
|
20
|
+
|
|
21
|
+
def __str__(self):
|
|
22
|
+
arr = [self.ptype]
|
|
23
|
+
for v in (self.v0, self.v1, self.v2, self.v3, self.v4, self.v5):
|
|
24
|
+
if v is None:
|
|
25
|
+
break
|
|
26
|
+
arr.append(v)
|
|
27
|
+
return ', '.join(arr)
|
|
28
|
+
|
|
29
|
+
def __repr__(self):
|
|
30
|
+
return '<CasbinRule {}: "{}">'.format(self.id, str(self))
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
import sqlalchemy as sa
|
|
3
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
4
|
+
from sqlalchemy import func
|
|
5
|
+
|
|
6
|
+
from backend.common.model import DataClassBase, get_id, id_key
|
|
7
|
+
from backend.utils.timezone import timezone
|
|
8
|
+
|
|
9
|
+
class LoginLog(DataClassBase):
|
|
10
|
+
"""Login Log Table"""
|
|
11
|
+
|
|
12
|
+
__tablename__ = 'login_log'
|
|
13
|
+
|
|
14
|
+
id: Mapped[id_key] = mapped_column(init=False)
|
|
15
|
+
user_x_id: Mapped[str] = mapped_column(sa.String(32))
|
|
16
|
+
email: Mapped[str] = mapped_column(sa.String, comment='User email')
|
|
17
|
+
status: Mapped[int] = mapped_column(sa.Integer, insert_default=0, comment='Login status (0 failed, 1 success)')
|
|
18
|
+
ip: Mapped[str] = mapped_column(sa.String, comment='Login IP address')
|
|
19
|
+
country: Mapped[str | None] = mapped_column(sa.String, comment='Country')
|
|
20
|
+
region: Mapped[str | None] = mapped_column(sa.String, comment='Region')
|
|
21
|
+
city: Mapped[str | None] = mapped_column(sa.String, comment='City')
|
|
22
|
+
user_agent: Mapped[str] = mapped_column(sa.TEXT, comment='User-Agent header')
|
|
23
|
+
os: Mapped[str | None] = mapped_column(sa.String, comment='Operating system')
|
|
24
|
+
browser: Mapped[str | None] = mapped_column(sa.String, comment='Browser')
|
|
25
|
+
device: Mapped[str | None] = mapped_column(sa.String, comment='Device')
|
|
26
|
+
msg: Mapped[str] = mapped_column(sa.TEXT, comment='Message')
|
|
27
|
+
login_time: Mapped[datetime] = mapped_column(comment='Login time')
|
|
28
|
+
created_time: Mapped[datetime] = mapped_column(sa.DateTime(timezone=True), init=False, default=func.now(), comment='Creation time')
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import sqlalchemy as sa
|
|
2
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from sqlalchemy.sql.functions import current_timestamp
|
|
5
|
+
|
|
6
|
+
from backend.common.model import DataClassBase, get_id, id_key
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OperaLog(DataClassBase):
|
|
11
|
+
"""Operation Log Table"""
|
|
12
|
+
|
|
13
|
+
__tablename__ = 'opera_log'
|
|
14
|
+
|
|
15
|
+
id: Mapped[id_key] = mapped_column(init=False)
|
|
16
|
+
# x_id: Mapped[str] = mapped_column(sa.String(32), unique=True, insert_default=get_id)
|
|
17
|
+
trace_id: Mapped[str] = mapped_column(sa.String(32), comment='Request Tracking ID')
|
|
18
|
+
user_email: Mapped[str | None] = mapped_column(sa.String, comment='User Email', nullable=True)
|
|
19
|
+
method: Mapped[str] = mapped_column(sa.String, comment='Request method')
|
|
20
|
+
title: Mapped[str] = mapped_column(sa.String, comment='Operating module')
|
|
21
|
+
path: Mapped[str] = mapped_column(sa.String, comment='Request path')
|
|
22
|
+
ip: Mapped[str] = mapped_column(sa.String, comment='IP address')
|
|
23
|
+
country: Mapped[str | None] = mapped_column(sa.String, comment='Country', nullable=True)
|
|
24
|
+
region: Mapped[str | None] = mapped_column(sa.String, comment='Region', nullable=True)
|
|
25
|
+
city: Mapped[str | None] = mapped_column(sa.String, comment='City', nullable=True)
|
|
26
|
+
user_agent: Mapped[str] = mapped_column(sa.String, comment='Request header')
|
|
27
|
+
os: Mapped[str] = mapped_column(sa.String, comment='Operating system')
|
|
28
|
+
browser: Mapped[str] = mapped_column(sa.String, comment='Browser (software)')
|
|
29
|
+
device: Mapped[str] = mapped_column(sa.String, comment='Device')
|
|
30
|
+
args: Mapped[dict] = mapped_column(sa.JSON(), comment='Request Parameters')
|
|
31
|
+
status: Mapped[int] = mapped_column(sa.Integer, comment='Operation status (0 abnormal 1 normal)')
|
|
32
|
+
code: Mapped[str] = mapped_column(sa.String(20), insert_default='200', comment='Operation status code')
|
|
33
|
+
msg: Mapped[str] = mapped_column(sa.TEXT, comment='Alert message')
|
|
34
|
+
cost_time: Mapped[float] = mapped_column(insert_default=0.0, comment='Request elapsed time (ms)')
|
|
35
|
+
opera_time: Mapped[datetime] = mapped_column(comment="Operating time")
|
|
36
|
+
created_time: Mapped[datetime] = mapped_column(sa.DateTime(timezone=True), init=False, default=current_timestamp(), comment="Creation time")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
import sqlalchemy as sa
|
|
3
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
4
|
+
|
|
5
|
+
from backend.common.model import Base, get_id, id_key
|
|
6
|
+
from backend.models.associations import user_role
|
|
7
|
+
from backend.models.associations import user_role
|
|
8
|
+
|
|
9
|
+
from backend.common.enums import Role as Role_enum
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Role(Base):
|
|
13
|
+
"""Boilerplate roles table"""
|
|
14
|
+
|
|
15
|
+
__tablename__ = 'role'
|
|
16
|
+
|
|
17
|
+
id: Mapped[id_key] = mapped_column(init=False)
|
|
18
|
+
x_id: Mapped[str] = mapped_column(sa.String(32), init=False, unique=True, default=get_id)
|
|
19
|
+
name: Mapped[str] = mapped_column(sa.String(), unique=True, default=Role_enum.USER.value, comment='role name')
|
|
20
|
+
# data_scope: Mapped[int | None] = mapped_column(insert_default=2, comment='Permission ranges (1: all data permissions 2: custom data permissions)')
|
|
21
|
+
status: Mapped[int] = mapped_column(sa.Integer, default=1, comment='Role status (0 deactivated 1 normal)')
|
|
22
|
+
remark: Mapped[str] = mapped_column(sa.Text, default=None, comment='note')
|
|
23
|
+
|
|
24
|
+
# Role users many-to-many
|
|
25
|
+
users: Mapped[List["User"]] = relationship(
|
|
26
|
+
init=False, secondary=user_role, back_populates="roles", lazy="noload"
|
|
27
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import sqlalchemy as sa
|
|
2
|
+
from sqlalchemy.orm import relationship, Mapped, mapped_column
|
|
3
|
+
from sqlalchemy.sql.functions import current_timestamp
|
|
4
|
+
from sqlalchemy import func
|
|
5
|
+
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from backend.common.model import Base, id_key, get_id
|
|
10
|
+
from backend.utils.timezone import timezone
|
|
11
|
+
from .associations import user_role
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class User(Base):
|
|
16
|
+
id: Mapped[id_key] = mapped_column(init=False)
|
|
17
|
+
x_id: Mapped[str] = mapped_column(sa.String(32), init=False, unique=True, default=get_id)
|
|
18
|
+
firstname: Mapped[str] = mapped_column(sa.String, init=False, nullable=True)
|
|
19
|
+
lastname: Mapped[str] = mapped_column(sa.String, init=False, index=True, nullable=True)
|
|
20
|
+
phone: Mapped[str] = mapped_column(sa.String, init=False, index=True, nullable=True)
|
|
21
|
+
email: Mapped[str] = mapped_column(sa.String(200), index=True, unique=True, nullable=False)
|
|
22
|
+
password: Mapped[str] = mapped_column(sa.String, nullable=False)
|
|
23
|
+
salt: Mapped[str] = mapped_column(sa.String, nullable=True)
|
|
24
|
+
status: Mapped[bool] = mapped_column(sa.Boolean, default=True, server_default='1') # User account status (False: deactivated, True: normal)
|
|
25
|
+
is_multi_login: Mapped[bool] = mapped_column(sa.Boolean(), nullable=False, default=False, server_default='1')
|
|
26
|
+
last_login_time: Mapped[datetime] = mapped_column(sa.DateTime(timezone=True), init=False, onupdate=func.now(), default=current_timestamp())
|
|
27
|
+
join_time: Mapped[datetime] = mapped_column(sa.DateTime(timezone=True), init=False, default=current_timestamp())
|
|
28
|
+
|
|
29
|
+
# User related roles
|
|
30
|
+
roles: Mapped[list["Role"]] = relationship(secondary=user_role, init=False, lazy="joined")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"model": "backend.models.Admin",
|
|
3
|
+
"data": [
|
|
4
|
+
{
|
|
5
|
+
"firstname": "Kaanari",
|
|
6
|
+
"lastname": "Tech",
|
|
7
|
+
"fullname": "Kaanari Tech",
|
|
8
|
+
"phone": "",
|
|
9
|
+
"email": "admin@kaanari.com",
|
|
10
|
+
"email_verified": true,
|
|
11
|
+
"password": "$pbkdf2-sha256$29000$ubcWorT2njOmtLY25ty71w$Yo/aLAd5AxAolsTjDoOqtkvUvUWD6TErRR7.tCsMQ8g"
|
|
12
|
+
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"model": "backend.models.User",
|
|
3
|
+
"data": [
|
|
4
|
+
{
|
|
5
|
+
"firstname": "Kaanari",
|
|
6
|
+
"lastname": "Tech",
|
|
7
|
+
"fullname": "Kaanari Tech",
|
|
8
|
+
"phone": "",
|
|
9
|
+
"email": "admin@kaanari.com",
|
|
10
|
+
"email_verified": true,
|
|
11
|
+
"password": "$pbkdf2-sha256$29000$ubcWorT2njOmtLY25ty71w$Yo/aLAd5AxAolsTjDoOqtkvUvUWD6TErRR7.tCsMQ8g"
|
|
12
|
+
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from backend.database.db_postgres import drop_all_tables, get_db
|
|
4
|
+
import fire
|
|
5
|
+
from sqlalchemyseed import load_entities_from_json
|
|
6
|
+
from sqlalchemyseed import Seeder
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def drop_tables() -> None:
|
|
11
|
+
drop_all_tables()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def seed() -> None:
|
|
15
|
+
print("start: import_seed")
|
|
16
|
+
db = next(get_db())
|
|
17
|
+
seeds_json_files = list(Path(__file__).parent.glob("json/*.json"))
|
|
18
|
+
try:
|
|
19
|
+
entities = []
|
|
20
|
+
for file in seeds_json_files:
|
|
21
|
+
print(f"load seed file={str(file)}")
|
|
22
|
+
entities.append(load_entities_from_json(str(file)))
|
|
23
|
+
|
|
24
|
+
seeder = Seeder(db)
|
|
25
|
+
seeder.seed(entities)
|
|
26
|
+
db.commit()
|
|
27
|
+
print("end: seeds import completed")
|
|
28
|
+
except Exception as e:
|
|
29
|
+
db.rollback()
|
|
30
|
+
print(f"end: seeds import failed. detail={e}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
fire.Fire()
|
|
Binary file
|