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,98 @@
1
+ import casbin
2
+ import casbin_async_sqlalchemy_adapter
3
+
4
+ from fastapi import Depends, Request
5
+
6
+ from backend.models.casbin_rule import CasbinRule
7
+ from backend.common.enums import MethodType
8
+ from backend.common.exception.errors import AuthorizationError, TokenError
9
+ from backend.common.security.jwt import DependsJwtAuth
10
+ from backend.core.conf import settings
11
+ from backend.database.db_postgres import async_engine
12
+
13
+
14
+ class RBAC:
15
+ @staticmethod
16
+ async def enforcer() -> casbin.AsyncEnforcer:
17
+ """
18
+ Get the Casbin enforcer
19
+
20
+ :return:
21
+ """
22
+ # Rule data is defined directly in the method as static data
23
+ _CASBIN_RBAC_MODEL_CONF_TEXT = """
24
+ [request_definition]
25
+ r = sub, obj, act
26
+
27
+ [policy_definition]
28
+ p = sub, obj, act
29
+
30
+ [role_definition]
31
+ g = _, _
32
+
33
+ [policy_effect]
34
+ e = some(where (p.eft == allow))
35
+
36
+ [matchers]
37
+ m = g(r.sub, p.sub) && (keyMatch(r.obj, p.obj) || keyMatch3(r.obj, p.obj)) && (r.act == p.act || p.act == "*")
38
+ """
39
+ adapter = casbin_async_sqlalchemy_adapter.Adapter(async_engine, db_class=CasbinRule)
40
+ model = casbin.AsyncEnforcer.new_model(text=_CASBIN_RBAC_MODEL_CONF_TEXT)
41
+ enforcer = casbin.AsyncEnforcer(model, adapter)
42
+ await enforcer.load_policy()
43
+ return enforcer
44
+
45
+ async def rbac_verify(self, request: Request, _token: str = DependsJwtAuth) -> None:
46
+ """
47
+ RBAC permission verification
48
+
49
+ :param request:
50
+ :param _token:
51
+ :return:
52
+ """
53
+ path = request.url.path
54
+
55
+ # Whitelist for authentication
56
+ if path in settings.TOKEN_REQUEST_PATH_EXCLUDE:
57
+ return
58
+
59
+ # Enforce JWT authorization status check
60
+ if not request.auth.scopes:
61
+ raise TokenError
62
+
63
+ # Superuser exemption from verification
64
+ if request.user.is_superuser:
65
+ return
66
+
67
+ # Check role data permission scope
68
+ user_roles = request.user.roles
69
+ if not user_roles:
70
+ raise AuthorizationError(msg='User has no assigned roles, authorization failed')
71
+
72
+ method = request.method
73
+ if method != MethodType.GET or method != MethodType.OPTIONS:
74
+ if not request.user.is_staff:
75
+ raise AuthorizationError(msg='This user is prohibited from performing backend management operations')
76
+
77
+ # Data permission scope
78
+ data_scope = any(role.data_scope == 1 for role in user_roles)
79
+ if data_scope:
80
+ return
81
+
82
+
83
+ for role in user_roles:
84
+ # Casbin permission verification
85
+ if (method, path) in settings.RBAC_CASBIN_EXCLUDE:
86
+ return
87
+ enforcer = await self.enforcer()
88
+
89
+ # check if at least one of the user is allowed
90
+ if enforcer.enforce(role.x_id, path, method):
91
+ return
92
+
93
+ raise AuthorizationError
94
+
95
+
96
+ rbac = RBAC()
97
+ # RBAC authorization dependency injection
98
+ DependsRBAC = Depends(rbac.rbac_verify)
@@ -0,0 +1,6 @@
1
+ import secrets
2
+
3
+
4
+ def generate_secret_token() -> str:
5
+ token = secrets.token_urlsafe()
6
+ return token
@@ -0,0 +1,11 @@
1
+ # from backend.common.socketio.server import sio
2
+
3
+
4
+ # async def task_notification(msg: str):
5
+ # """
6
+ # Send notification
7
+
8
+ # :param msg:
9
+ # :return:
10
+ # """
11
+ # await sio.emit('task_notification', {'msg': msg})
@@ -0,0 +1,50 @@
1
+ # import socketio
2
+
3
+ # from backend.core.conf import settings
4
+ # from backend.common.security.jwt import jwt_authentication
5
+ # from backend.common.log import log
6
+
7
+ # sio = socketio.AsyncServer(
8
+ # client_manager=socketio.AsyncRedisManager(
9
+ # f'redis://:{settings.REDIS_PASSWORD}@{settings.REDIS_HOST}:'
10
+ # f'{settings.REDIS_PORT}/{task_settings.CELERY_BROKER_REDIS_DATABASE}'
11
+ # )
12
+ # if task_settings.CELERY_BROKER == 'redis'
13
+ # else socketio.AsyncAioPikaManager(
14
+ # (
15
+ # f'amqp://{task_settings.RABBITMQ_USERNAME}:{task_settings.RABBITMQ_PASSWORD}@'
16
+ # f'{task_settings.RABBITMQ_HOST}:{task_settings.RABBITMQ_PORT}'
17
+ # )
18
+ # ),
19
+ # async_mode='asgi',
20
+ # cors_allowed_origins=settings.CORS_ALLOWED_ORIGINS,
21
+ # cors_credentials=True,
22
+ # namespaces=['/ws'],
23
+ # )
24
+
25
+ # @sio.event
26
+ # async def connect(sid, environ, auth):
27
+ # if not auth:
28
+ # print('ws connection failed: no authorization')
29
+ # return False
30
+
31
+ # token = auth.get('token')
32
+ # if not token:
33
+ # print('ws connection failed: no token authorization')
34
+ # return False
35
+
36
+ # if token == 'internal':
37
+ # return True
38
+
39
+ # try:
40
+ # await jwt_authentication(token)
41
+ # except Exception as e:
42
+ # log.info(f'ws Connection failed: {e}')
43
+ # return False
44
+
45
+ # return True
46
+
47
+
48
+ # @sio.event
49
+ # async def disconnect(sid):
50
+ # pass
@@ -0,0 +1,69 @@
1
+ from typing import Any
2
+ from typing import List
3
+
4
+ from httpx import AsyncClient
5
+
6
+ from backend.app.admin.schema.sso import OAuthCodeResponseSchema
7
+ from backend.app.admin.schema.sso import OAuthRedirectLink
8
+ from backend.app.admin.schema.sso import OAuthTokenResponseSchema
9
+ from backend.app.admin.schema.sso import OAuthUserDataResponseSchema
10
+
11
+
12
+ class OAuthBase:
13
+ session: AsyncClient
14
+ client_id: str
15
+ secret_key: str
16
+ webhook_redirect_uri: str
17
+
18
+ scope: List[str]
19
+ response_type: str = "code"
20
+
21
+ def __init__(
22
+ self,
23
+ session: AsyncClient,
24
+ client_id: str,
25
+ secret_key: str,
26
+ webhook_redirect_uri: str,
27
+ ) -> None:
28
+ self.session = session
29
+ self.client_id = client_id
30
+ self.secret_key = secret_key
31
+ self.webhook_redirect_uri = webhook_redirect_uri
32
+
33
+ def scope_to_str(self, delimiter: str = "%20") -> str:
34
+ """
35
+ Convert a scope list to string representation
36
+ Replacing spaces with encoded spaces% 20 for pydantic model validation
37
+ """
38
+
39
+ return f"{delimiter}".join(self.scope)
40
+
41
+ def prepare_user_data(
42
+ self, external_id: str, user_data: dict[Any, Any]
43
+ ) -> OAuthUserDataResponseSchema:
44
+ """Converting interface socials for the general data format of the system"""
45
+
46
+ raise NotImplementedError
47
+
48
+ def generate_link_for_code(self) -> OAuthRedirectLink:
49
+ """
50
+ Generating a link to a redirect to the service to receive a confirmation code.
51
+ It is necessary for the user to further enter the service and
52
+ receive a confirmation code from the service on Webhook.
53
+ """
54
+
55
+ raise NotImplementedError
56
+
57
+ async def get_token(
58
+ self, code: OAuthCodeResponseSchema
59
+ ) -> OAuthTokenResponseSchema:
60
+ """Exchange of a confirmation code for a user token."""
61
+
62
+ raise NotImplementedError
63
+
64
+ async def get_user_data(
65
+ self, token: OAuthTokenResponseSchema
66
+ ) -> OAuthUserDataResponseSchema:
67
+ """ "Getting information about a user through an access token."""
68
+
69
+ raise NotImplementedError
@@ -0,0 +1,127 @@
1
+ from typing import Any
2
+
3
+ from fastapi import HTTPException
4
+ from fastapi import status
5
+ from httpx import AsyncClient
6
+ from pydantic_core import Url
7
+
8
+ from .base import OAuthBase
9
+ from backend.core.conf import settings
10
+ from backend.app.admin.schema.sso import OAuthCodeResponseSchema
11
+ from backend.app.admin.schema.sso import OAuthRedirectLink
12
+ from backend.app.admin.schema.sso import OAuthTokenResponseSchema
13
+ from backend.app.admin.schema.sso import OAuthUserDataResponseSchema
14
+ from backend.app.admin.schema.sso import SocialTypes
15
+
16
+
17
+ class GoogleOAuth(OAuthBase):
18
+ """
19
+ Config Google
20
+ https://developers.google.com/identity/protocols/oauth2/web-server#httprest_3
21
+ """
22
+
23
+ scope = [
24
+ "openid",
25
+ "https://www.googleapis.com/auth/userinfo.email",
26
+ "https://www.googleapis.com/auth/userinfo.profile",
27
+ ]
28
+
29
+ access_type = "offline"
30
+ grand_type = "authorization_code"
31
+
32
+ def generate_body_for_access_token(self, code: OAuthCodeResponseSchema) -> str:
33
+ """
34
+ Generating the request body to send to the service to receive the user's token.
35
+ """
36
+
37
+ return (
38
+ f"code={code.code}&"
39
+ f"client_id={self.client_id}&"
40
+ f"client_secret={self.secret_key}&"
41
+ f"grant_type={self.grand_type}&"
42
+ f"redirect_uri={self.webhook_redirect_uri}"
43
+ )
44
+
45
+ def prepare_user_data(
46
+ self, external_id: str, user_data: dict[Any, Any]
47
+ ) -> OAuthUserDataResponseSchema:
48
+ """Converting interface socials for the general data format of the system"""
49
+ return OAuthUserDataResponseSchema(
50
+ external_id=external_id,
51
+ email=user_data["email"],
52
+ social_type=SocialTypes.google,
53
+ img=user_data["picture"],
54
+ firstname=user_data["family_name"] if "family_name" in user_data else "",
55
+ lastname=user_data["given_name"] if "given_name" in user_data else "",
56
+ )
57
+
58
+ def generate_link_for_code(self) -> OAuthRedirectLink:
59
+ """
60
+ Generating a link to a redirect to the service to receive a confirmation code.
61
+ It is necessary for the user to further enter the service
62
+ and receive a confirmation code from the service on Webhook.
63
+ """
64
+
65
+ url = (
66
+ "https://accounts.google.com/o/oauth2/v2/auth?"
67
+ f"scope={self.scope_to_str()}&"
68
+ f"access_type={self.access_type}&"
69
+ f"response_type={self.response_type}&"
70
+ f"redirect_uri={self.webhook_redirect_uri}&"
71
+ f"client_id={self.client_id}"
72
+ )
73
+
74
+ return OAuthRedirectLink(url=Url(url=url))
75
+
76
+ async def get_token(
77
+ self, code: OAuthCodeResponseSchema
78
+ ) -> OAuthTokenResponseSchema:
79
+ """Exchange of a confirmation code for a user token."""
80
+
81
+ response = await self.session.post(
82
+ url="https://oauth2.googleapis.com/token",
83
+ content=self.generate_body_for_access_token(code),
84
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
85
+ )
86
+ token_data = response.json()
87
+ if response.status_code != status.HTTP_200_OK:
88
+ raise HTTPException(
89
+ status_code=status.HTTP_401_UNAUTHORIZED,
90
+ detail=token_data["error_description"],
91
+ )
92
+ token_data = response.json()
93
+
94
+ return OAuthTokenResponseSchema(token=token_data["id_token"])
95
+
96
+ async def get_user_data(
97
+ self, token: OAuthTokenResponseSchema
98
+ ) -> OAuthUserDataResponseSchema:
99
+ """ "Getting information about a user through an access token."""
100
+
101
+ response = await self.session.get(
102
+ url="https://oauth2.googleapis.com/tokeninfo",
103
+ params=dict(id_token=token.token),
104
+ )
105
+ user_data = response.json()
106
+ if response.status_code != status.HTTP_200_OK:
107
+ raise HTTPException(
108
+ status_code=status.HTTP_401_UNAUTHORIZED,
109
+ detail=user_data["error_description"],
110
+ )
111
+
112
+ return self.prepare_user_data(user_data["sub"], user_data)
113
+
114
+ async def verify_and_process(
115
+ self, code: OAuthCodeResponseSchema
116
+ ) -> OAuthUserDataResponseSchema:
117
+ token = await self.get_token(code=code)
118
+ datas = await self.get_user_data(token=token)
119
+ return datas
120
+
121
+
122
+ google_oauth = GoogleOAuth(
123
+ session=AsyncClient(),
124
+ client_id=settings.GOOGLE_CLIENT_ID,
125
+ secret_key=settings.GOOGLE_SECRET_KEY,
126
+ webhook_redirect_uri=settings.GOOGLE_WEBHOOK_OAUTH_REDIRECT_URI,
127
+ )
@@ -0,0 +1,208 @@
1
+ from functools import lru_cache
2
+ import os
3
+ from typing import Literal
4
+
5
+ from pydantic_settings import BaseSettings, SettingsConfigDict
6
+
7
+ from backend.core.path_conf import ApiV2Path
8
+
9
+
10
+ class Settings(BaseSettings):
11
+ """Global settings.
12
+
13
+ Every field has a sane development default so the app boots out of the box
14
+ (e.g. ``docker-run.sh up`` with the bundled docker-compose). Override via the
15
+ ``.env`` file for your own setup, and ALWAYS set real secrets in production.
16
+ """
17
+
18
+ model_config = SettingsConfigDict(
19
+ env_file=f'{ApiV2Path}/.env', env_file_encoding='utf-8', extra='ignore'
20
+ )
21
+
22
+ APP_NAME: str = 'shaapi'
23
+
24
+ # Env Config
25
+ ENVIRONMENT: Literal['dev', 'preprod', 'prod'] = 'dev'
26
+
27
+ # Database schema bootstrap: in dev, tables are auto-created on startup for a
28
+ # zero-friction first run. In production set DB_AUTO_CREATE=false and rely on
29
+ # Alembic migrations (`shaapi makemigrations` / `shaapi migrate`).
30
+ DB_AUTO_CREATE: bool = True
31
+
32
+ # Observability (opt-in). When disabled, the OpenTelemetry/Prometheus stack
33
+ # is never imported, keeping the lean core lightweight.
34
+ OBSERVABILITY_ENABLED: bool = False
35
+ OTLP_GRPC_ENDPOINT: str | None = None
36
+
37
+ # Postgres
38
+ POSTGRES_HOST: str = 'localhost'
39
+ POSTGRES_PORT: int = 5432
40
+ POSTGRES_USER: str = 'postgres'
41
+ POSTGRES_PASSWORD: str = 'postgres'
42
+ POSTGRES_ECHO: bool = False
43
+ POSTGRES_DATABASE: str = 'shaapi'
44
+
45
+ CLIENT_URL: str = 'http://localhost:3000'
46
+
47
+ # Redis
48
+ REDIS_HOST: str = 'localhost'
49
+ REDIS_PORT: int = 6379
50
+ REDIS_PASSWORD: str | None = None
51
+ REDIS_DATABASE: int = 0
52
+ REDIS_TIMEOUT: int = 5
53
+
54
+ # Object storage (MinIO / S3-compatible)
55
+ MINIO_ENDPOINT: str = 'localhost:9000'
56
+ MINIO_PORT: int = 9000
57
+ MINIO_ACCESS_KEY: str = 'minioadmin'
58
+ MINIO_SECRET_KEY: str = 'minioadmin'
59
+ MINIO_BUCKET_NAME: str = 'shaapi'
60
+ MINIO_CLOUD_URL: str = 'http://localhost:9000'
61
+
62
+ # SMTP / email (optional: leave blank to disable outgoing mail)
63
+ SMTP_TLS: str = 'True'
64
+ SMTP_PORT: str = '587'
65
+ SMTP_HOST: str = ''
66
+ SMTP_USER: str = ''
67
+ EMAILS_FROM_EMAIL: str = ''
68
+ EMAILS_FROM_NAME: str = ''
69
+ SMTP_PASSWORD: str = ''
70
+ EMAIL_TEMPLATES_DIR: str = os.getcwd() + '/templates/build'
71
+
72
+ # Token / crypto secrets — CHANGE THESE IN PRODUCTION
73
+ # Generate with: python -c "import secrets; print(secrets.token_urlsafe(32))"
74
+ TOKEN_SECRET_KEY: str = 'dev-insecure-change-me-token-secret-key'
75
+ # Generate with: python -c "import os; print(os.urandom(32).hex())"
76
+ OPERA_LOG_ENCRYPT_SECRET_KEY: str = 'dev-insecure-change-me-opera-log-key'
77
+
78
+ # Google SSO (optional)
79
+ GOOGLE_CLIENT_ID: str = ''
80
+ GOOGLE_SECRET_KEY: str = ''
81
+ GOOGLE_WEBHOOK_OAUTH_REDIRECT_URI: str = ''
82
+
83
+ # FastAPI
84
+ FASTAPI_API_V1_PATH: str = '/api/v1'
85
+ FASTAPI_TITLE: str = 'shaapi'
86
+ FASTAPI_VERSION: str = '0.0.1'
87
+ FASTAPI_DESCRIPTION: str = 'A lean, batteries-included FastAPI backend.'
88
+ FASTAPI_DOCS_URL: str | None = f'{FASTAPI_API_V1_PATH}/docs'
89
+ FASTAPI_REDOCS_URL: str | None = f'{FASTAPI_API_V1_PATH}/redocs'
90
+ FASTAPI_OPENAPI_URL: str | None = f'{FASTAPI_API_V1_PATH}/openapi'
91
+ FASTAPI_STATIC_FILES: bool = False
92
+
93
+ # Token
94
+ TOKEN_ALGORITHM: str = 'HS256'
95
+ TOKEN_EXPIRE_SECONDS: int = 60 * 60 * 24 * 1 # access token lifetime
96
+ TOKEN_REFRESH_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7 # refresh token lifetime
97
+ TOKEN_REDIS_PREFIX: str = 'shaapi:token'
98
+ USER_SECURE_TOKEN_REDIS_PREFIX: str = 'shaapi:user:token'
99
+ ADMIN_SECURE_TOKEN_REDIS_PREFIX: str = 'shaapi:admin:token'
100
+ TOKEN_REFRESH_REDIS_PREFIX: str = 'shaapi:refresh_token'
101
+ CAPTCHA_LOGIN_REDIS_PREFIX: str = 'shaapi:captcha:login'
102
+ TOKEN_REQUEST_PATH_EXCLUDE: list[str] = [ # JWT / RBAC whitelist
103
+ f'/admin{FASTAPI_API_V1_PATH}/auth/login',
104
+ f'/admin{FASTAPI_API_V1_PATH}/auth/token/new',
105
+ ]
106
+
107
+ # JWT
108
+ JWT_USER_REDIS_PREFIX: str = 'shaapi:user'
109
+ JWT_ADMIN_REDIS_PREFIX: str = 'shaapi:admin'
110
+ JWT_USER_REDIS_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7
111
+
112
+ # Permission (RBAC)
113
+ PERMISSION_MODE: Literal['casbin', 'role-menu'] = 'casbin'
114
+ PERMISSION_REDIS_PREFIX: str = 'shaapi:permission'
115
+
116
+ # Casbin RBAC whitelist
117
+ RBAC_CASBIN_EXCLUDE: set[tuple[str, str]] = {
118
+ ('POST', f'/admin{FASTAPI_API_V1_PATH}/auth/logout'),
119
+ ('POST', f'/admin{FASTAPI_API_V1_PATH}/auth/token/new'),
120
+ }
121
+
122
+ # Role-Menu
123
+ RBAC_ROLE_MENU_EXCLUDE: list[str] = [
124
+ 'sys:monitor:redis',
125
+ 'sys:monitor:server',
126
+ ]
127
+
128
+ # Cookies
129
+ COOKIE_REFRESH_TOKEN_KEY: str = 'shaapi_refresh_token'
130
+ COOKIE_REFRESH_TOKEN_EXPIRE_SECONDS: int = TOKEN_REFRESH_EXPIRE_SECONDS
131
+
132
+ # Log
133
+ LOG_ROOT_LEVEL: str = 'NOTSET'
134
+ LOG_STD_FORMAT: str = (
135
+ '<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</> | <lvl>{level: <8}</> | '
136
+ '<cyan> {correlation_id} </> | <lvl>{message}</>'
137
+ )
138
+ LOG_LOGURU_FORMAT: str = (
139
+ '<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</> | <lvl>{level: <8}</> | '
140
+ '<cyan> {correlation_id} </> | <lvl>{message}</>'
141
+ )
142
+ LOG_CID_DEFAULT_VALUE: str = '-'
143
+ LOG_CID_UUID_LENGTH: int = 32 # must <= 32
144
+ LOG_STDOUT_LEVEL: str = 'INFO'
145
+ LOG_STDERR_LEVEL: str = 'ERROR'
146
+ LOG_STDOUT_FILENAME: str = 'shaapi_access.log'
147
+ LOG_STDERR_FILENAME: str = 'shaapi_error.log'
148
+
149
+ # Middleware
150
+ MIDDLEWARE_CORS: bool = True
151
+ MIDDLEWARE_ACCESS: bool = True
152
+
153
+ # Trace ID
154
+ TRACE_ID_REQUEST_HEADER_KEY: str = 'X-Request-ID'
155
+
156
+ # CORS
157
+ CORS_ALLOWED_ORIGINS: list[str] = [
158
+ 'http://localhost:3000', # Front-end address, no trailing '/'
159
+ ]
160
+ CORS_EXPOSE_HEADERS: list[str] = [
161
+ TRACE_ID_REQUEST_HEADER_KEY,
162
+ ]
163
+
164
+ # DateTime
165
+ DATETIME_TIMEZONE: str = 'Africa/Abidjan'
166
+ DATETIME_FORMAT: str = '%Y-%m-%d %H:%M:%S'
167
+
168
+ # Request limiter
169
+ REQUEST_LIMITER_REDIS_PREFIX: str = 'shaapi:limiter'
170
+
171
+ # Demo mode (only GET, OPTIONS requests are allowed)
172
+ DEMO_MODE: bool = False
173
+ DEMO_MODE_EXCLUDE: set[tuple[str, str]] = {
174
+ ('POST', f'{FASTAPI_API_V1_PATH}/auth/login'),
175
+ ('POST', f'{FASTAPI_API_V1_PATH}/auth/logout'),
176
+ ('GET', f'{FASTAPI_API_V1_PATH}/auth/captcha'),
177
+ }
178
+
179
+ # IP location
180
+ IP_LOCATION_PARSE: Literal['online', 'offline', 'false'] = 'online'
181
+ IP_LOCATION_REDIS_PREFIX: str = 'shaapi:ip:location'
182
+ IP_LOCATION_EXPIRE_SECONDS: int = 60 * 60 * 24 * 1
183
+
184
+ # Opera log
185
+ OPERA_LOG_PATH_EXCLUDE: list[str] = [
186
+ '/favicon.ico',
187
+ FASTAPI_DOCS_URL,
188
+ FASTAPI_REDOCS_URL,
189
+ FASTAPI_OPENAPI_URL,
190
+ f'{FASTAPI_API_V1_PATH}/auth/login/swagger',
191
+ ]
192
+ OPERA_LOG_ENCRYPT_TYPE: int = 1 # 0: AES; 1: md5; 2: ItsDangerous; 3: plain; other: ******
193
+ OPERA_LOG_ENCRYPT_KEY_INCLUDE: list[str] = [ # values to encrypt in request bodies
194
+ 'password',
195
+ 'old_password',
196
+ 'new_password',
197
+ 'confirm_password',
198
+ ]
199
+
200
+
201
+ @lru_cache
202
+ def get_settings() -> Settings:
203
+ """Return the cached global settings instance."""
204
+ return Settings()
205
+
206
+
207
+ # Configuration instance
208
+ settings = get_settings()
@@ -0,0 +1,24 @@
1
+ import os
2
+
3
+ from pathlib import Path
4
+
5
+ # Get the project root directory
6
+ # Or use an absolute path to the backend directory, e.g. windows: BasePath = D:\git_project\fastapi_mysql\backend
7
+ BasePath = Path(__file__).resolve().parent.parent
8
+
9
+ ApiV2Path = Path(__file__).resolve().parent.parent.parent
10
+
11
+ # alembic migration file storage path
12
+ ALEMBIC_Versions_DIR = os.path.join(BasePath, 'alembic', 'versions')
13
+
14
+ # Log file path
15
+ LOG_DIR = os.path.join(BasePath, 'log')
16
+
17
+ # Offline IP Database Path
18
+ IP2REGION_XDB = os.path.join(BasePath, 'static', 'ip2region.xdb')
19
+
20
+ # Mount the static directory
21
+ STATIC_DIR = os.path.join(BasePath, 'static')
22
+
23
+ # jinja2 template file path
24
+ JINJA2_TEMPLATE_DIR = os.path.join(BasePath, 'templates')