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