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,267 @@
1
+ from fast_captcha import text_captcha
2
+ from sqlalchemy import and_, desc, select
3
+ from sqlalchemy.ext.asyncio import AsyncSession
4
+ from sqlalchemy.sql import Select
5
+
6
+ from backend.models import User
7
+ from backend.crud.crud_role import role_dao
8
+ from backend.app.admin.schema.user import (
9
+ UserRegister,
10
+ UserUpdate
11
+ )
12
+ from backend.common.security.jwt import get_hash_password
13
+ from backend.common.enums import Role as Role_enum
14
+ from backend.utils.timezone import timezone
15
+ from backend.crud.crud_base import CRUDBase
16
+
17
+
18
+ class CRUDUser(CRUDBase[User]):
19
+ async def get(self, db: AsyncSession, admin_id: int) -> User | None:
20
+ """
21
+ Getting admin
22
+
23
+ :param db:
24
+ :param admin_id:
25
+ :return:
26
+ """
27
+ return await self.select_model(db, admin_id)
28
+
29
+ async def get_by_x_id(self, db: AsyncSession, x_id: str, populates: list = []) -> User | None:
30
+ """
31
+ Get admin by email
32
+
33
+ :param db:
34
+ :param email:
35
+ :return:
36
+ """
37
+ return await self.select_model_by_column(db, x_id=x_id, populates=populates)
38
+
39
+ async def get_by_email(self, db: AsyncSession, email: str, populates: list = []) -> User | None:
40
+ """
41
+ Get admin by email
42
+
43
+ :param db:
44
+ :param email:
45
+ :return:
46
+ """
47
+ return await self.select_model_by_column(db, email=email, populates=populates)
48
+
49
+ async def get_by_phone(self, db: AsyncSession, phone: str) -> User | None:
50
+ """
51
+ Get admin by phone
52
+
53
+ :param db:
54
+ :param phone:
55
+ :return:
56
+ """
57
+ return await self.select_model_by_column(db, phone=phone, populates=['roles'])
58
+
59
+ async def get_by_pseudo(self, db: AsyncSession, pseudo: str) -> User | None:
60
+ """
61
+ Get admin by pseudo
62
+
63
+ :param db:
64
+ :param pseudo:
65
+ :return:
66
+ """
67
+ return await self.select_model_by_column(db, pseudo=pseudo, populates=['roles'])
68
+
69
+ async def update_login_time(self, db: AsyncSession, email: str) -> int:
70
+ """
71
+ Update login time
72
+
73
+ :param db:
74
+ :param email:
75
+ :return:
76
+ """
77
+ return await self.update_model_by_column(db, {'last_login_time': timezone.now()}, email=email)
78
+
79
+ async def create(self, db: AsyncSession, obj: UserRegister, *, social: bool = False) -> None:
80
+ """
81
+ Create User
82
+
83
+ :param db:
84
+ :param obj:
85
+ :param social: Social admins, adapted to oauth 2.0
86
+ :return:
87
+ """
88
+ if not social:
89
+ salt = text_captcha(5)
90
+ obj.password = get_hash_password(f'{obj.password}{salt}')
91
+ dict_obj = obj.model_dump()
92
+ dict_obj.update({'salt': salt})
93
+ else:
94
+ dict_obj = obj.model_dump()
95
+ dict_obj.update({'salt': None})
96
+
97
+ new_admin = self.model(**dict_obj)
98
+ await db.add(db, new_admin)
99
+
100
+ async def add(self, db: AsyncSession, obj: UserRegister) -> User:
101
+ """
102
+ Add admin
103
+
104
+ :param db:
105
+ :param obj:
106
+ :return:
107
+ """
108
+ salt = text_captcha(5)
109
+ obj.password = get_hash_password(f'{obj.password}{salt}')
110
+ dict_obj = obj.model_dump(exclude={'roles'})
111
+
112
+ dict_obj.update({'salt': salt})
113
+
114
+
115
+ new_admin = self.model(**dict_obj)
116
+
117
+
118
+ role = await role_dao.get_by_name(db, Role_enum.ADMIN.value)
119
+
120
+ role_list = []
121
+ if role is not None:
122
+ role_list.append(role)
123
+ new_admin.roles.extend(role_list)
124
+ db.add(new_admin)
125
+ await db.flush()
126
+ await db.refresh(new_admin)
127
+ return new_admin
128
+
129
+ async def update_admin_info(self, db: AsyncSession, input_admin: int, obj: UserUpdate) -> int:
130
+ """
131
+ Updating admin information
132
+
133
+ :param db:
134
+ :param input_admin:
135
+ :param obj:
136
+ :return:
137
+ """
138
+ return await self.update_model(db, input_admin, obj)
139
+
140
+ async def update_profile_image(self, db: AsyncSession, input_admin: int, profile_image: dict) -> int:
141
+ """
142
+ Update admin profile image
143
+
144
+ :param db:
145
+ :param input_admin:
146
+ :param avatar:
147
+ :return:
148
+ """
149
+ return await self.update_model(db, input_admin, {'profile_image': dict})
150
+
151
+ async def check_email(self, db: AsyncSession, email: str) -> User | None:
152
+ """
153
+ Check admin email
154
+
155
+ :param db:
156
+ :param email:
157
+ :return:
158
+ """
159
+ return await self.select_model_by_column(db, email=email)
160
+
161
+ async def reset_password(self, db: AsyncSession, pk: int, new_pwd: str) -> int:
162
+ """
163
+ Reset admin password
164
+
165
+ :param db:
166
+ :param pk:
167
+ :param new_pwd:
168
+ :return:
169
+ """
170
+ return await self.update_model(db, pk, {'password': new_pwd})
171
+
172
+ async def get_list(self, email: str = None, phone: str = None, status: bool = None, role: str | None = None) -> Select:
173
+ """
174
+ Get admin list
175
+
176
+ :param dept:
177
+ :param adminname:
178
+ :param phone:
179
+ :param status:
180
+ :return:
181
+ """
182
+ stmt = (
183
+ select(self.model)
184
+ .order_by(desc(self.model.join_time))
185
+ )
186
+ where_list = []
187
+ if email:
188
+ where_list.append(self.model.email.like(f'%{email}%'))
189
+ if phone:
190
+ where_list.append(self.model.phone.like(f'%{phone}%'))
191
+ if status is not None:
192
+ where_list.append(self.model.status == status)
193
+ if where_list:
194
+ stmt = stmt.where(and_(*where_list))
195
+ return stmt
196
+
197
+ async def get_status(self, db: AsyncSession, admin_id: int) -> int:
198
+ """
199
+ Get admin status
200
+
201
+ :param db:
202
+ :param admin_id:
203
+ :return:
204
+ """
205
+ admin = await self.get(db, admin_id)
206
+ return admin.status
207
+
208
+ async def get_multi_login(self, db: AsyncSession, admin_id: int) -> bool:
209
+ """
210
+ Get admin multipoint login status
211
+
212
+ :param db:
213
+ :param admin_id:
214
+ :return:
215
+ """
216
+ admin = await self.get(db, admin_id)
217
+ return admin.is_multi_login
218
+
219
+ async def set_status(self, db: AsyncSession, admin_id: int, status: bool) -> int:
220
+ """
221
+ Set admin account status
222
+
223
+ :param db:
224
+ :param admin_id:
225
+ :param status:
226
+ :return:
227
+ """
228
+ return await self.update_model(db, admin_id, {'status': status})
229
+
230
+ async def set_multi_login(self, db: AsyncSession, admin_id: int, multi_login: bool) -> int:
231
+ """
232
+ Set multi login
233
+
234
+ :param db:
235
+ :param admin_id:
236
+ :param multi_login:
237
+ :return:
238
+ """
239
+ return await self.update_model(db, admin_id, {'is_multi_login': multi_login})
240
+
241
+ async def get_with_relation(self, db: AsyncSession, *, id: int = None, x_id: str = None, email: str = None, populates: list = []) -> User | None:
242
+ """
243
+ Get admin and (roles)
244
+
245
+ :param db:
246
+ :param admin_id:
247
+ :param email:
248
+ :return:
249
+ """
250
+ stmt = (
251
+ select(self.model)
252
+ )
253
+
254
+ filters = []
255
+ if id:
256
+ filters.append(self.model.id == id)
257
+ if email:
258
+ filters.append(self.model.email == email)
259
+ if x_id:
260
+ filters.append(self.model.x_id == x_id)
261
+
262
+ stmt = self.get_with_relationship(stmt, populates=populates)
263
+ admin = await db.execute(stmt.where(*filters))
264
+ return admin.scalars().first()
265
+
266
+
267
+ user_dao: CRUDUser = CRUDUser(User)
File without changes
@@ -0,0 +1,125 @@
1
+ import sys
2
+
3
+ from typing import Annotated, Generator
4
+ from uuid import uuid4
5
+ from sqlalchemy import MetaData, create_engine
6
+
7
+ from fastapi import Depends
8
+ from sqlalchemy import URL
9
+ from sqlalchemy.orm import Session
10
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
11
+ from collections.abc import AsyncGenerator
12
+ from sqlalchemy import event
13
+
14
+
15
+ from backend.common.log import log
16
+ from backend.common.model import MappedBase
17
+ from backend.core.conf import settings
18
+ from sqlalchemy.orm import sessionmaker
19
+
20
+
21
+ SQLALCHEMY_DATABASE_URL = (
22
+ f"postgresql+asyncpg://{settings.POSTGRES_USER}:{settings.POSTGRES_PASSWORD}@{settings.POSTGRES_HOST}:"
23
+ f"{settings.POSTGRES_PORT}/{settings.POSTGRES_DATABASE}"
24
+ )
25
+
26
+ try:
27
+ engine = create_engine(
28
+ SQLALCHEMY_DATABASE_URL,
29
+ pool_pre_ping=True,
30
+ echo=False,
31
+ future=True,
32
+ )
33
+ session_factory = sessionmaker(autocommit=False, autoflush=False, bind=engine)
34
+ except Exception as e:
35
+ print(f"DB connection error. detail={e}")
36
+
37
+
38
+ def create_engine_and_session(url: str | URL):
39
+ try:
40
+ # database engine
41
+ engine = create_async_engine(
42
+ url, echo=settings.POSTGRES_ECHO, future=True, pool_pre_ping=True
43
+ )
44
+ # log.success('Database Connection Successful')
45
+ except Exception as e:
46
+ log.error("❌ Database link failure {}", e)
47
+ sys.exit()
48
+ else:
49
+ db_session = async_sessionmaker(
50
+ bind=engine, autoflush=False, expire_on_commit=False
51
+ )
52
+ return engine, db_session
53
+
54
+
55
+ async_engine, async_db_session = create_engine_and_session(SQLALCHEMY_DATABASE_URL)
56
+
57
+
58
+ async def get_async_db() -> AsyncGenerator[AsyncSession, None]:
59
+ """session generator"""
60
+ session = async_db_session()
61
+ try:
62
+ yield session
63
+ except Exception as se:
64
+ await session.rollback()
65
+ raise se
66
+ finally:
67
+ await session.close()
68
+
69
+
70
+ # Session Annotated
71
+ CurrentSession = Annotated[AsyncSession, Depends(get_async_db)]
72
+
73
+
74
+ def get_db() -> Generator[Session, None, None]:
75
+ """
76
+ Create a database session when accessing from an endpoint, using Depend
77
+ If there are no errors, validate.
78
+ If there is an error, go back and close in all cases.
79
+ """
80
+
81
+ db = None
82
+ try:
83
+ db = session_factory()
84
+ yield db
85
+ db.commit()
86
+ except Exception:
87
+ if db:
88
+ db.rollback()
89
+ finally:
90
+ if db:
91
+ db.close()
92
+
93
+
94
+ async def create_table():
95
+ """Creating Database Tables"""
96
+ async with async_engine.begin() as coon:
97
+ await coon.run_sync(MappedBase.metadata.create_all)
98
+
99
+
100
+ def uuid4_str() -> str:
101
+ """Database Engine UUID Type Compatibility Solution"""
102
+ return str(uuid4())
103
+
104
+
105
+ def drop_all_tables() -> None:
106
+ print("start: drop_all_tables")
107
+ """
108
+ Delete all tables, types, Roles, etc.
109
+ and return to initial state (development environment only)
110
+ """
111
+ if settings.ENVIRONMENT != "dev":
112
+ # Run only in local environnement
113
+ print("drop_all_table() should be run only in dev env.")
114
+ return
115
+
116
+ metadata = MetaData()
117
+ metadata.reflect(bind=engine)
118
+
119
+ for table_key in metadata.tables:
120
+ table = metadata.tables.get(table_key)
121
+ if table is not None:
122
+ print(f"Deleting {table_key} table")
123
+ metadata.drop_all(engine, [table], checkfirst=True)
124
+
125
+ print("end: drop_all_tables")
@@ -0,0 +1,62 @@
1
+ import sys
2
+
3
+ from redis.asyncio import Redis
4
+ from redis.exceptions import AuthenticationError, TimeoutError
5
+
6
+ from backend.common.log import log
7
+ from backend.core.conf import settings
8
+
9
+
10
+ class RedisCli(Redis):
11
+ def __init__(self):
12
+ super(RedisCli, self).__init__(
13
+ host=settings.REDIS_HOST,
14
+ port=settings.REDIS_PORT,
15
+ # password=settings.REDIS_PASSWORD,
16
+ db=settings.REDIS_DATABASE,
17
+ socket_timeout=settings.REDIS_TIMEOUT,
18
+ decode_responses=True, # Transcoding utf-8
19
+ )
20
+
21
+ async def open(self):
22
+ """
23
+ Trigger Initialization Connection
24
+
25
+ :return:
26
+ """
27
+ try:
28
+ await self.ping()
29
+ except TimeoutError:
30
+ log.error('❌ Database redis connection timeout')
31
+ sys.exit()
32
+ except AuthenticationError:
33
+ log.error('❌ Database redis connection authentication failed')
34
+ sys.exit()
35
+ except Exception as e:
36
+ log.error('❌ Database redis connection exception {}', e)
37
+ sys.exit()
38
+
39
+ async def delete_prefix(self, prefix: str, exclude: str | list = None):
40
+ """
41
+ Delete all keys with the specified prefix
42
+
43
+ :param prefix:
44
+ :param exclude:
45
+ :return:
46
+ """
47
+ keys = []
48
+ async for key in self.scan_iter(match=f'{prefix}*'):
49
+ if isinstance(exclude, str):
50
+ if key != exclude:
51
+ keys.append(key)
52
+ elif isinstance(exclude, list):
53
+ if key not in exclude:
54
+ keys.append(key)
55
+ else:
56
+ keys.append(key)
57
+ if keys:
58
+ await self.delete(*keys)
59
+
60
+
61
+ # Create a redis client instance
62
+ redis_client = RedisCli()
@@ -0,0 +1,19 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ cd backend
5
+
6
+ # Apply database migrations (idempotent: no-op if already up to date).
7
+ # Migrations are authored explicitly with `shaapi makemigrations`, never
8
+ # auto-generated at boot.
9
+ alembic upgrade head
10
+
11
+ # Live-reload in development (the source is bind-mounted by the dev compose).
12
+ # In production (ENVIRONMENT=prod) the baked-in code is served without reload.
13
+ RELOAD=""
14
+ if [ "${ENVIRONMENT:-dev}" = "dev" ]; then
15
+ RELOAD="--reload"
16
+ fi
17
+
18
+ # Start the API server (exec => uvicorn becomes PID 1 and receives signals)
19
+ exec uvicorn backend.main:app --host 0.0.0.0 --port 8000 $RELOAD
@@ -0,0 +1,18 @@
1
+ locale = {
2
+ "http_200": "Request successful",
3
+ "http_201": "Resource created successfully",
4
+ "http_202": "Request accepted but not yet completed",
5
+ "http_204": "Request successful but no content returned",
6
+ "http_400": "Bad request",
7
+ "http_401": "Unauthorized",
8
+ "http_403": "Forbidden",
9
+ "http_404": "Resource not found",
10
+ "http_410": "Resource permanently deleted",
11
+ "http_422": "Invalid request parameters",
12
+ "http_425": "Request cannot be processed due to server limitations",
13
+ "http_429": "Too many requests, server rate limit reached",
14
+ "http_500": "Internal server error",
15
+ "http_502": "Gateway error",
16
+ "http_503": "Service unavailable",
17
+ "http_504": "Gateway timeout"
18
+ }
@@ -0,0 +1,10 @@
1
+ locale = {
2
+ 'incorrect_credential': 'Incorrect email or password',
3
+ 'account_locked': 'This account has been locked out. Please contact the system administrator',
4
+ 'successful': 'Login Successful',
5
+ 'refresh_token_not_found': 'Refresh Token not found',
6
+ 'invalid_refresh_token': 'Refresh Token is invalid',
7
+ 'enpty_password': 'Empty password',
8
+ 'exist': 'this account already exist',
9
+ 'code_not_found': 'We can\'t proced this request, try again.',
10
+ }
@@ -0,0 +1,18 @@
1
+ locale = {
2
+ "http_200": "Requête réussie",
3
+ "http_201": "Ressource créée avec succès",
4
+ "http_202": "Requête acceptée mais pas encore terminée",
5
+ "http_204": "Requête réussie mais aucun contenu retourné",
6
+ "http_400": "Mauvaise requête",
7
+ "http_401": "Non autorisé",
8
+ "http_403": "Interdit",
9
+ "http_404": "Ressource non trouvée",
10
+ "http_410": "Ressource supprimée de façon permanente",
11
+ "http_422": "Paramètres de la requête non valides",
12
+ "http_425": "La requête ne peut pas être traitée en raison des limitations du serveur",
13
+ "http_429": "Trop de requêtes, limite de débit du serveur atteinte",
14
+ "http_500": "Erreur interne du serveur",
15
+ "http_502": "Erreur de passerelle",
16
+ "http_503": "Service indisponible",
17
+ "http_504": "Délai d'attente de la passerelle dépassé"
18
+ }
@@ -0,0 +1,10 @@
1
+ locale = {
2
+ 'incorrect_credential': 'Email ou mot de passe incorrect',
3
+ 'account_locked': 'Ce compte a été verrouillé. Veuillez contacter l\'administrateur du système',
4
+ 'successful': 'Connexion réussie',
5
+ 'refresh_token_not_found': 'Le Jeton de rafraîchissement est introuvable, veuillez vous reconnecter',
6
+ 'invalid_refresh_token': 'Le jeton de rafraîchissement est invalide',
7
+ 'enpty_password': 'Mot de passe vide',
8
+ 'exist': 'Ce compte existe déjà',
9
+ 'code_not_found': 'Nous ne pouvons traiter cette demande veuillez réessayer plus tard.',
10
+ }
@@ -0,0 +1,54 @@
1
+ """shaapi application entry point.
2
+
3
+ The top-level FastAPI app is a thin parent that mounts the feature
4
+ sub-applications (currently ``/admin``: auth, users, roles, RBAC...).
5
+ Each sub-app owns its own middleware, lifespan and OpenAPI docs.
6
+ """
7
+ import logging
8
+
9
+ import uvicorn
10
+ from fastapi import FastAPI
11
+
12
+ from backend.core.conf import settings
13
+ from backend.core.registrar import register_app, register_init
14
+ from backend.app.api import admin_router
15
+
16
+ # The parent app owns the lifespan (DB tables, Redis, rate limiter) because
17
+ # Starlette does not run the lifespan of mounted sub-applications.
18
+ app = FastAPI(
19
+ title=settings.FASTAPI_TITLE,
20
+ lifespan=register_init,
21
+ docs_url=None,
22
+ redoc_url=None,
23
+ openapi_url=None,
24
+ )
25
+
26
+ # Observability (opt-in). Enable with OBSERVABILITY_ENABLED=true and a valid
27
+ # OTLP_GRPC_ENDPOINT. Kept out of the lean core so the app boots without the
28
+ # OpenTelemetry/Prometheus stack installed.
29
+ if settings.OBSERVABILITY_ENABLED and settings.OTLP_GRPC_ENDPOINT:
30
+ from backend.utils.prometheus import EndpointFilter, metrics, setting_otlp
31
+
32
+ setting_otlp(app, settings.APP_NAME, settings.OTLP_GRPC_ENDPOINT)
33
+ logging.getLogger("uvicorn.access").addFilter(EndpointFilter())
34
+ app.add_route("/metrics", metrics)
35
+
36
+
37
+ @app.get("/health", tags=["health"])
38
+ async def health() -> dict:
39
+ """Liveness probe."""
40
+ return {"status": "ok"}
41
+
42
+
43
+ # Mount feature sub-applications
44
+ app.mount("/admin", register_app(admin_router, "admin"))
45
+
46
+
47
+ if __name__ == "__main__":
48
+ # Handy for IDE debugging. In Docker the entrypoint runs uvicorn directly.
49
+ uvicorn.run(
50
+ "backend.main:app",
51
+ host="0.0.0.0",
52
+ port=8000,
53
+ reload=settings.ENVIRONMENT == "dev",
54
+ )
@@ -0,0 +1,19 @@
1
+ from fastapi import Request, Response
2
+ from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
3
+
4
+ from backend.common.log import log
5
+ from backend.utils.timezone import timezone
6
+
7
+
8
+ class AccessMiddleware(BaseHTTPMiddleware):
9
+ """Request Log Middleware"""
10
+
11
+ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
12
+ start_time = timezone.now()
13
+ response = await call_next(request)
14
+ end_time = timezone.now()
15
+ log.info(
16
+ f'{request.client.host: <15} | {request.method: <8} | {response.status_code: <6} | '
17
+ f'{request.url.path} | {round((end_time - start_time).total_seconds(), 3) * 1000.0}ms'
18
+ )
19
+ return response
@@ -0,0 +1,19 @@
1
+ from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
2
+ from starlette.requests import Request
3
+
4
+
5
+ class I18nMiddleware(BaseHTTPMiddleware):
6
+ WHITE_LIST = ['en', 'fr']
7
+
8
+ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
9
+ # 1. headers 2. path 3. query string
10
+ locale = request.headers.get('locale', None) or \
11
+ request.path_params.get('locale', None) or \
12
+ request.query_params.get('locale', None) or \
13
+ 'fr'
14
+
15
+ if locale not in self.WHITE_LIST:
16
+ locale = 'fr'
17
+ request.state.locale = locale
18
+
19
+ return await call_next(request)