fix(webhooks): harden webhook config and secret handling
This commit is contained in:
@@ -43,10 +43,19 @@ async def build_and_start_web_app(
|
|||||||
telegram_uses_webhook_mode = bool(settings.WEBHOOK_BASE_URL)
|
telegram_uses_webhook_mode = bool(settings.WEBHOOK_BASE_URL)
|
||||||
|
|
||||||
if telegram_uses_webhook_mode:
|
if telegram_uses_webhook_mode:
|
||||||
telegram_webhook_path = f"/{settings.BOT_TOKEN}"
|
telegram_webhook_path = settings.telegram_webhook_path
|
||||||
app.router.add_post(telegram_webhook_path, SimpleRequestHandler(dispatcher=dp, bot=bot))
|
app.router.add_post(
|
||||||
|
telegram_webhook_path,
|
||||||
|
SimpleRequestHandler(
|
||||||
|
dispatcher=dp,
|
||||||
|
bot=bot,
|
||||||
|
secret_token=settings.TELEGRAM_WEBHOOK_SECRET,
|
||||||
|
),
|
||||||
|
)
|
||||||
logging.info(
|
logging.info(
|
||||||
f"Telegram webhook route configured at: [POST] {telegram_webhook_path} (relative to base URL)"
|
"Telegram webhook route configured at: [POST] %s (secret_token=%s)",
|
||||||
|
telegram_webhook_path,
|
||||||
|
"set" if settings.TELEGRAM_WEBHOOK_SECRET else "not_set",
|
||||||
)
|
)
|
||||||
|
|
||||||
from bot.handlers.user.payment import yookassa_webhook_route
|
from bot.handlers.user.payment import yookassa_webhook_route
|
||||||
@@ -59,7 +68,7 @@ async def build_and_start_web_app(
|
|||||||
cp_path = settings.cryptopay_webhook_path
|
cp_path = settings.cryptopay_webhook_path
|
||||||
if cp_path.startswith("/"):
|
if cp_path.startswith("/"):
|
||||||
app.router.add_post(cp_path, cryptopay_webhook_route)
|
app.router.add_post(cp_path, cryptopay_webhook_route)
|
||||||
logging.info(f"CryptoPay webhook route configured at: [POST] {cp_path}")
|
logging.info("CryptoPay webhook route configured at: [POST] %s", cp_path)
|
||||||
|
|
||||||
fk_path = settings.freekassa_webhook_path
|
fk_path = settings.freekassa_webhook_path
|
||||||
if fk_path.startswith("/"):
|
if fk_path.startswith("/"):
|
||||||
|
|||||||
+32
-37
@@ -58,52 +58,47 @@ async def on_startup_configured(dispatcher: Dispatcher):
|
|||||||
|
|
||||||
telegram_webhook_url_to_set = settings.WEBHOOK_BASE_URL
|
telegram_webhook_url_to_set = settings.WEBHOOK_BASE_URL
|
||||||
if telegram_webhook_url_to_set:
|
if telegram_webhook_url_to_set:
|
||||||
full_telegram_webhook_url = (
|
full_telegram_webhook_url = settings.telegram_full_webhook_url
|
||||||
f"{str(telegram_webhook_url_to_set).rstrip('/')}/{settings.BOT_TOKEN}"
|
if not full_telegram_webhook_url:
|
||||||
)
|
logging.error(
|
||||||
|
"STARTUP: Telegram webhook URL could not be built (WEBHOOK_BASE_URL missing)."
|
||||||
|
)
|
||||||
|
raise SystemExit("WEBHOOK_BASE_URL is required. Polling mode is disabled.")
|
||||||
|
|
||||||
logging.info(
|
logging.info(
|
||||||
f"STARTUP: Attempting to set Telegram webhook to: {full_telegram_webhook_url if full_telegram_webhook_url != 'ERROR_URL_TOKEN_DETECTED' else 'HIDDEN DUE TO TOKEN'}"
|
"STARTUP: Attempting to set Telegram webhook (path=%s)",
|
||||||
|
settings.telegram_webhook_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
if full_telegram_webhook_url != "ERROR_URL_TOKEN_DETECTED":
|
try:
|
||||||
try:
|
current_webhook_info = await bot.get_webhook_info()
|
||||||
current_webhook_info = await bot.get_webhook_info()
|
if current_webhook_info.url:
|
||||||
logging.info(
|
logging.info("STARTUP: Telegram webhook already set (non-empty URL).")
|
||||||
f"STARTUP: Current Telegram webhook info BEFORE setting: {current_webhook_info.model_dump_json(exclude_none=True, indent=2)}"
|
else:
|
||||||
)
|
logging.info("STARTUP: Telegram webhook currently empty (will set).")
|
||||||
|
|
||||||
set_success = await bot.set_webhook(
|
set_success = await bot.set_webhook(
|
||||||
url=full_telegram_webhook_url,
|
url=full_telegram_webhook_url,
|
||||||
drop_pending_updates=True,
|
drop_pending_updates=True,
|
||||||
allowed_updates=dispatcher.resolve_used_update_types(),
|
allowed_updates=dispatcher.resolve_used_update_types(),
|
||||||
)
|
secret_token=settings.TELEGRAM_WEBHOOK_SECRET,
|
||||||
if set_success:
|
)
|
||||||
logging.info(
|
if set_success:
|
||||||
f"STARTUP: bot.set_webhook to {full_telegram_webhook_url} returned SUCCESS (True)."
|
logging.info("STARTUP: bot.set_webhook returned SUCCESS (True).")
|
||||||
)
|
else:
|
||||||
else:
|
logging.error("STARTUP: bot.set_webhook returned FAILURE (False).")
|
||||||
logging.error(
|
|
||||||
f"STARTUP: bot.set_webhook to {full_telegram_webhook_url} returned FAILURE (False)."
|
|
||||||
)
|
|
||||||
|
|
||||||
new_webhook_info = await bot.get_webhook_info()
|
new_webhook_info = await bot.get_webhook_info()
|
||||||
logging.info(
|
if not new_webhook_info.url:
|
||||||
f"STARTUP: Telegram Webhook info AFTER setting: {new_webhook_info.model_dump_json(exclude_none=True, indent=2)}"
|
|
||||||
)
|
|
||||||
if not new_webhook_info.url:
|
|
||||||
logging.error(
|
|
||||||
"STARTUP: CRITICAL - Telegram Webhook URL is EMPTY after set attempt. Check bot token and URL validity."
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e_setwebhook:
|
|
||||||
logging.error(
|
logging.error(
|
||||||
f"STARTUP: EXCEPTION during set/get Telegram webhook: {e_setwebhook}",
|
"STARTUP: CRITICAL - Telegram Webhook URL is EMPTY after set attempt. Check bot token and URL validity."
|
||||||
exc_info=True,
|
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
|
except Exception as e_setwebhook:
|
||||||
logging.error(
|
logging.error(
|
||||||
"STARTUP: Skipped setting Telegram webhook due to security or configuration error."
|
"STARTUP: EXCEPTION during set/get Telegram webhook: %s",
|
||||||
|
e_setwebhook,
|
||||||
|
exc_info=True,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logging.error(
|
logging.error(
|
||||||
|
|||||||
@@ -59,6 +59,29 @@ class PanelApiService:
|
|||||||
headers["Authorization"] = f"Bearer {self.api_key}"
|
headers["Authorization"] = f"Bearer {self.api_key}"
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _sanitize_payload_for_log(payload: Any) -> Any:
|
||||||
|
if isinstance(payload, dict):
|
||||||
|
redacted: Dict[str, Any] = {}
|
||||||
|
for key, value in payload.items():
|
||||||
|
lowered = str(key).lower()
|
||||||
|
if any(mask_key in lowered for mask_key in (
|
||||||
|
"token",
|
||||||
|
"secret",
|
||||||
|
"password",
|
||||||
|
"authorization",
|
||||||
|
"api_key",
|
||||||
|
"apikey",
|
||||||
|
"key",
|
||||||
|
)):
|
||||||
|
redacted[key] = "***"
|
||||||
|
else:
|
||||||
|
redacted[key] = PanelApiService._sanitize_payload_for_log(value)
|
||||||
|
return redacted
|
||||||
|
if isinstance(payload, list):
|
||||||
|
return [PanelApiService._sanitize_payload_for_log(item) for item in payload]
|
||||||
|
return payload
|
||||||
|
|
||||||
async def _request(self,
|
async def _request(self,
|
||||||
method: str,
|
method: str,
|
||||||
endpoint: str,
|
endpoint: str,
|
||||||
@@ -83,8 +106,8 @@ class PanelApiService:
|
|||||||
if current_params:
|
if current_params:
|
||||||
try:
|
try:
|
||||||
url_with_params_for_log += "?" + urlencode(current_params)
|
url_with_params_for_log += "?" + urlencode(current_params)
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
pass
|
logging.debug("Failed to encode params for panel API log URL: %s", exc)
|
||||||
|
|
||||||
json_payload_for_log = kwargs.get('json') if method.upper() in [
|
json_payload_for_log = kwargs.get('json') if method.upper() in [
|
||||||
"POST", "PATCH", "PUT"
|
"POST", "PATCH", "PUT"
|
||||||
@@ -92,10 +115,11 @@ class PanelApiService:
|
|||||||
log_prefix = f"Panel API Req: {method.upper()} {url_with_params_for_log}"
|
log_prefix = f"Panel API Req: {method.upper()} {url_with_params_for_log}"
|
||||||
if json_payload_for_log:
|
if json_payload_for_log:
|
||||||
try:
|
try:
|
||||||
payload_str = json.dumps(json_payload_for_log)
|
sanitized_payload = self._sanitize_payload_for_log(json_payload_for_log)
|
||||||
|
payload_str = json.dumps(sanitized_payload)
|
||||||
log_prefix += f" | Payload: {payload_str[:300]}{'...' if len(payload_str) > 300 else ''}"
|
log_prefix += f" | Payload: {payload_str[:300]}{'...' if len(payload_str) > 300 else ''}"
|
||||||
except Exception:
|
except Exception:
|
||||||
log_prefix += f" | Payload: {str(json_payload_for_log)[:300]}..."
|
log_prefix += " | Payload: <unavailable>"
|
||||||
try:
|
try:
|
||||||
async with aiohttp_session.request(method.upper(),
|
async with aiohttp_session.request(method.upper(),
|
||||||
url_for_request,
|
url_for_request,
|
||||||
@@ -106,7 +130,8 @@ class PanelApiService:
|
|||||||
|
|
||||||
log_suffix = f"| Status: {response_status}"
|
log_suffix = f"| Status: {response_status}"
|
||||||
|
|
||||||
if log_full_response or not (200 <= response_status < 300):
|
should_log_full_body = bool(log_full_response and self.settings.LOG_LEVEL == "DEBUG")
|
||||||
|
if should_log_full_body or not (200 <= response_status < 300):
|
||||||
try:
|
try:
|
||||||
parsed_json_for_log = json.loads(response_text)
|
parsed_json_for_log = json.loads(response_text)
|
||||||
pretty_response_text = json.dumps(parsed_json_for_log,
|
pretty_response_text = json.dumps(parsed_json_for_log,
|
||||||
@@ -404,7 +429,10 @@ class PanelApiService:
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
logging.error(
|
logging.error(
|
||||||
f"Failed to create panel user '{username_on_panel}'. Payload: {payload}, Response: {response if not log_response else '(full response logged above)'}"
|
"Failed to create panel user '%s'. Payload: %s, Response: %s",
|
||||||
|
username_on_panel,
|
||||||
|
self._sanitize_payload_for_log(payload),
|
||||||
|
response if not log_response else "(full response logged above)",
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -426,7 +454,10 @@ class PanelApiService:
|
|||||||
return full_response.get("response")
|
return full_response.get("response")
|
||||||
|
|
||||||
logging.error(
|
logging.error(
|
||||||
f"Failed to update user {user_uuid} details on panel. Payload: {update_payload}, Response: {full_response if not log_response else '(logged above)'}"
|
"Failed to update user %s details on panel. Payload: %s, Response: %s",
|
||||||
|
user_uuid,
|
||||||
|
self._sanitize_payload_for_log(update_payload),
|
||||||
|
full_response if not log_response else "(logged above)",
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -136,16 +136,19 @@ class PanelWebhookService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def handle_webhook(self, raw_body: bytes, signature_header: Optional[str]) -> web.Response:
|
async def handle_webhook(self, raw_body: bytes, signature_header: Optional[str]) -> web.Response:
|
||||||
if self.settings.PANEL_WEBHOOK_SECRET:
|
if not self.settings.PANEL_WEBHOOK_SECRET:
|
||||||
if not signature_header:
|
logging.critical("Panel webhook rejected: PANEL_WEBHOOK_SECRET is not configured")
|
||||||
return web.Response(status=403, text="no_signature")
|
return web.Response(status=503, text="panel_webhook_secret_required")
|
||||||
expected_sig = hmac.new(
|
|
||||||
self.settings.PANEL_WEBHOOK_SECRET.encode(),
|
if not signature_header:
|
||||||
raw_body,
|
return web.Response(status=403, text="no_signature")
|
||||||
hashlib.sha256,
|
expected_sig = hmac.new(
|
||||||
).hexdigest()
|
self.settings.PANEL_WEBHOOK_SECRET.encode(),
|
||||||
if not hmac.compare_digest(expected_sig, signature_header):
|
raw_body,
|
||||||
return web.Response(status=403, text="invalid_signature")
|
hashlib.sha256,
|
||||||
|
).hexdigest()
|
||||||
|
if not hmac.compare_digest(expected_sig, signature_header):
|
||||||
|
return web.Response(status=403, text="invalid_signature")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payload = json.loads(raw_body.decode())
|
payload = json.loads(raw_body.decode())
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class StarsService:
|
|||||||
title=description,
|
title=description,
|
||||||
description=description,
|
description=description,
|
||||||
payload=payload,
|
payload=payload,
|
||||||
provider_token="",
|
provider_token=self.settings.STARS_PROVIDER_TOKEN or "",
|
||||||
currency="XTR",
|
currency="XTR",
|
||||||
prices=prices,
|
prices=prices,
|
||||||
)
|
)
|
||||||
|
|||||||
+63
-1
@@ -77,6 +77,14 @@ class Settings(BaseSettings):
|
|||||||
)
|
)
|
||||||
|
|
||||||
WEBHOOK_BASE_URL: Optional[str] = None
|
WEBHOOK_BASE_URL: Optional[str] = None
|
||||||
|
TELEGRAM_WEBHOOK_PATH: str = Field(
|
||||||
|
default="/webhook/telegram",
|
||||||
|
description="Relative path for Telegram webhook endpoint",
|
||||||
|
)
|
||||||
|
TELEGRAM_WEBHOOK_SECRET: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Secret token for Telegram webhook header validation",
|
||||||
|
)
|
||||||
|
|
||||||
CRYPTOPAY_TOKEN: Optional[str] = None
|
CRYPTOPAY_TOKEN: Optional[str] = None
|
||||||
CRYPTOPAY_NETWORK: str = Field(default="mainnet")
|
CRYPTOPAY_NETWORK: str = Field(default="mainnet")
|
||||||
@@ -114,6 +122,10 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
YOOKASSA_ENABLED: bool = Field(default=True)
|
YOOKASSA_ENABLED: bool = Field(default=True)
|
||||||
STARS_ENABLED: bool = Field(default=True)
|
STARS_ENABLED: bool = Field(default=True)
|
||||||
|
STARS_PROVIDER_TOKEN: Optional[str] = Field(
|
||||||
|
default="",
|
||||||
|
description="Provider token for Telegram invoices. For Stars (XTR) should stay empty.",
|
||||||
|
)
|
||||||
PAYMENT_METHODS_ORDER: Optional[str] = Field(
|
PAYMENT_METHODS_ORDER: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Comma-separated list of payment methods to show (e.g., severpay,freekassa,yookassa,platega,stars,cryptopay)",
|
description="Comma-separated list of payment methods to show (e.g., severpay,freekassa,yookassa,platega,stars,cryptopay)",
|
||||||
@@ -201,7 +213,7 @@ class Settings(BaseSettings):
|
|||||||
CRYPT4_ENABLED: bool = Field(default=False, description="Enable happ crypt4 encryption for subscription URLs")
|
CRYPT4_ENABLED: bool = Field(default=False, description="Enable happ crypt4 encryption for subscription URLs")
|
||||||
CRYPT4_REDIRECT_URL: Optional[str] = Field(default=None, description="Base redirect URL used for the connect button when crypt4 is enabled")
|
CRYPT4_REDIRECT_URL: Optional[str] = Field(default=None, description="Base redirect URL used for the connect button when crypt4 is enabled")
|
||||||
|
|
||||||
WEB_SERVER_HOST: str = Field(default="0.0.0.0")
|
WEB_SERVER_HOST: str = Field(default="127.0.0.1")
|
||||||
WEB_SERVER_PORT: int = Field(default=8080)
|
WEB_SERVER_PORT: int = Field(default=8080)
|
||||||
LOGS_PAGE_SIZE: int = Field(default=10)
|
LOGS_PAGE_SIZE: int = Field(default=10)
|
||||||
|
|
||||||
@@ -287,6 +299,22 @@ class Settings(BaseSettings):
|
|||||||
return cleaned
|
return cleaned
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def telegram_webhook_path(self) -> str:
|
||||||
|
path = (self.TELEGRAM_WEBHOOK_PATH or "").strip() or "/webhook/telegram"
|
||||||
|
if not path.startswith("/"):
|
||||||
|
path = f"/{path}"
|
||||||
|
return path
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def telegram_full_webhook_url(self) -> Optional[str]:
|
||||||
|
base = self.WEBHOOK_BASE_URL
|
||||||
|
if base:
|
||||||
|
return f"{base.rstrip('/')}{self.telegram_webhook_path}"
|
||||||
|
return None
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
@property
|
@property
|
||||||
def yookassa_webhook_path(self) -> str:
|
def yookassa_webhook_path(self) -> str:
|
||||||
@@ -528,6 +556,18 @@ class Settings(BaseSettings):
|
|||||||
)
|
)
|
||||||
LOG_CHAT_ID: Optional[int] = Field(default=None, description="Telegram chat/group ID for sending notifications")
|
LOG_CHAT_ID: Optional[int] = Field(default=None, description="Telegram chat/group ID for sending notifications")
|
||||||
LOG_THREAD_ID: Optional[int] = Field(default=None, description="Thread ID for supergroup messages (optional)")
|
LOG_THREAD_ID: Optional[int] = Field(default=None, description="Thread ID for supergroup messages (optional)")
|
||||||
|
LOG_STORE_MESSAGE_CONTENT: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description="Store message/callback content in message logs",
|
||||||
|
)
|
||||||
|
LOG_STORE_RAW_UPDATES: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description="Store raw update previews in message logs",
|
||||||
|
)
|
||||||
|
LOG_EXPORT_INCLUDE_SENSITIVE: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description="Include content/raw update fields in admin CSV export",
|
||||||
|
)
|
||||||
|
|
||||||
@field_validator('LOG_LEVEL', mode='before')
|
@field_validator('LOG_LEVEL', mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -557,12 +597,29 @@ class Settings(BaseSettings):
|
|||||||
sanitized[key] = value
|
sanitized[key] = value
|
||||||
return sanitized
|
return sanitized
|
||||||
|
|
||||||
|
@field_validator(
|
||||||
|
'TELEGRAM_WEBHOOK_PATH',
|
||||||
|
mode='before',
|
||||||
|
)
|
||||||
|
@classmethod
|
||||||
|
def normalize_webhook_path(cls, v):
|
||||||
|
if not isinstance(v, str):
|
||||||
|
return "/webhook/telegram"
|
||||||
|
cleaned = v.strip()
|
||||||
|
if not cleaned:
|
||||||
|
return "/webhook/telegram"
|
||||||
|
if not cleaned.startswith("/"):
|
||||||
|
cleaned = f"/{cleaned}"
|
||||||
|
return cleaned
|
||||||
|
|
||||||
@field_validator(
|
@field_validator(
|
||||||
'REQUIRED_CHANNEL_LINK',
|
'REQUIRED_CHANNEL_LINK',
|
||||||
'PLATEGA_RETURN_URL',
|
'PLATEGA_RETURN_URL',
|
||||||
'PLATEGA_FAILED_URL',
|
'PLATEGA_FAILED_URL',
|
||||||
'SEVERPAY_RETURN_URL',
|
'SEVERPAY_RETURN_URL',
|
||||||
'CRYPT4_REDIRECT_URL',
|
'CRYPT4_REDIRECT_URL',
|
||||||
|
'TELEGRAM_WEBHOOK_SECRET',
|
||||||
|
'PANEL_WEBHOOK_SECRET',
|
||||||
mode='before',
|
mode='before',
|
||||||
)
|
)
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -619,6 +676,11 @@ def get_settings() -> Settings:
|
|||||||
logging.warning(
|
logging.warning(
|
||||||
"CRITICAL: PANEL_API_URL is not set. Panel integration will not work."
|
"CRITICAL: PANEL_API_URL is not set. Panel integration will not work."
|
||||||
)
|
)
|
||||||
|
if _settings_instance.WEBHOOK_BASE_URL and not _settings_instance.TELEGRAM_WEBHOOK_SECRET:
|
||||||
|
logging.warning(
|
||||||
|
"WARNING: TELEGRAM_WEBHOOK_SECRET is empty while webhook mode is enabled. "
|
||||||
|
"Set TELEGRAM_WEBHOOK_SECRET to validate X-Telegram-Bot-Api-Secret-Token header."
|
||||||
|
)
|
||||||
if not _settings_instance.YOOKASSA_SHOP_ID or not _settings_instance.YOOKASSA_SECRET_KEY:
|
if not _settings_instance.YOOKASSA_SHOP_ID or not _settings_instance.YOOKASSA_SECRET_KEY:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
"CRITICAL: YooKassa credentials (SHOP_ID or SECRET_KEY) are not set. Payments will not work."
|
"CRITICAL: YooKassa credentials (SHOP_ID or SECRET_KEY) are not set. Payments will not work."
|
||||||
|
|||||||
+17
-1
@@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from urllib.parse import urlsplit, urlunsplit
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
@@ -9,12 +10,27 @@ from .migrator import run_database_migrations
|
|||||||
async_engine = None
|
async_engine = None
|
||||||
|
|
||||||
|
|
||||||
|
def _mask_db_url(url: str) -> str:
|
||||||
|
try:
|
||||||
|
parsed = urlsplit(url)
|
||||||
|
if parsed.username is None:
|
||||||
|
return url
|
||||||
|
username = parsed.username
|
||||||
|
host = parsed.hostname or ""
|
||||||
|
port = f":{parsed.port}" if parsed.port else ""
|
||||||
|
netloc = f"{username}:***@{host}{port}"
|
||||||
|
return urlunsplit((parsed.scheme, netloc, parsed.path, parsed.query, parsed.fragment))
|
||||||
|
except Exception:
|
||||||
|
return "<masked>"
|
||||||
|
|
||||||
|
|
||||||
def init_db_connection(settings: Settings) -> sessionmaker:
|
def init_db_connection(settings: Settings) -> sessionmaker:
|
||||||
global async_engine
|
global async_engine
|
||||||
|
|
||||||
if async_engine is None:
|
if async_engine is None:
|
||||||
|
masked_url = _mask_db_url(settings.DATABASE_URL)
|
||||||
logging.info(
|
logging.info(
|
||||||
f"Attempting to create SQLAlchemy engine with URL: {settings.DATABASE_URL}"
|
f"Attempting to create SQLAlchemy engine with URL: {masked_url}"
|
||||||
)
|
)
|
||||||
async_engine = create_async_engine(
|
async_engine = create_async_engine(
|
||||||
settings.DATABASE_URL,
|
settings.DATABASE_URL,
|
||||||
|
|||||||
Reference in New Issue
Block a user