Files

195 lines
5.4 KiB
Python

"""Чтение/запись allowlist-ключей в .env (админка платежей)."""
from __future__ import annotations
import os
import re
from pathlib import Path
from dotenv import dotenv_values
from config import ROOT_DIR
# Ключи, которыми можно управлять из админки
PAYMENT_ENV_KEYS: tuple[str, ...] = (
"CRYPTO_BOT_TOKEN",
"CRYPTO_PAY_TESTNET",
"HELEKET_MERCHANT_ID",
"HELEKET_API_KEY",
"HELEKET_CALLBACK_URL",
"HELEKET_CURRENCY",
"LAVA_SHOP_ID",
"LAVA_SECRET_KEY",
"LAVA_ADDITIONAL_KEY",
"LAVA_HOOK_URL",
"LAVA_SUCCESS_URL",
"LAVA_FAIL_URL",
"LAVA_EXPIRE_MINUTES",
"DIGISELLER_SELLER_ID",
"DIGISELLER_API_KEY",
"DIGISELLER_PRODUCT_IDS",
"DIGISELLER_PAY_URL",
"DIGISELLER_CREDIT_FROM",
"DIGISELLER_CALLBACK_URL",
"ANYPAY_API_ID",
"ANYPAY_API_KEY",
"ANYPAY_PROJECT_ID",
"ANYPAY_CURRENCY",
"ANYPAY_METHOD",
"ANYPAY_SUCCESS_URL",
"ANYPAY_FAIL_URL",
"ANYPAY_EMAIL",
"ANYPAY_CHECK_IP",
"STARS_ENABLED",
"STARS_RUB_PER_STAR",
"TOPUP_CODE_PAY_URL",
)
SECRET_ENV_KEYS = frozenset(
{
"CRYPTO_BOT_TOKEN",
"HELEKET_API_KEY",
"LAVA_SECRET_KEY",
"LAVA_ADDITIONAL_KEY",
"DIGISELLER_API_KEY",
"ANYPAY_API_KEY",
"SMTP_PASSWORD",
}
)
BOOL_ENV_KEYS = frozenset(
{"CRYPTO_PAY_TESTNET", "STARS_ENABLED", "ANYPAY_CHECK_IP", "SMTP_TLS", "SMTP_SSL"}
)
_KEY_LINE_RE = re.compile(
r"^(?P<prefix>\s*(?:export\s+)?)(?P<key>[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?P<value>.*)$"
)
def env_file_path() -> Path:
raw = (os.getenv("ENV_FILE") or "").strip()
if raw:
return Path(raw)
return ROOT_DIR / ".env"
def env_writable(path: Path | None = None) -> bool:
p = path or env_file_path()
try:
if p.exists():
return os.access(p, os.W_OK)
parent = p.parent
return parent.exists() and os.access(parent, os.W_OK)
except OSError:
return False
def mask_secret(value: str) -> str:
v = (value or "").strip()
if not v:
return ""
if len(v) <= 6:
return "••••••"
return f"{v[:3]}{v[-4:]}"
def read_env_values(path: Path | None = None) -> dict[str, str]:
p = path or env_file_path()
if not p.exists():
return {}
raw = dotenv_values(p)
out: dict[str, str] = {}
for k, v in raw.items():
if k is None:
continue
out[str(k)] = "" if v is None else str(v)
return out
def _format_env_value(value: str) -> str:
"""Значение для .env без лишних кавычек, если не нужны."""
if value == "":
return ""
if any(ch in value for ch in ' \t#"\'\\$`\n\r'):
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
return f'"{escaped}"'
return value
def _write_env_inplace(path: Path, text: str) -> None:
"""Перезаписать содержимое файла без rename (Docker bind-mount .env)."""
data = text.encode("utf-8")
with open(path, "r+b") as fh:
fh.truncate(0)
fh.seek(0)
fh.write(data)
fh.flush()
os.fsync(fh.fileno())
def upsert_env_keys(
updates: dict[str, str | None],
*,
path: Path | None = None,
allowlist: tuple[str, ...] = PAYMENT_ENV_KEYS,
) -> Path:
"""Записать ключи в .env и в os.environ. None / пустая строка удаляет ключ.
Пишем in-place: docker volume mount файла не позволяет atomic rename
(Errno 16 Device or resource busy).
"""
p = path or env_file_path()
allowed = frozenset(allowlist)
filtered = {k: v for k, v in updates.items() if k in allowed}
if not filtered:
return p
if not p.exists():
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text("", encoding="utf-8")
raw = p.read_text(encoding="utf-8")
lines = raw.splitlines(keepends=True)
if lines and not lines[-1].endswith(("\n", "\r")):
# нормализуем конец файла при дальнейшей склейке
pass
pending = dict(filtered)
new_lines: list[str] = []
for line in lines:
stripped = line.lstrip()
if not stripped or stripped.startswith("#"):
new_lines.append(line)
continue
m = _KEY_LINE_RE.match(line.rstrip("\r\n"))
if not m:
new_lines.append(line)
continue
key = m.group("key")
if key not in pending:
new_lines.append(line)
continue
value = pending.pop(key)
ending = "\n" if line.endswith("\n") else ("\r\n" if line.endswith("\r\n") else "\n")
if value is None or value == "":
os.environ.pop(key, None)
continue
prefix = m.group("prefix") or ""
new_lines.append(f"{prefix}{key}={_format_env_value(value)}{ending}")
os.environ[key] = value
for key, value in pending.items():
if value is None or value == "":
os.environ.pop(key, None)
continue
if new_lines and not new_lines[-1].endswith(("\n", "\r")):
new_lines[-1] = new_lines[-1] + "\n"
new_lines.append(f"{key}={_format_env_value(value)}\n")
os.environ[key] = value
text = "".join(new_lines)
if text and not text.endswith("\n"):
text += "\n"
_write_env_inplace(p, text)
return p