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,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()