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,110 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from asgiref.sync import sync_to_async
|
|
5
|
+
from fastapi import Request
|
|
6
|
+
from user_agents import parse
|
|
7
|
+
from XdbSearchIP.xdbSearcher import XdbSearcher
|
|
8
|
+
|
|
9
|
+
from backend.common.dataclasses import IpInfo, UserAgentInfo
|
|
10
|
+
from backend.common.log import log
|
|
11
|
+
from backend.core.conf import settings
|
|
12
|
+
from backend.core.path_conf import IP2REGION_XDB
|
|
13
|
+
from backend.database.db_redis import redis_client
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_request_ip(request: Request) -> str:
|
|
17
|
+
"""Get the ip address of the request"""
|
|
18
|
+
real = request.headers.get('X-Real-IP')
|
|
19
|
+
if real:
|
|
20
|
+
ip = real
|
|
21
|
+
else:
|
|
22
|
+
forwarded = request.headers.get('X-Forwarded-For')
|
|
23
|
+
if forwarded:
|
|
24
|
+
ip = forwarded.split(',')[0]
|
|
25
|
+
else:
|
|
26
|
+
ip = request.client.host
|
|
27
|
+
if ip == 'testclient':
|
|
28
|
+
ip = '127.0.0.1'
|
|
29
|
+
return ip
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def get_location_online(ip: str, user_agent: str) -> dict | None:
|
|
33
|
+
"""
|
|
34
|
+
Obtain ip address attributes online, no guarantee of availability, higher accuracy
|
|
35
|
+
|
|
36
|
+
:param ip:
|
|
37
|
+
:param user_agent:
|
|
38
|
+
:return:
|
|
39
|
+
"""
|
|
40
|
+
async with httpx.AsyncClient(timeout=3) as client:
|
|
41
|
+
ip_api_url = f'http://ip-api.com/json/{ip}?lang=fr-FR'
|
|
42
|
+
headers = {'User-Agent': user_agent}
|
|
43
|
+
try:
|
|
44
|
+
response = await client.get(ip_api_url, headers=headers)
|
|
45
|
+
if response.status_code == 200:
|
|
46
|
+
return response.json()
|
|
47
|
+
except Exception as e:
|
|
48
|
+
log.error(f'Failed to obtain ip address attributes online, error message:{e}')
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@sync_to_async
|
|
53
|
+
def get_location_offline(ip: str) -> dict | None:
|
|
54
|
+
"""
|
|
55
|
+
Get ip address generically offline, can't guarantee accuracy, 100% available
|
|
56
|
+
|
|
57
|
+
:param ip:
|
|
58
|
+
:return:
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
cb = XdbSearcher.loadContentFromFile(dbfile=IP2REGION_XDB)
|
|
62
|
+
searcher = XdbSearcher(contentBuff=cb)
|
|
63
|
+
data = searcher.search(ip)
|
|
64
|
+
searcher.close()
|
|
65
|
+
data = data.split('|')
|
|
66
|
+
return {
|
|
67
|
+
'country': data[0] if data[0] != '0' else None,
|
|
68
|
+
'regionName': data[2] if data[2] != '0' else None,
|
|
69
|
+
'city': data[3] if data[3] != '0' else None,
|
|
70
|
+
}
|
|
71
|
+
except Exception as e:
|
|
72
|
+
log.error(f'Failed to obtain ip address generics offline, error message:{e}')
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def parse_ip_info(request: Request) -> IpInfo:
|
|
77
|
+
country, region, city = None, None, None
|
|
78
|
+
ip = get_request_ip(request)
|
|
79
|
+
location = await redis_client.get(f'{settings.IP_LOCATION_REDIS_PREFIX}:{ip}')
|
|
80
|
+
if location:
|
|
81
|
+
location = json.loads(location)
|
|
82
|
+
country = location.get("country")
|
|
83
|
+
region = location.get("region")
|
|
84
|
+
city = location.get("city")
|
|
85
|
+
return IpInfo(ip=ip, country=country, region=region, city=city)
|
|
86
|
+
if settings.IP_LOCATION_PARSE == 'online':
|
|
87
|
+
location_info = await get_location_online(ip, request.headers.get('User-Agent'))
|
|
88
|
+
elif settings.IP_LOCATION_PARSE == 'offline':
|
|
89
|
+
location_info = await get_location_offline(ip)
|
|
90
|
+
else:
|
|
91
|
+
location_info = None
|
|
92
|
+
if location_info:
|
|
93
|
+
country = location_info.get('country')
|
|
94
|
+
region = location_info.get('regionName')
|
|
95
|
+
city = location_info.get('city')
|
|
96
|
+
await redis_client.set(
|
|
97
|
+
f'{settings.IP_LOCATION_REDIS_PREFIX}:{ip}',
|
|
98
|
+
json.dumps({"country": country, "region": region, "city": city}),
|
|
99
|
+
ex=settings.IP_LOCATION_EXPIRE_SECONDS,
|
|
100
|
+
)
|
|
101
|
+
return IpInfo(ip=ip, country=country, region=region, city=city)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def parse_user_agent_info(request: Request) -> UserAgentInfo:
|
|
105
|
+
user_agent = request.headers.get('User-Agent')
|
|
106
|
+
_user_agent = parse(user_agent)
|
|
107
|
+
os = _user_agent.get_os()
|
|
108
|
+
browser = _user_agent.get_browser()
|
|
109
|
+
device = _user_agent.get_device()
|
|
110
|
+
return UserAgentInfo(user_agent=user_agent, device=device, os=os, browser=browser)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from typing import Any, Sequence, TypeVar
|
|
3
|
+
|
|
4
|
+
from fastapi.encoders import decimal_encoder
|
|
5
|
+
from msgspec import json
|
|
6
|
+
from sqlalchemy import Row, RowMapping
|
|
7
|
+
from sqlalchemy.orm import ColumnProperty, SynonymProperty, class_mapper
|
|
8
|
+
from starlette.responses import JSONResponse
|
|
9
|
+
|
|
10
|
+
RowData = Row | RowMapping | Any
|
|
11
|
+
|
|
12
|
+
R = TypeVar('R', bound=RowData)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def select_columns_serialize(row: R) -> dict:
|
|
16
|
+
"""
|
|
17
|
+
Serialize SQLAlchemy select table columns, does not contain relational columns
|
|
18
|
+
|
|
19
|
+
:param row:
|
|
20
|
+
:return:
|
|
21
|
+
"""
|
|
22
|
+
result = {}
|
|
23
|
+
for column in row.__table__.columns.keys():
|
|
24
|
+
v = getattr(row, column)
|
|
25
|
+
if isinstance(v, Decimal):
|
|
26
|
+
v = decimal_encoder(v)
|
|
27
|
+
result[column] = v
|
|
28
|
+
return result
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def select_list_serialize(row: Sequence[R]) -> list:
|
|
32
|
+
"""
|
|
33
|
+
Serialize SQLAlchemy select list
|
|
34
|
+
|
|
35
|
+
:param row:
|
|
36
|
+
:return:
|
|
37
|
+
"""
|
|
38
|
+
result = [select_columns_serialize(_) for _ in row]
|
|
39
|
+
return result
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def select_as_dict(row: R, use_alias: bool = False) -> dict:
|
|
43
|
+
"""
|
|
44
|
+
Converting SQLAlchemy select to dict, which can contain relational data,
|
|
45
|
+
depends on the properties of the select object itself
|
|
46
|
+
|
|
47
|
+
If set use_alias is True, the column name will be returned as alias,
|
|
48
|
+
If alias doesn't exist in columns, we don't recommend setting it to True
|
|
49
|
+
|
|
50
|
+
:param row:
|
|
51
|
+
:param use_alias:
|
|
52
|
+
:return:
|
|
53
|
+
"""
|
|
54
|
+
if not use_alias:
|
|
55
|
+
result = row.__dict__
|
|
56
|
+
if '_sa_instance_state' in result:
|
|
57
|
+
del result['_sa_instance_state']
|
|
58
|
+
return result
|
|
59
|
+
else:
|
|
60
|
+
result = {}
|
|
61
|
+
mapper = class_mapper(row.__class__)
|
|
62
|
+
for prop in mapper.iterate_properties:
|
|
63
|
+
if isinstance(prop, (ColumnProperty, SynonymProperty)):
|
|
64
|
+
key = prop.key
|
|
65
|
+
result[key] = getattr(row, key)
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class MsgSpecJSONResponse(JSONResponse):
|
|
70
|
+
"""
|
|
71
|
+
JSON response using the high-performance msgspec library to serialize data to JSON.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def render(self, content: Any) -> bytes:
|
|
75
|
+
return json.encode(content)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import zoneinfo
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from datetime import timezone as datetime_timezone
|
|
5
|
+
|
|
6
|
+
from backend.core.conf import settings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TimeZone:
|
|
10
|
+
def __init__(self, tz: str = settings.DATETIME_TIMEZONE):
|
|
11
|
+
self.tz_info = zoneinfo.ZoneInfo(tz)
|
|
12
|
+
|
|
13
|
+
def now(self) -> datetime:
|
|
14
|
+
"""
|
|
15
|
+
Get time zone time
|
|
16
|
+
|
|
17
|
+
:return:
|
|
18
|
+
"""
|
|
19
|
+
return datetime.now(self.tz_info)
|
|
20
|
+
|
|
21
|
+
def f_datetime(self, dt: datetime) -> datetime:
|
|
22
|
+
"""
|
|
23
|
+
datetime Time to Time Zone Time
|
|
24
|
+
|
|
25
|
+
:param dt:
|
|
26
|
+
:return:
|
|
27
|
+
"""
|
|
28
|
+
return dt.astimezone(self.tz_info)
|
|
29
|
+
|
|
30
|
+
def f_str(self, date_str: str, format_str: str = settings.DATETIME_FORMAT) -> datetime:
|
|
31
|
+
"""
|
|
32
|
+
Time string to time zone time
|
|
33
|
+
|
|
34
|
+
:param date_str:
|
|
35
|
+
:param format_str:
|
|
36
|
+
:return:
|
|
37
|
+
"""
|
|
38
|
+
return datetime.strptime(date_str, format_str).replace(tzinfo=self.tz_info)
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def f_utc(dt: datetime) -> datetime:
|
|
42
|
+
"""
|
|
43
|
+
Time Zone Time to UTC (GMT) Time Zone
|
|
44
|
+
|
|
45
|
+
:param dt:
|
|
46
|
+
:return:
|
|
47
|
+
"""
|
|
48
|
+
return dt.astimezone(datetime_timezone.utc)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
timezone = TimeZone()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Translator:
|
|
6
|
+
_instances: Dict[str, 'Translator'] = {}
|
|
7
|
+
|
|
8
|
+
def __new__(cls, lang: str) -> 'Translator':
|
|
9
|
+
if lang not in cls._instances:
|
|
10
|
+
cls._instances[lang] = super(Translator, cls).__new__(cls)
|
|
11
|
+
return cls._instances[lang]
|
|
12
|
+
|
|
13
|
+
def __init__(self, lang: str):
|
|
14
|
+
self.lang = lang
|
|
15
|
+
|
|
16
|
+
def t(self, key: str, **kwargs: Dict[str, Any]) -> str:
|
|
17
|
+
file_key, *translation_keys = key.split('.')
|
|
18
|
+
|
|
19
|
+
locale_module = importlib.import_module(f'backend.lang.{self.lang}.{file_key}')
|
|
20
|
+
|
|
21
|
+
translation = locale_module.locale
|
|
22
|
+
for translation_key in translation_keys:
|
|
23
|
+
translation = translation.get(translation_key, None)
|
|
24
|
+
if translation is None:
|
|
25
|
+
return f'Key {key} not found in {self.lang} locale'
|
|
26
|
+
if kwargs.keys():
|
|
27
|
+
translation = translation.format(**kwargs)
|
|
28
|
+
return translation
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
echo CI_JOB_TOKEN=$CI_JOB_TOKEN >> .env
|
|
2
|
+
echo CI_REGISTRY=$CI_REGISTRY >> .env
|
|
3
|
+
echo CI_REGISTRY_USER=$CI_REGISTRY_USER >> .env
|
|
4
|
+
|
|
5
|
+
echo VPS_IP_ADDRESS=$VPS_IP_ADDRESS >> .env
|
|
6
|
+
echo VPS_USER=$VPS_USER >> .env
|
|
7
|
+
echo API_IMAGE=$IMAGE:api >> .env
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
echo ENVIRONMENT=$ENVIRONMENT >> .env
|
|
11
|
+
echo TITLE=$TITLE >> .env
|
|
12
|
+
echo CLIENT_ROOT_PATH=$CLIENT_ROOT_PATH >> .env
|
|
13
|
+
echo ADMIN_ROOT_PATH=$ADMIN_ROOT_PATH >> .env
|
|
14
|
+
echo CLIENT_URL=$CLIENT_URL >> .env
|
|
15
|
+
echo API_BASE_URL=$API_BASE_URL >> .env
|
|
16
|
+
echo API_URL=$API_URL >> .env
|
|
17
|
+
echo ACCESS_TOKEN_EXPIRE_MINUTES=$ACCESS_TOKEN_EXPIRE_MINUTES >> .env
|
|
18
|
+
# POSTGRES
|
|
19
|
+
|
|
20
|
+
echo POSTGRES_HOST=$POSTGRES_HOST >> .env
|
|
21
|
+
echo POSTGRES_PORT=$POSTGRES_PORT >> .env
|
|
22
|
+
echo POSTGRES_USER=$POSTGRES_USER >> .env
|
|
23
|
+
echo POSTGRES_PASSWORD=$POSTGRES_PASSWORD >> .env
|
|
24
|
+
# Redis
|
|
25
|
+
echo REDIS_HOST=$REDIS_HOST >> .env
|
|
26
|
+
echo REDIS_PORT=$REDIS_PORT >> .env
|
|
27
|
+
echo REDIS_PASSWORD=$REDIS_PASSWORD >> .env
|
|
28
|
+
echo REDIS_DATABASE=$REDIS_DATABASE >> .env
|
|
29
|
+
# Token
|
|
30
|
+
echo TOKEN_SECRET_KEY=$TOKEN_SECRET_KEY >> .env
|
|
31
|
+
# Opera Log
|
|
32
|
+
echo OPERA_LOG_ENCRYPT_SECRET_KEY=$OPERA_LOG_ENCRYPT_SECRET_KEY >> .env
|
|
33
|
+
# Admin
|
|
34
|
+
# OAuth2
|
|
35
|
+
echo OAUTH2_GITHUB_CLIENT_ID=$OAUTH2_GITHUB_CLIENT_ID >> .env
|
|
36
|
+
echo OAUTH2_GITHUB_CLIENT_SECRET=$OAUTH2_GITHUB_CLIENT_SECRET >> .env
|
|
37
|
+
echo OAUTH2_LINUX_DO_CLIENT_ID=$OAUTH2_LINUX_DO_CLIENT_ID >> .env
|
|
38
|
+
echo OAUTH2_LINUX_DO_CLIENT_SECRET=$OAUTH2_LINUX_DO_CLIENT_SECRET >> .env
|
|
39
|
+
# Task
|
|
40
|
+
# Celery
|
|
41
|
+
echo CELERY_BROKER_REDIS_DATABASE=$CELERY_BROKER_REDIS_DATABASE >> .env
|
|
42
|
+
echo CELERY_BACKEND_REDIS_DATABASE=$CELERY_BACKEND_REDIS_DATABASE >> .env
|
|
43
|
+
# Rabbitmq
|
|
44
|
+
echo RABBITMQ_HOST=$RABBITMQ_HOST >> .env
|
|
45
|
+
echo RABBITMQ_PORT=$RABBITMQ_PORT >> .env
|
|
46
|
+
echo RABBITMQ_USERNAME=$RABBITMQ_USERNAME >> .env
|
|
47
|
+
echo RABBITMQ_PASSWORD=$RABBITMQ_PASSWORD >> .env
|
|
48
|
+
# Minio
|
|
49
|
+
echo MINIO_ENDPOINT=$MINIO_ENDPOINT >> .env
|
|
50
|
+
echo MINIO_PORT=$MINIO_PORT >> .env
|
|
51
|
+
echo MINIO_ACCESS_KEY=$MINIO_ACCESS_KEY >> .env
|
|
52
|
+
echo MINIO_SECRET_KEY=$MINIO_SECRET_KEY >> .env
|
|
53
|
+
echo MINIO_BUCKET_NAME=$MINIO_BUCKET_NAME >> .env
|
|
54
|
+
echo MINIO_CLOUD_URL=$MINIO_CLOUD_URL >> .env
|
|
55
|
+
|
|
56
|
+
echo SMTP_TLS=$SMTP_TLS >> .env
|
|
57
|
+
echo SMTP_PORT=$SMTP_PORT >> .env
|
|
58
|
+
echo SMTP_HOST=$SMTP_HOST >> .env
|
|
59
|
+
echo SMTP_USER=$SMTP_USER >> .env
|
|
60
|
+
echo EMAILS_FROM_EMAIL=$EMAILS_FROM_EMAIL >> .env
|
|
61
|
+
echo EMAILS_FROM_NAME=$EMAILS_FROM_NAME >> .env
|
|
62
|
+
echo SMTP_PASSWORD=$SMTP_PASSWORD >> .env
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Opt-in observability stack. Enable with: ./docker-run.sh up --monitoring
|
|
2
|
+
# Adds Prometheus (metrics), Tempo (traces), Loki (logs) and Grafana (UI),
|
|
3
|
+
# and rebuilds the API image with the `monitoring` extra + observability on.
|
|
4
|
+
services:
|
|
5
|
+
api:
|
|
6
|
+
build:
|
|
7
|
+
args:
|
|
8
|
+
EXTRAS: "--extra monitoring"
|
|
9
|
+
environment:
|
|
10
|
+
- OBSERVABILITY_ENABLED=true
|
|
11
|
+
- OTLP_GRPC_ENDPOINT=shaapi_tempo:4317
|
|
12
|
+
|
|
13
|
+
shaapi_prometheus:
|
|
14
|
+
image: prom/prometheus:v2.51.2
|
|
15
|
+
container_name: shaapi_prometheus
|
|
16
|
+
ports:
|
|
17
|
+
- "9090:9090"
|
|
18
|
+
volumes:
|
|
19
|
+
- ./etc/prometheus:/workspace
|
|
20
|
+
command:
|
|
21
|
+
- --config.file=/workspace/prometheus.yml
|
|
22
|
+
- --enable-feature=exemplar-storage
|
|
23
|
+
networks:
|
|
24
|
+
- shaapi
|
|
25
|
+
|
|
26
|
+
shaapi_tempo:
|
|
27
|
+
image: grafana/tempo:2.4.1
|
|
28
|
+
container_name: shaapi_tempo
|
|
29
|
+
command: ["--target=all", "--storage.trace.backend=local", "--storage.trace.local.path=/var/tempo", "--auth.enabled=false"]
|
|
30
|
+
ports:
|
|
31
|
+
- "4317:4317"
|
|
32
|
+
- "4318:4318"
|
|
33
|
+
networks:
|
|
34
|
+
- shaapi
|
|
35
|
+
|
|
36
|
+
shaapi_loki:
|
|
37
|
+
image: grafana/loki:3.0.0
|
|
38
|
+
container_name: shaapi_loki
|
|
39
|
+
command: -config.file=/etc/loki/local-config.yaml
|
|
40
|
+
ports:
|
|
41
|
+
- "3100:3100"
|
|
42
|
+
networks:
|
|
43
|
+
- shaapi
|
|
44
|
+
|
|
45
|
+
shaapi_grafana:
|
|
46
|
+
image: grafana/grafana:10.4.2
|
|
47
|
+
container_name: shaapi_grafana
|
|
48
|
+
ports:
|
|
49
|
+
- "3000:3000"
|
|
50
|
+
environment:
|
|
51
|
+
- GF_AUTH_ANONYMOUS_ENABLED=true
|
|
52
|
+
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
|
|
53
|
+
- GF_AUTH_DISABLE_LOGIN_FORM=true
|
|
54
|
+
volumes:
|
|
55
|
+
- ./etc/grafana/:/etc/grafana/provisioning/datasources
|
|
56
|
+
- ./etc/dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml
|
|
57
|
+
- ./etc/dashboards:/etc/grafana/dashboards
|
|
58
|
+
depends_on:
|
|
59
|
+
- shaapi_prometheus
|
|
60
|
+
- shaapi_tempo
|
|
61
|
+
- shaapi_loki
|
|
62
|
+
networks:
|
|
63
|
+
- shaapi
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Development overrides â automatically applied by `docker compose up`.
|
|
2
|
+
# Bind-mounts the source so code changes hot-reload (uvicorn --reload) WITHOUT
|
|
3
|
+
# rebuilding the image. The image is only rebuilt when dependencies change.
|
|
4
|
+
#
|
|
5
|
+
# For production, run the base file only (no mount, baked-in code):
|
|
6
|
+
# docker compose -f docker-compose.yml up -d
|
|
7
|
+
# and set ENVIRONMENT=prod in your .env.
|
|
8
|
+
services:
|
|
9
|
+
api:
|
|
10
|
+
volumes:
|
|
11
|
+
# Live source. The venv lives at /opt/venv, so it is not shadowed.
|
|
12
|
+
- .:/app
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
services:
|
|
2
|
+
api:
|
|
3
|
+
build:
|
|
4
|
+
context: .
|
|
5
|
+
dockerfile: Dockerfile
|
|
6
|
+
image: shaapi:latest
|
|
7
|
+
container_name: shaapi_api
|
|
8
|
+
restart: unless-stopped
|
|
9
|
+
ports:
|
|
10
|
+
- "8000:8000"
|
|
11
|
+
env_file:
|
|
12
|
+
- .env
|
|
13
|
+
environment:
|
|
14
|
+
# Point the app at the compose service names (overrides .env localhost)
|
|
15
|
+
- POSTGRES_HOST=shaapi_postgres
|
|
16
|
+
- REDIS_HOST=shaapi_redis
|
|
17
|
+
- MINIO_ENDPOINT=shaapi_minio:9000
|
|
18
|
+
depends_on:
|
|
19
|
+
postgres:
|
|
20
|
+
condition: service_healthy
|
|
21
|
+
redis:
|
|
22
|
+
condition: service_healthy
|
|
23
|
+
minio:
|
|
24
|
+
condition: service_started
|
|
25
|
+
networks:
|
|
26
|
+
- shaapi
|
|
27
|
+
|
|
28
|
+
postgres:
|
|
29
|
+
image: postgres:16-alpine
|
|
30
|
+
container_name: shaapi_postgres
|
|
31
|
+
restart: unless-stopped
|
|
32
|
+
environment:
|
|
33
|
+
- POSTGRES_USER=${POSTGRES_USER:-postgres}
|
|
34
|
+
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres}
|
|
35
|
+
- POSTGRES_DB=${POSTGRES_DATABASE:-shaapi}
|
|
36
|
+
volumes:
|
|
37
|
+
- shaapi_postgres:/var/lib/postgresql/data
|
|
38
|
+
ports:
|
|
39
|
+
- "${POSTGRES_PORT:-5432}:5432"
|
|
40
|
+
healthcheck:
|
|
41
|
+
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DATABASE:-shaapi}"]
|
|
42
|
+
interval: 5s
|
|
43
|
+
timeout: 5s
|
|
44
|
+
retries: 10
|
|
45
|
+
networks:
|
|
46
|
+
- shaapi
|
|
47
|
+
|
|
48
|
+
redis:
|
|
49
|
+
image: redis:7-alpine
|
|
50
|
+
container_name: shaapi_redis
|
|
51
|
+
restart: unless-stopped
|
|
52
|
+
volumes:
|
|
53
|
+
- shaapi_redis:/data
|
|
54
|
+
healthcheck:
|
|
55
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
56
|
+
interval: 5s
|
|
57
|
+
timeout: 5s
|
|
58
|
+
retries: 10
|
|
59
|
+
networks:
|
|
60
|
+
- shaapi
|
|
61
|
+
|
|
62
|
+
minio:
|
|
63
|
+
image: minio/minio:latest
|
|
64
|
+
container_name: shaapi_minio
|
|
65
|
+
restart: unless-stopped
|
|
66
|
+
command: server /data --console-address ":9001"
|
|
67
|
+
environment:
|
|
68
|
+
- MINIO_ROOT_USER=${MINIO_ACCESS_KEY:-minioadmin}
|
|
69
|
+
- MINIO_ROOT_PASSWORD=${MINIO_SECRET_KEY:-minioadmin}
|
|
70
|
+
volumes:
|
|
71
|
+
- shaapi_minio:/data
|
|
72
|
+
ports:
|
|
73
|
+
- "9000:9000"
|
|
74
|
+
- "9001:9001"
|
|
75
|
+
healthcheck:
|
|
76
|
+
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
|
77
|
+
interval: 30s
|
|
78
|
+
timeout: 20s
|
|
79
|
+
retries: 3
|
|
80
|
+
networks:
|
|
81
|
+
- shaapi
|
|
82
|
+
|
|
83
|
+
volumes:
|
|
84
|
+
shaapi_postgres:
|
|
85
|
+
shaapi_redis:
|
|
86
|
+
shaapi_minio:
|
|
87
|
+
|
|
88
|
+
networks:
|
|
89
|
+
shaapi:
|
|
90
|
+
driver: bridge
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Manage the shaapi Docker stack.
|
|
3
|
+
#
|
|
4
|
+
# Usage: ./docker-run.sh [command] [--monitoring] [--prod]
|
|
5
|
+
#
|
|
6
|
+
# Commands:
|
|
7
|
+
# up Start all containers (default)
|
|
8
|
+
# down Stop all containers
|
|
9
|
+
# build (Re)build the API image
|
|
10
|
+
# logs Tail logs from all services
|
|
11
|
+
# api-logs Tail API logs only
|
|
12
|
+
# restart Restart all containers
|
|
13
|
+
# restart-api Restart only the API container
|
|
14
|
+
# dev Start deps in background + API in foreground (live logs)
|
|
15
|
+
# shell Open a shell inside the API container
|
|
16
|
+
# db psql into Postgres
|
|
17
|
+
# redis redis-cli into Redis
|
|
18
|
+
# migrate Apply database migrations (alembic upgrade head)
|
|
19
|
+
# makemigrations [msg] Generate a new migration from model changes
|
|
20
|
+
# ps Show container status
|
|
21
|
+
#
|
|
22
|
+
# Options:
|
|
23
|
+
# --monitoring Include the Prometheus/Grafana/Tempo/Loki stack
|
|
24
|
+
# --prod Production mode: no source bind-mount, serve the baked image
|
|
25
|
+
set -e
|
|
26
|
+
|
|
27
|
+
cd "$(dirname "$0")"
|
|
28
|
+
|
|
29
|
+
# Pick a compose command (v2 plugin preferred)
|
|
30
|
+
if docker compose version >/dev/null 2>&1; then
|
|
31
|
+
DC="docker compose"
|
|
32
|
+
elif command -v docker-compose >/dev/null 2>&1; then
|
|
33
|
+
DC="docker-compose"
|
|
34
|
+
else
|
|
35
|
+
echo "â Docker Compose not found. Install Docker Desktop or the compose plugin."
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Create .env from template on first run
|
|
40
|
+
if [ ! -f .env ]; then
|
|
41
|
+
echo "âšī¸ No .env found â creating one from .env.template"
|
|
42
|
+
cp .env.template .env
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# Parse positional command + flags
|
|
46
|
+
MONITORING=false
|
|
47
|
+
PROD=false
|
|
48
|
+
POSITIONAL=()
|
|
49
|
+
for arg in "$@"; do
|
|
50
|
+
case "$arg" in
|
|
51
|
+
--monitoring) MONITORING=true ;;
|
|
52
|
+
--prod) PROD=true ;;
|
|
53
|
+
*) POSITIONAL+=("$arg") ;;
|
|
54
|
+
esac
|
|
55
|
+
done
|
|
56
|
+
COMMAND="${POSITIONAL[0]:-up}"
|
|
57
|
+
|
|
58
|
+
# Compose file composition
|
|
59
|
+
FILES="-f docker-compose.yml"
|
|
60
|
+
if [ "$PROD" = false ]; then
|
|
61
|
+
FILES="$FILES -f docker-compose.override.yml" # dev: bind-mount + reload
|
|
62
|
+
fi
|
|
63
|
+
if [ "$MONITORING" = true ]; then
|
|
64
|
+
FILES="$FILES -f docker-compose.monitoring.yml"
|
|
65
|
+
echo "đ Monitoring enabled â Grafana: http://localhost:3000 Prometheus: http://localhost:9090"
|
|
66
|
+
fi
|
|
67
|
+
DC="$DC $FILES"
|
|
68
|
+
|
|
69
|
+
case "$COMMAND" in
|
|
70
|
+
up)
|
|
71
|
+
echo "Starting shaapi..."
|
|
72
|
+
$DC up -d --build
|
|
73
|
+
echo "â
API: http://localhost:8000 | Swagger: http://localhost:8000/admin/api/v1/docs"
|
|
74
|
+
;;
|
|
75
|
+
down) $DC down ;;
|
|
76
|
+
build) $DC build ;;
|
|
77
|
+
logs) $DC logs -f ;;
|
|
78
|
+
api-logs) $DC logs -f api ;;
|
|
79
|
+
restart) $DC restart ;;
|
|
80
|
+
restart-api) $DC restart api ;;
|
|
81
|
+
dev)
|
|
82
|
+
$DC up -d postgres redis minio
|
|
83
|
+
$DC up --build api
|
|
84
|
+
;;
|
|
85
|
+
shell) $DC exec api bash ;;
|
|
86
|
+
db) $DC exec postgres psql -U "${POSTGRES_USER:-postgres}" -d "${POSTGRES_DATABASE:-shaapi}" ;;
|
|
87
|
+
redis) $DC exec redis redis-cli ;;
|
|
88
|
+
migrate) $DC exec api bash -c "cd backend && alembic upgrade head" ;;
|
|
89
|
+
makemigrations)
|
|
90
|
+
MSG="${POSITIONAL[1]:-auto}"
|
|
91
|
+
$DC exec api bash -c "cd backend && alembic revision --autogenerate -m '$MSG'"
|
|
92
|
+
;;
|
|
93
|
+
ps) $DC ps ;;
|
|
94
|
+
*)
|
|
95
|
+
echo "Unknown command: $COMMAND"
|
|
96
|
+
echo "Run: up | down | build | logs | api-logs | restart | restart-api | dev | shell | db | redis | migrate | makemigrations | ps [--monitoring] [--prod]"
|
|
97
|
+
exit 1
|
|
98
|
+
;;
|
|
99
|
+
esac
|