feat: payments setup wizard — legal pages + YooKassa keys from the admin UI
The site/offer.html and site/privacy.html legal templates existed in
the repo but were never actually wired to anything — no route served
them, install.sh never copied them anywhere. Nobody deploying this
for real payments had a live offer/privacy page, which YooKassa
requires for merchant approval.
Rewrote both templates with {{TOKEN}} placeholders (new legal.py
renders them from .env-backed settings, read fresh on every request,
no restart needed to fix a typo) and added a proper setup section in
the Payments tab: business type/name/INN/support contact/refund
window, saved via POST /admin/api/payments/legal-settings, live at
GET /offer and /privacy immediately. Unset fields render as a visible
not-set-yet badge instead of breaking the page. Effective date
auto-stamps once on first save and stays stable across later edits
(verified: editing the name afterward doesn't reset it).
YooKassa shop_id + secret_key get their own section: validated live
against YooKassa's own /v3/me before being saved (same pattern as the
existing bot-token getMe check), never echoed back to the frontend
once set. Includes an inline guide — where to find the keys in
YooKassa's dashboard, and that self-employed registration there needs
just passport + INN, no separate cash register.
Both new dropdowns use the existing custom .dd component, not a raw
select element — this codebase deliberately doesn't use native
selects (see the comment already in admin.html) because of the
OS-rendered white popup, so a new form had to follow that pattern,
not reintroduce it.
Verified: template rendering with empty settings (fallback badges,
no leftover tokens) and fully filled settings, HTML-escaping of field
values (a script tag in a field renders as text, not markup), the
one-time-only date stamp, and all new routes registering correctly.
Also fixed a stale doc string in the panel's own admin-facing docs
tab that still quoted the old rate-limit numbers from before the
real limits shipped.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
3bd38103c3
commit
6311532c45
6 changed files with 249 additions and 19 deletions
121
admin.html
121
admin.html
|
|
@ -502,9 +502,53 @@
|
|||
<div id="view-payments" class="view">
|
||||
<div class="page-title">Платежи</div>
|
||||
<div class="page-sub">ЮKassa / Platega — история и статус, с проверкой на стороне провайдера при пропущенном вебхуке</div>
|
||||
<div class="table-wrap"><table><thead><tr>
|
||||
<th>Пользователь</th><th>Сервер</th><th>Тариф</th><th>Провайдер</th><th>Сумма</th><th>Создан</th><th>Статус</th><th></th>
|
||||
</tr></thead><tbody id="payments-body"></tbody></table></div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-head"><h2>Настройка приёма платежей</h2></div>
|
||||
<p class="page-sub" style="margin-bottom:16px">Заполни один раз — панель сама соберёт из этого публичную оферту и политику конфиденциальности (обязательны для подключения ЮKassa) на своих страницах, готовых к показу клиентам.</p>
|
||||
|
||||
<div class="doc-block" style="margin-bottom:20px">
|
||||
<h2>Как это работает</h2>
|
||||
<p>1. Заполни реквизиты ниже (кто ты для закона — самозанятый/ИП/ООО, ИНН, контакты). Это те же данные, что ЮKassa попросит при регистрации магазина.</p>
|
||||
<p>2. Подключи ЮKassa: заведи магазин на <a href="https://yookassa.ru" target="_blank">yookassa.ru</a>, в личном кабинете возьми <b>shop_id</b> и <b>секретный ключ</b> (Настройки → Ключи API), вставь сюда. Панель сразу проверит их и сохранит.</p>
|
||||
<p>3. Ссылки на готовые оферту и политику (<code>https://{домен}/offer</code>, <code>/privacy</code>) — дай их ЮKassa при регистрации магазина, она их обязательно спросит.</p>
|
||||
<p class="muted">Самозанятым для приёма платежей от физлиц регистрация магазина в ЮKassa доступна напрямую по паспорту и ИНН, без онлайн-кассы — она уже встроена в сервис ЮKassa. ИП/ООО — обычная регистрация магазина.</p>
|
||||
</div>
|
||||
|
||||
<h3 style="font-size:14px;margin:0 0 12px">Реквизиты для документов</h3>
|
||||
<div class="form-row">
|
||||
<div><label class="f">Кто ты</label><div id="legal-type" class="dd"></div></div>
|
||||
<div><label class="f" id="legal-name-label">ФИО</label><input type="text" id="legal-name" placeholder="Иванов Иван Иванович"></div>
|
||||
<div><label class="f">ИНН</label><input type="text" id="legal-inn" placeholder="770123456789"></div>
|
||||
</div>
|
||||
<div class="form-row" style="margin-top:12px">
|
||||
<div><label class="f">Email поддержки</label><input type="text" id="legal-email" placeholder="support@example.com"></div>
|
||||
<div><label class="f">Telegram-контакт поддержки</label><input type="text" id="legal-contact" placeholder="@support"></div>
|
||||
<div><label class="f">Возврат в течение (часов)</label><input type="text" id="legal-refund" placeholder="24"></div>
|
||||
</div>
|
||||
<div class="form-row" style="margin-top:12px">
|
||||
<button class="btn" onclick="saveLegalSettings()">Сохранить реквизиты</button>
|
||||
</div>
|
||||
<div id="legal-result"></div>
|
||||
<p class="check-hint">Страницы всегда доступны по ссылкам: <a href="/offer" target="_blank" id="legal-offer-link">/offer</a> · <a href="/privacy" target="_blank" id="legal-privacy-link">/privacy</a> — незаполненные поля показываются пометкой, что их надо указать, страница не ломается.</p>
|
||||
|
||||
<h3 style="font-size:14px;margin:24px 0 12px">ЮKassa — ключи API</h3>
|
||||
<div id="yookassa-status" class="page-sub" style="margin-bottom:12px"></div>
|
||||
<div class="form-row">
|
||||
<div><label class="f">shop_id</label><input type="text" id="yk-shop-id" placeholder="123456"></div>
|
||||
<div><label class="f">Секретный ключ</label><input type="password" id="yk-secret-key" placeholder="live_..."></div>
|
||||
<div style="flex:0"><label class="f"> </label><button class="btn" onclick="saveYookassaSettings()">Проверить и сохранить</button></div>
|
||||
</div>
|
||||
<p class="check-hint">Панель сама постучится в ЮKassa (<code>/v3/me</code>) и сохранит ключи только если они рабочие. После сохранения включаются приём оплаты и вебхуки; бот перезапустится сам, для самой панели (обработка вебхуков) один раз выполни на сервере <code>mbs restart</code>.</p>
|
||||
<div id="yookassa-result"></div>
|
||||
</div>
|
||||
|
||||
<div class="section" style="margin-top:20px">
|
||||
<div class="section-head"><h2>История</h2></div>
|
||||
<div class="table-wrap"><table><thead><tr>
|
||||
<th>Пользователь</th><th>Сервер</th><th>Тариф</th><th>Провайдер</th><th>Сумма</th><th>Создан</th><th>Статус</th><th></th>
|
||||
</tr></thead><tbody id="payments-body"></tbody></table></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="view-docs" class="view">
|
||||
|
|
@ -526,7 +570,7 @@
|
|||
<div class="doc-block">
|
||||
<h2>Пароль и безопасность</h2>
|
||||
<p>Пароль админ-панели меняется командой <code>mbs pass</code> на сервере (без аргумента — сгенерит случайный). Панель физически откажется стартовать, если в <code>.env</code> стоит "change-me"/"admin"/что-то короче 8 символов — так что пропустить это не выйдет по-тихому.</p>
|
||||
<p>Сессия логина живёт в httpOnly-куке, на <code>/admin/api/login</code> висит rate-limit (5 попыток в минуту с одного IP). SSH-доступ на сервер — сам по себе, панель на него не влияет; отдельно стоит подумать про отключение root-логина по паролю в пользу ключей, если этого ещё не сделано.</p>
|
||||
<p>Сессия логина живёт в httpOnly-куке, опционально поверх пароля — 2FA (TOTP). На <code>/admin/api/login</code> и <code>/admin/api/login/totp</code> висит rate-limit (10 попыток за 15 минут на пароль, 10 за 5 минут на код — с одного IP). SSH-доступ на сервер — сам по себе, панель на него не влияет; отдельно стоит подумать про отключение root-логина по паролю в пользу ключей, если этого ещё не сделано.</p>
|
||||
</div>
|
||||
|
||||
<div class="doc-block">
|
||||
|
|
@ -685,7 +729,7 @@ function showView(name) {
|
|||
if (name === "gifts") loadGifts();
|
||||
if (name === "nodes") loadNodes();
|
||||
if (name === "traffic") loadTraffic();
|
||||
if (name === "payments") loadPayments();
|
||||
if (name === "payments") { loadPayments(); loadPaymentsSettings(); }
|
||||
if (name === "settings") { loadBotSettings(); loadAdmins(); loadTotpStatus(); }
|
||||
}
|
||||
|
||||
|
|
@ -860,6 +904,20 @@ function initDropdowns() {
|
|||
});
|
||||
DD.ucGrantNode = createDropdown("uc-grant-node", { options: [], placeholder: "Сервер" });
|
||||
DD.ucGrantPlan = createDropdown("uc-grant-plan", { options: [], placeholder: "Срок" });
|
||||
DD.legalType = createDropdown("legal-type", {
|
||||
options: [
|
||||
{ value: "self", label: "Самозанятый" },
|
||||
{ value: "ip", label: "ИП" },
|
||||
{ value: "ooo", label: "ООО" },
|
||||
],
|
||||
value: "self", placeholder: "Кто ты",
|
||||
onChange: (type) => {
|
||||
const label = document.getElementById("legal-name-label");
|
||||
const input = document.getElementById("legal-name");
|
||||
if (type === "ooo") { label.textContent = "Название"; input.placeholder = 'ООО «Ромашка»'; }
|
||||
else { label.textContent = "ФИО"; input.placeholder = "Иванов Иван Иванович"; }
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const ICONS = {
|
||||
|
|
@ -941,6 +999,59 @@ async function checkPayment(id, btn) {
|
|||
}
|
||||
}
|
||||
|
||||
async function loadPaymentsSettings() {
|
||||
const [legalRes, ykRes] = await Promise.all([
|
||||
api("/admin/api/payments/legal-settings"),
|
||||
api("/admin/api/payments/yookassa-settings"),
|
||||
]);
|
||||
document.getElementById("legal-name").value = legalRes.LEGAL_NAME || "";
|
||||
document.getElementById("legal-inn").value = legalRes.LEGAL_INN || "";
|
||||
document.getElementById("legal-refund").value = legalRes.REFUND_HOURS || "24";
|
||||
document.getElementById("legal-contact").value = legalRes.SUPPORT_CONTACT || "";
|
||||
document.getElementById("legal-email").value = legalRes.SUPPORT_EMAIL || "";
|
||||
|
||||
document.getElementById("yk-shop-id").value = ykRes.shop_id || "";
|
||||
const status = document.getElementById("yookassa-status");
|
||||
if (ykRes.enabled && ykRes.has_secret) {
|
||||
status.innerHTML = '<span class="badge ok">подключена</span> shop_id: ' + esc(ykRes.shop_id);
|
||||
} else {
|
||||
status.innerHTML = '<span class="badge bad">не настроена</span>';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveLegalSettings() {
|
||||
const result = document.getElementById("legal-result");
|
||||
const body = {
|
||||
LEGAL_NAME: document.getElementById("legal-name").value.trim(),
|
||||
LEGAL_INN: document.getElementById("legal-inn").value.trim(),
|
||||
REFUND_HOURS: document.getElementById("legal-refund").value.trim() || "24",
|
||||
SUPPORT_CONTACT: document.getElementById("legal-contact").value.trim(),
|
||||
SUPPORT_EMAIL: document.getElementById("legal-email").value.trim(),
|
||||
};
|
||||
try {
|
||||
await api("/admin/api/payments/legal-settings", { method: "POST", body: JSON.stringify(body) });
|
||||
result.innerHTML = '<p class="page-sub" style="margin-top:10px;color:var(--green)">Сохранено — страницы /offer и /privacy обновились сразу, без рестарта</p>';
|
||||
} catch (e) {
|
||||
result.innerHTML = '<p class="page-sub" style="margin-top:10px;color:var(--red)">Не получилось: ' + esc(e.message) + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveYookassaSettings() {
|
||||
const shop_id = document.getElementById("yk-shop-id").value.trim();
|
||||
const secret_key = document.getElementById("yk-secret-key").value.trim();
|
||||
const result = document.getElementById("yookassa-result");
|
||||
if (!shop_id || !secret_key) return;
|
||||
result.innerHTML = '<p class="page-sub" style="margin-top:10px">Проверяю ключи у ЮKassa…</p>';
|
||||
try {
|
||||
const res = await api("/admin/api/payments/yookassa-settings", { method: "POST", body: JSON.stringify({ shop_id, secret_key }) });
|
||||
result.innerHTML = `<p class="page-sub" style="margin-top:10px;color:var(--green)">Ключи рабочие, сохранено.${res.restarted_bot ? " Бот перезапущен." : " Бот сам не перезапустился — выполни mbs restart."} Для приёма вебхуков панелью выполни на сервере <code>mbs restart</code>.</p>`;
|
||||
document.getElementById("yk-secret-key").value = "";
|
||||
loadPaymentsSettings();
|
||||
} catch (e) {
|
||||
result.innerHTML = '<p class="page-sub" style="margin-top:10px;color:var(--red)">Не получилось: ' + esc(e.message) + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBotSettings() {
|
||||
const data = await api("/admin/api/settings/bot");
|
||||
document.getElementById("settings-bot-username").textContent = "@" + data.username;
|
||||
|
|
|
|||
62
api.py
62
api.py
|
|
@ -12,6 +12,7 @@ from fastapi.responses import HTMLResponse, PlainTextResponse, FileResponse
|
|||
|
||||
import backup
|
||||
import db
|
||||
import legal
|
||||
import links
|
||||
import nodeprov
|
||||
import payments
|
||||
|
|
@ -374,6 +375,57 @@ def admin_check_payment(payment_id: str, request: Request):
|
|||
return {"status": status}
|
||||
|
||||
|
||||
@app.get("/admin/api/payments/legal-settings")
|
||||
def admin_get_legal_settings(request: Request):
|
||||
require_admin(request)
|
||||
return legal.get_settings()
|
||||
|
||||
|
||||
@app.post("/admin/api/payments/legal-settings")
|
||||
def admin_set_legal_settings(request: Request, body: dict = Body(...)):
|
||||
require_admin(request)
|
||||
for key in ("LEGAL_NAME", "LEGAL_INN", "REFUND_HOURS", "SUPPORT_CONTACT", "SUPPORT_EMAIL"):
|
||||
if key in body:
|
||||
_update_env_var(key, str(body[key]).strip())
|
||||
if not legal.read_env_var("OFFER_EFFECTIVE_DATE"):
|
||||
_update_env_var("OFFER_EFFECTIVE_DATE", datetime.date.today().strftime("%d.%m.%Y"))
|
||||
return {"ok": True, "settings": legal.get_settings()}
|
||||
|
||||
|
||||
@app.get("/admin/api/payments/yookassa-settings")
|
||||
def admin_get_yookassa_settings(request: Request):
|
||||
require_admin(request)
|
||||
return {
|
||||
"enabled": payments.YOOKASSA_ENABLED,
|
||||
"shop_id": legal.read_env_var("YOOKASSA_SHOP_ID", ""),
|
||||
"has_secret": bool(legal.read_env_var("YOOKASSA_SECRET_KEY", "")),
|
||||
}
|
||||
|
||||
|
||||
@app.post("/admin/api/payments/yookassa-settings")
|
||||
def admin_set_yookassa_settings(request: Request, body: dict = Body(...)):
|
||||
require_admin(request)
|
||||
shop_id = (body.get("shop_id") or "").strip()
|
||||
secret_key = (body.get("secret_key") or "").strip()
|
||||
if not shop_id or not secret_key:
|
||||
raise HTTPException(400, "shop_id и secret_key обязательны")
|
||||
try:
|
||||
payments.validate_yookassa_credentials(shop_id, secret_key)
|
||||
except Exception:
|
||||
raise HTTPException(401, "ЮKassa не приняла эти ключи — проверь shop_id и секретный ключ")
|
||||
_update_env_var("YOOKASSA_SHOP_ID", shop_id)
|
||||
_update_env_var("YOOKASSA_SECRET_KEY", secret_key)
|
||||
_update_env_var("YOOKASSA_ENABLED", "true")
|
||||
_update_env_var("PAYMENTS_ENABLED", "true")
|
||||
restarted = False
|
||||
try:
|
||||
subprocess.run(["systemctl", "restart", "mbs-bot"], check=True, timeout=15)
|
||||
restarted = True
|
||||
except Exception:
|
||||
restarted = False
|
||||
return {"ok": True, "restarted_bot": restarted}
|
||||
|
||||
|
||||
@app.post("/payments/webhook/yookassa")
|
||||
async def yookassa_webhook(request: Request):
|
||||
body = await request.json()
|
||||
|
|
@ -994,3 +1046,13 @@ def root(request: Request):
|
|||
@app.get("/admin")
|
||||
def admin_page():
|
||||
return FileResponse(ADMIN_HTML_PATH, headers=_NO_CACHE)
|
||||
|
||||
|
||||
@app.get("/offer", response_class=HTMLResponse)
|
||||
def offer_page():
|
||||
return legal.render("offer.html")
|
||||
|
||||
|
||||
@app.get("/privacy", response_class=HTMLResponse)
|
||||
def privacy_page():
|
||||
return legal.render("privacy.html")
|
||||
|
|
|
|||
52
legal.py
Normal file
52
legal.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import html
|
||||
import os
|
||||
|
||||
from config import BASE_DIR, BOT_USERNAME
|
||||
|
||||
ENV_PATH = os.path.join(BASE_DIR, ".env")
|
||||
SITE_DIR = os.path.join(BASE_DIR, "site")
|
||||
|
||||
FIELD_KEYS = ["LEGAL_NAME", "LEGAL_INN", "REFUND_HOURS", "SUPPORT_CONTACT", "SUPPORT_EMAIL", "OFFER_EFFECTIVE_DATE"]
|
||||
|
||||
|
||||
def read_env_var(key: str, default: str = "") -> str:
|
||||
if not os.path.exists(ENV_PATH):
|
||||
return default
|
||||
with open(ENV_PATH, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line.startswith(f"{key}="):
|
||||
return line[len(key) + 1:]
|
||||
return default
|
||||
|
||||
|
||||
def get_settings() -> dict:
|
||||
return {key: read_env_var(key) for key in FIELD_KEYS}
|
||||
|
||||
|
||||
def _fallback(label: str) -> str:
|
||||
return f'<span class="fill">{html.escape(label)}</span>'
|
||||
|
||||
|
||||
def _field(value: str, fallback_label: str) -> str:
|
||||
return html.escape(value) if value else _fallback(fallback_label)
|
||||
|
||||
|
||||
def render(template_name: str) -> str:
|
||||
path = os.path.join(SITE_DIR, template_name)
|
||||
with open(path, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
s = get_settings()
|
||||
replacements = {
|
||||
"EFFECTIVE_DATE": _field(s["OFFER_EFFECTIVE_DATE"], "дата не указана"),
|
||||
"LEGAL_NAME": _field(s["LEGAL_NAME"], "название/ФИО не указано"),
|
||||
"INN": _field(s["LEGAL_INN"], "ИНН не указан"),
|
||||
"BOT_USERNAME": _field(f"@{BOT_USERNAME}" if BOT_USERNAME else "", "бот не указан"),
|
||||
"REFUND_HOURS": html.escape(s["REFUND_HOURS"]) if s["REFUND_HOURS"] else "24",
|
||||
"SUPPORT_CONTACT": _field(s["SUPPORT_CONTACT"], "контакт не указан"),
|
||||
"SUPPORT_EMAIL": _field(s["SUPPORT_EMAIL"], "email не указан"),
|
||||
}
|
||||
for token, value in replacements.items():
|
||||
content = content.replace("{{" + token + "}}", value)
|
||||
return content
|
||||
|
|
@ -62,6 +62,11 @@ def create_yookassa_payment(payment_id: str, amount_rub: int, description: str)
|
|||
return external_id, pay_url
|
||||
|
||||
|
||||
def validate_yookassa_credentials(shop_id: str, secret_key: str) -> dict:
|
||||
auth = base64.b64encode(f"{shop_id}:{secret_key}".encode()).decode()
|
||||
return _get_json("https://api.yookassa.ru/v3/me", {"Authorization": f"Basic {auth}"})
|
||||
|
||||
|
||||
def verify_yookassa_notification(body: dict) -> bool:
|
||||
return body.get("event") == "payment.succeeded" and "object" in body
|
||||
|
||||
|
|
|
|||
|
|
@ -34,12 +34,12 @@
|
|||
<div class="wrap">
|
||||
<a class="back" href="/">← На главную</a>
|
||||
<h1>Публичная оферта</h1>
|
||||
<p class="updated">Действует с <span class="fill">[дата]</span></p>
|
||||
<p class="updated">Действует с {{EFFECTIVE_DATE}}</p>
|
||||
|
||||
<p>Настоящий документ является публичной офертой <span class="fill">[ФИО ИП / наименование самозанятого или юрлица]</span>, <span class="fill">[ИНН]</span> (далее — «Исполнитель»), адресованной любому дееспособному физическому лицу (далее — «Клиент»), на заключение договора о предоставлении доступа к VPN-сервису на условиях, указанных ниже. Оплата услуги означает полное и безоговорочное принятие условий оферты (акцепт).</p>
|
||||
<p>Настоящий документ является публичной офертой {{LEGAL_NAME}}, {{INN}} (далее — «Исполнитель»), адресованной любому дееспособному физическому лицу (далее — «Клиент»), на заключение договора о предоставлении доступа к VPN-сервису на условиях, указанных ниже. Оплата услуги означает полное и безоговорочное принятие условий оферты (акцепт).</p>
|
||||
|
||||
<h2>1. Предмет договора</h2>
|
||||
<p>Исполнитель предоставляет Клиенту доступ к серверу VPN (VLESS/Hysteria2) на срок, соответствующий выбранному тарифу, через Telegram-бота <span class="fill">[@ваш_бот]</span>. Доступ выдаётся автоматически после подтверждения оплаты.</p>
|
||||
<p>Исполнитель предоставляет Клиенту доступ к серверу VPN (VLESS/Hysteria2) на срок, соответствующий выбранному тарифу, через Telegram-бота {{BOT_USERNAME}}. Доступ выдаётся автоматически после подтверждения оплаты.</p>
|
||||
|
||||
<h2>2. Стоимость и порядок оплаты</h2>
|
||||
<ol>
|
||||
|
|
@ -52,7 +52,7 @@
|
|||
<p>Доступ предоставляется на срок выбранного тарифа (от 7 дней до 1 года) и автоматически прекращается по истечении срока. Продление — отдельной оплатой, автосписание не производится.</p>
|
||||
|
||||
<h2>4. Возврат средств</h2>
|
||||
<p>Возврат возможен в течение <span class="fill">[N]</span> часов с момента оплаты, если доступ ни разу не был использован (не было подключений к серверу), — по обращению в поддержку <span class="fill">[контакт]</span>. После начала использования услуга считается оказанной.</p>
|
||||
<p>Возврат возможен в течение {{REFUND_HOURS}} часов с момента оплаты, если доступ ни разу не был использован (не было подключений к серверу), — по обращению в поддержку {{SUPPORT_CONTACT}}. После начала использования услуга считается оказанной.</p>
|
||||
|
||||
<h2>5. Права и обязанности сторон</h2>
|
||||
<ol>
|
||||
|
|
@ -63,10 +63,10 @@
|
|||
|
||||
<h2>6. Реквизиты Исполнителя</h2>
|
||||
<p class="muted">
|
||||
<span class="fill">[ФИО / наименование]</span><br>
|
||||
ИНН <span class="fill">[номер]</span><br>
|
||||
Email: <span class="fill">[email]</span><br>
|
||||
Telegram: <span class="fill">[контакт поддержки]</span>
|
||||
{{LEGAL_NAME}}<br>
|
||||
ИНН {{INN}}<br>
|
||||
Email: {{SUPPORT_EMAIL}}<br>
|
||||
Telegram: {{SUPPORT_CONTACT}}
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -34,9 +34,9 @@
|
|||
<div class="wrap">
|
||||
<a class="back" href="/">← На главную</a>
|
||||
<h1>Политика конфиденциальности</h1>
|
||||
<p class="updated">Действует с <span class="fill">[дата]</span></p>
|
||||
<p class="updated">Действует с {{EFFECTIVE_DATE}}</p>
|
||||
|
||||
<p>Настоящая политика описывает, какие данные собирает и как обрабатывает <span class="fill">[ФИО ИП / наименование]</span> (далее — «Оператор») при использовании Telegram-бота и сайта MBS Panel.</p>
|
||||
<p>Настоящая политика описывает, какие данные собирает и как обрабатывает {{LEGAL_NAME}} (далее — «Оператор») при использовании Telegram-бота и сайта MBS Panel.</p>
|
||||
|
||||
<h2>1. Какие данные собираются</h2>
|
||||
<ul>
|
||||
|
|
@ -58,13 +58,13 @@
|
|||
<p>Данные хранятся на серверах Оператора и не передаются третьим лицам, кроме платёжных провайдеров (ЮKassa, Platega) в объёме, необходимом для обработки оплаты, и в случаях, прямо предусмотренных законодательством РФ.</p>
|
||||
|
||||
<h2>4. Права пользователя</h2>
|
||||
<p>Пользователь вправе запросить удаление своих данных и прекращение обработки, обратившись на <span class="fill">[email/контакт поддержки]</span>. Удаление данных влечёт прекращение доступа к активным подпискам.</p>
|
||||
<p>Пользователь вправе запросить удаление своих данных и прекращение обработки, обратившись на {{SUPPORT_EMAIL}} или {{SUPPORT_CONTACT}}. Удаление данных влечёт прекращение доступа к активным подпискам.</p>
|
||||
|
||||
<h2>5. Контакты</h2>
|
||||
<p class="muted">
|
||||
<span class="fill">[ФИО / наименование]</span><br>
|
||||
Email: <span class="fill">[email]</span><br>
|
||||
Telegram: <span class="fill">[контакт поддержки]</span>
|
||||
{{LEGAL_NAME}}<br>
|
||||
Email: {{SUPPORT_EMAIL}}<br>
|
||||
Telegram: {{SUPPORT_CONTACT}}
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue