Add Telegram approval flow: inline buttons, user creation, email notifications
This commit is contained in:
@@ -37,3 +37,11 @@ SERVICE_ICONS_DIR = Path("static/service-icons")
|
|||||||
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
|
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
|
||||||
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
|
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
|
||||||
TELEGRAM_API_URL = os.getenv("TELEGRAM_API_URL", "https://api.telegram.org/bot")
|
TELEGRAM_API_URL = os.getenv("TELEGRAM_API_URL", "https://api.telegram.org/bot")
|
||||||
|
|
||||||
|
SMTP_HOST = os.getenv("SMTP_HOST", "mail.hosting.reg.ru")
|
||||||
|
SMTP_PORT = int(os.getenv("SMTP_PORT", "465"))
|
||||||
|
SMTP_USERNAME = os.getenv("SMTP_USERNAME", "")
|
||||||
|
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "")
|
||||||
|
SMTP_FROM_EMAIL = os.getenv("SMTP_FROM_EMAIL", "stand@4mont.ru")
|
||||||
|
SMTP_FROM_NAME = os.getenv("SMTP_FROM_NAME", "\u0418\u043d\u0444\u0440\u0430\u0441\u0442\u0443\u043a\u0442\u0443\u0440\u043d\u044b\u0439 \u043f\u043e\u043b\u0438\u0433\u043e\u043d MONT")
|
||||||
|
PORTAL_URL = os.getenv("PORTAL_URL", "https://stend.4mont.ru")
|
||||||
|
|||||||
+271
-8
@@ -27,11 +27,13 @@ from config import (
|
|||||||
MAX_ACTIVE_SERVICES_PER_USER, PUBLIC_HOST, SESSION_IDLE_SECONDS,
|
MAX_ACTIVE_SERVICES_PER_USER, PUBLIC_HOST, SESSION_IDLE_SECONDS,
|
||||||
WEB_POOL_BUFFER, WEB_POOL_SIZE,
|
WEB_POOL_BUFFER, WEB_POOL_SIZE,
|
||||||
TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, TELEGRAM_API_URL,
|
TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, TELEGRAM_API_URL,
|
||||||
|
SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD,
|
||||||
|
SMTP_FROM_EMAIL, SMTP_FROM_NAME, PORTAL_URL,
|
||||||
)
|
)
|
||||||
from database import get_db
|
from database import get_db
|
||||||
from models import (
|
from models import (
|
||||||
AuditLog, Category, RdpSlot, Service, ServiceCategory, ServiceType,
|
AuditLog, Category, RdpSlot, Service, ServiceCategory, ServiceType,
|
||||||
SessionModel, SessionStatus, User, UserServiceAccess,
|
PendingAccessRequest, SessionModel, SessionStatus, User, UserServiceAccess,
|
||||||
)
|
)
|
||||||
from utils import (
|
from utils import (
|
||||||
audit, ensure_icons_dir, format_service_comment, log_event, normalize_web_target,
|
audit, ensure_icons_dir, format_service_comment, log_event, normalize_web_target,
|
||||||
@@ -90,6 +92,56 @@ def _get_geo(ip: str) -> str:
|
|||||||
pass
|
pass
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
import secrets as _secrets
|
||||||
|
import string as _string
|
||||||
|
import smtplib as _smtplib
|
||||||
|
import ssl as _ssl
|
||||||
|
import json as _json2
|
||||||
|
from email.mime.multipart import MIMEMultipart as _MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText as _MIMEText
|
||||||
|
|
||||||
|
def _generate_password(length: int = 10) -> str:
|
||||||
|
alphabet = _string.ascii_letters + _string.digits
|
||||||
|
while True:
|
||||||
|
pwd = ''.join(_secrets.choice(alphabet) for _ in range(length))
|
||||||
|
if (any(c.isupper() for c in pwd) and any(c.islower() for c in pwd)
|
||||||
|
and any(c.isdigit() for c in pwd)):
|
||||||
|
return pwd
|
||||||
|
|
||||||
|
def _send_email(to: str, subject: str, html_body: str) -> None:
|
||||||
|
msg = _MIMEMultipart("alternative")
|
||||||
|
msg["Subject"] = subject
|
||||||
|
msg["From"] = f"{SMTP_FROM_NAME} <{SMTP_FROM_EMAIL}>"
|
||||||
|
msg["To"] = to
|
||||||
|
msg.attach(_MIMEText(html_body, "html", "utf-8"))
|
||||||
|
ctx = _ssl.create_default_context()
|
||||||
|
with _smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, context=ctx) as srv:
|
||||||
|
srv.login(SMTP_USERNAME, SMTP_PASSWORD)
|
||||||
|
srv.sendmail(SMTP_FROM_EMAIL, to, msg.as_string())
|
||||||
|
|
||||||
|
def _tg_api(method: str, payload: dict) -> dict:
|
||||||
|
import urllib.request as _ur
|
||||||
|
import json as _j
|
||||||
|
url = f"{TELEGRAM_API_URL}{TELEGRAM_BOT_TOKEN}/{method}"
|
||||||
|
data = _j.dumps(payload).encode()
|
||||||
|
req = _ur.Request(url, data=data, headers={"Content-Type": "application/json"})
|
||||||
|
with _ur.urlopen(req, timeout=10) as r:
|
||||||
|
return _j.loads(r.read())
|
||||||
|
|
||||||
|
def _make_approval_keyboard(req_id: str) -> dict:
|
||||||
|
return {
|
||||||
|
"inline_keyboard": [
|
||||||
|
[
|
||||||
|
{"text": "7 дней", "callback_data": f"a7_{req_id}"},
|
||||||
|
{"text": "14 дней", "callback_data": f"a14_{req_id}"},
|
||||||
|
{"text": "30 дней", "callback_data": f"a30_{req_id}"},
|
||||||
|
{"text": "90 дней", "callback_data": f"a90_{req_id}"},
|
||||||
|
],
|
||||||
|
[{"text": "Отказать", "callback_data": f"r_{req_id}"}],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
app = FastAPI(title="MONT - инфрастуктурный полигон")
|
app = FastAPI(title="MONT - инфрастуктурный полигон")
|
||||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||||
|
|
||||||
@@ -443,6 +495,210 @@ def privacy_page():
|
|||||||
return RedirectResponse(url="https://www.mont.ru/ru-ru/privacy", status_code=301)
|
return RedirectResponse(url="https://www.mont.ru/ru-ru/privacy", status_code=301)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/telegram-webhook", include_in_schema=False)
|
||||||
|
async def telegram_webhook(request: Request, db: Session = Depends(get_db)):
|
||||||
|
import json as _jw
|
||||||
|
import datetime as _dt2
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
except Exception:
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
cq = data.get("callback_query")
|
||||||
|
if not cq:
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
cq_id = cq["id"]
|
||||||
|
cb_data = cq.get("data", "")
|
||||||
|
chat_id = cq["message"]["chat"]["id"]
|
||||||
|
msg_id = cq["message"]["message_id"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
_tg_api("answerCallbackQuery", {"callback_query_id": cq_id})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# parse callback_data: a7_ID, a14_ID, a30_ID, a90_ID, r_ID
|
||||||
|
import re as _rew
|
||||||
|
approve_match = _rew.match(r'^a(\d+)_(.+)$', cb_data)
|
||||||
|
reject_match = _rew.match(r'^r_(.+)$', cb_data)
|
||||||
|
|
||||||
|
if not approve_match and not reject_match:
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
req_id = approve_match.group(2) if approve_match else reject_match.group(1)
|
||||||
|
pending = db.get(PendingAccessRequest, req_id)
|
||||||
|
if not pending:
|
||||||
|
try:
|
||||||
|
_tg_api("editMessageText", {
|
||||||
|
"chat_id": chat_id, "message_id": msg_id,
|
||||||
|
"text": "Запрос не найден (возможно уже обработан).",
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
if pending.status != "pending":
|
||||||
|
try:
|
||||||
|
_tg_api("editMessageText", {
|
||||||
|
"chat_id": chat_id, "message_id": msg_id,
|
||||||
|
"text": f"Запрос уже обработан: {pending.status}.",
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
products = _jw.loads(pending.products_json or "[]")
|
||||||
|
portal_url = PORTAL_URL
|
||||||
|
|
||||||
|
if approve_match:
|
||||||
|
days = int(approve_match.group(1))
|
||||||
|
password = _generate_password()
|
||||||
|
username = pending.email
|
||||||
|
|
||||||
|
# ensure username unique
|
||||||
|
if db.scalar(select(User).where(User.username == username)):
|
||||||
|
username = pending.email.split("@")[0] + "_" + _secrets.token_hex(3)
|
||||||
|
|
||||||
|
expires = _dt2.datetime.now(_dt2.timezone.utc) + _dt2.timedelta(days=days)
|
||||||
|
parts = pending.name.strip().split(None, 1)
|
||||||
|
new_user = User(
|
||||||
|
username=username,
|
||||||
|
password_hash=hash_password(password),
|
||||||
|
expires_at=expires,
|
||||||
|
active=True,
|
||||||
|
is_admin=False,
|
||||||
|
first_name=parts[0] if parts else "",
|
||||||
|
last_name=parts[1] if len(parts) > 1 else "",
|
||||||
|
)
|
||||||
|
db.add(new_user)
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
# assign requested services
|
||||||
|
if products:
|
||||||
|
from sqlalchemy import func as _func
|
||||||
|
matched = db.scalars(
|
||||||
|
select(Service).where(
|
||||||
|
func.lower(Service.name).in_([p.lower() for p in products]),
|
||||||
|
Service.active == True,
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
for svc in matched:
|
||||||
|
db.add(UserServiceAccess(user_id=new_user.id, service_id=svc.id))
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# send approval email
|
||||||
|
products_html = ""
|
||||||
|
if products:
|
||||||
|
items = "".join(f"<li>{p}</li>" for p in products)
|
||||||
|
products_html = f"<p style='margin:16px 0 6px'><b>Предоставлен доступ к продуктам:</b></p><ul style='margin:0;padding-left:20px;color:#c8d8ea'>{items}</ul>"
|
||||||
|
|
||||||
|
html_email = f"""<!DOCTYPE html>
|
||||||
|
<html lang="ru"><head><meta charset="utf-8"/></head>
|
||||||
|
<body style="margin:0;padding:0;background:#0a1929;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0"><tr><td align="center" style="padding:40px 20px">
|
||||||
|
<table width="560" cellpadding="0" cellspacing="0" style="background:linear-gradient(150deg,#0b1a2e,#0d2040);border-radius:16px;overflow:hidden;border:1px solid rgba(255,255,255,0.08)">
|
||||||
|
<tr><td style="padding:32px 36px 0">
|
||||||
|
<img src="{portal_url}/static/logo.png" alt="MONT" height="40" style="margin-bottom:24px"/><br>
|
||||||
|
<h1 style="margin:0 0 8px;font-size:22px;color:#e8f1fb">Доступ к Инфраструктурному полигону MONT</h1>
|
||||||
|
<p style="margin:0 0 24px;color:#7a9abd;font-size:14px">Ваш запрос одобрен</p>
|
||||||
|
<hr style="border:none;border-top:1px solid rgba(255,255,255,0.08);margin:0 0 24px"/>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="padding:0 36px">
|
||||||
|
<p style="color:#c8d8ea;font-size:15px;line-height:1.6;margin:0 0 20px">Здравствуйте, <b>{pending.name}</b>!<br>
|
||||||
|
Вам предоставлен доступ к полигону на <b>{days} {'день' if days==1 else 'дня' if days<5 else 'дней'}</b>.</p>
|
||||||
|
<table width="100%" cellpadding="12" cellspacing="0" style="background:rgba(255,255,255,0.04);border-radius:10px;border:1px solid rgba(255,255,255,0.07)">
|
||||||
|
<tr><td style="color:#7a9abd;font-size:13px;width:40%">Адрес портала</td>
|
||||||
|
<td><a href="{portal_url}" style="color:#5b9bd5;font-size:14px">{portal_url}</a></td></tr>
|
||||||
|
<tr><td style="color:#7a9abd;font-size:13px;border-top:1px solid rgba(255,255,255,0.06)">Логин</td>
|
||||||
|
<td style="border-top:1px solid rgba(255,255,255,0.06);color:#e8f1fb;font-size:14px;font-family:monospace">{username}</td></tr>
|
||||||
|
<tr><td style="color:#7a9abd;font-size:13px;border-top:1px solid rgba(255,255,255,0.06)">Пароль</td>
|
||||||
|
<td style="border-top:1px solid rgba(255,255,255,0.06);color:#e8f1fb;font-size:14px;font-family:monospace">{password}</td></tr>
|
||||||
|
<tr><td style="color:#7a9abd;font-size:13px;border-top:1px solid rgba(255,255,255,0.06)">Доступ до</td>
|
||||||
|
<td style="border-top:1px solid rgba(255,255,255,0.06);color:#e8f1fb;font-size:14px">{expires.strftime('%d.%m.%Y')}</td></tr>
|
||||||
|
</table>
|
||||||
|
{products_html}
|
||||||
|
<div style="margin:28px 0">
|
||||||
|
<a href="{portal_url}" style="display:inline-block;padding:12px 28px;background:linear-gradient(135deg,#1a5db5,#2d8cf0);color:#fff;text-decoration:none;border-radius:8px;font-size:15px;font-weight:600">Войти в полигон</a>
|
||||||
|
</div>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="padding:20px 36px 28px;color:#4a6a8a;font-size:12px;border-top:1px solid rgba(255,255,255,0.06);line-height:1.6">
|
||||||
|
Если у вас возникли вопросы, свяжитесь с вашим менеджером MONT или напишите на <a href="mailto:mont@mont.ru" style="color:#5b9bd5">mont@mont.ru</a>
|
||||||
|
</td></tr>
|
||||||
|
</table></td></tr></table>
|
||||||
|
</body></html>"""
|
||||||
|
|
||||||
|
pending.status = "approved"
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
try:
|
||||||
|
_send_email(pending.email, "Доступ к Инфраструктурному полигону MONT", html_email)
|
||||||
|
email_status = "Email отправлен"
|
||||||
|
except Exception as ex:
|
||||||
|
log_event("email_send_error", error=str(ex))
|
||||||
|
email_status = f"Ошибка отправки email: {ex}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
_tg_api("editMessageText", {
|
||||||
|
"chat_id": chat_id, "message_id": msg_id,
|
||||||
|
"text": (
|
||||||
|
f"Одобрено на {days} дней\n"
|
||||||
|
f"Логин: `{username}`\n"
|
||||||
|
f"Пароль: `{password}`\n"
|
||||||
|
f"{email_status}"
|
||||||
|
),
|
||||||
|
"parse_mode": "Markdown",
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif reject_match:
|
||||||
|
manager_contact = pending.manager if pending.manager else "менеджера MONT"
|
||||||
|
html_email = f"""<!DOCTYPE html>
|
||||||
|
<html lang="ru"><head><meta charset="utf-8"/></head>
|
||||||
|
<body style="margin:0;padding:0;background:#0a1929;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0"><tr><td align="center" style="padding:40px 20px">
|
||||||
|
<table width="560" cellpadding="0" cellspacing="0" style="background:linear-gradient(150deg,#0b1a2e,#0d2040);border-radius:16px;overflow:hidden;border:1px solid rgba(255,255,255,0.08)">
|
||||||
|
<tr><td style="padding:32px 36px 0">
|
||||||
|
<img src="{portal_url}/static/logo.png" alt="MONT" height="40" style="margin-bottom:24px"/><br>
|
||||||
|
<h1 style="margin:0 0 8px;font-size:22px;color:#e8f1fb">Запрос на доступ к полигону MONT</h1>
|
||||||
|
<hr style="border:none;border-top:1px solid rgba(255,255,255,0.08);margin:16px 0 24px"/>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="padding:0 36px">
|
||||||
|
<p style="color:#c8d8ea;font-size:15px;line-height:1.7;margin:0 0 20px">Здравствуйте, <b>{pending.name}</b>!<br><br>
|
||||||
|
К сожалению, на данный момент мы не можем предоставить доступ к полигону.</p>
|
||||||
|
<p style="color:#c8d8ea;font-size:15px;line-height:1.7;margin:0 0 24px">
|
||||||
|
Для уточнения деталей, пожалуйста, свяжитесь с <b>{manager_contact}</b>.<br>
|
||||||
|
Если вы не знаете, кто ваш менеджер, напишите нам на <a href="mailto:mont@mont.ru" style="color:#5b9bd5">mont@mont.ru</a> — мы поможем.</p>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="padding:20px 36px 28px;color:#4a6a8a;font-size:12px;border-top:1px solid rgba(255,255,255,0.06)">
|
||||||
|
С уважением, команда MONT
|
||||||
|
</td></tr>
|
||||||
|
</table></td></tr></table>
|
||||||
|
</body></html>"""
|
||||||
|
|
||||||
|
pending.status = "rejected"
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
try:
|
||||||
|
_send_email(pending.email, "Запрос на доступ к полигону MONT", html_email)
|
||||||
|
email_status = "Email отправлен"
|
||||||
|
except Exception as ex:
|
||||||
|
log_event("email_send_error", error=str(ex))
|
||||||
|
email_status = f"Ошибка отправки email: {ex}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
_tg_api("editMessageText", {
|
||||||
|
"chat_id": chat_id, "message_id": msg_id,
|
||||||
|
"text": f"Отклонено. {email_status}",
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
@app.get("/robots.txt", include_in_schema=False)
|
@app.get("/robots.txt", include_in_schema=False)
|
||||||
def robots_txt():
|
def robots_txt():
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
@@ -534,19 +790,26 @@ async def request_access(request: Request, db: Session = Depends(get_db)):
|
|||||||
log_event("telegram_not_configured")
|
log_event("telegram_not_configured")
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
# save pending request
|
||||||
|
import json as _j2
|
||||||
|
req_id = _secrets.token_urlsafe(8)[:12]
|
||||||
|
pending = PendingAccessRequest(
|
||||||
|
id=req_id, name=name, company=company, email=email,
|
||||||
|
phone=phone, manager=manager,
|
||||||
|
products_json=_j2.dumps(products, ensure_ascii=False),
|
||||||
|
)
|
||||||
|
db.add(pending)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payload = _json.dumps({
|
_tg_api("sendMessage", {
|
||||||
"chat_id": TELEGRAM_CHAT_ID,
|
"chat_id": TELEGRAM_CHAT_ID,
|
||||||
"text": text,
|
"text": text,
|
||||||
"parse_mode": "Markdown",
|
"parse_mode": "Markdown",
|
||||||
}).encode()
|
"reply_markup": _make_approval_keyboard(req_id),
|
||||||
url = f"{TELEGRAM_API_URL}{TELEGRAM_BOT_TOKEN}/sendMessage"
|
})
|
||||||
req = _urllib_request.Request(url, data=payload, headers={"Content-Type": "application/json"})
|
|
||||||
with _urllib_request.urlopen(req, timeout=10) as resp:
|
|
||||||
resp.read()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_event("telegram_send_error", error=str(e))
|
log_event("telegram_send_error", error=str(e))
|
||||||
raise HTTPException(status_code=502, detail="Ошибка отправки запроса")
|
|
||||||
|
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|||||||
@@ -115,3 +115,17 @@ class AuditLog(Base):
|
|||||||
action: Mapped[str] = mapped_column(String(128), index=True)
|
action: Mapped[str] = mapped_column(String(128), index=True)
|
||||||
details: Mapped[str] = mapped_column(Text)
|
details: Mapped[str] = mapped_column(Text)
|
||||||
created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=lambda: dt.datetime.now(dt.timezone.utc), index=True)
|
created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=lambda: dt.datetime.now(dt.timezone.utc), index=True)
|
||||||
|
|
||||||
|
|
||||||
|
class PendingAccessRequest(Base):
|
||||||
|
__tablename__ = "pending_access_requests"
|
||||||
|
|
||||||
|
id: Mapped[str] = mapped_column(String(12), primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(256))
|
||||||
|
company: Mapped[str] = mapped_column(String(256))
|
||||||
|
email: Mapped[str] = mapped_column(String(256))
|
||||||
|
phone: Mapped[str] = mapped_column(String(64))
|
||||||
|
manager: Mapped[str] = mapped_column(String(256), default="")
|
||||||
|
products_json: Mapped[str] = mapped_column(Text, default="[]")
|
||||||
|
status: Mapped[str] = mapped_column(String(16), default="pending")
|
||||||
|
created_at: Mapped[dt.datetime] = mapped_column(DateTime(timezone=True), default=lambda: dt.datetime.now(dt.timezone.utc))
|
||||||
|
|||||||
@@ -60,6 +60,13 @@ services:
|
|||||||
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
|
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
|
||||||
TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID:-}
|
TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID:-}
|
||||||
TELEGRAM_API_URL: ${TELEGRAM_API_URL:-https://api.telegram.org/bot}
|
TELEGRAM_API_URL: ${TELEGRAM_API_URL:-https://api.telegram.org/bot}
|
||||||
|
SMTP_HOST: ${SMTP_HOST:-mail.hosting.reg.ru}
|
||||||
|
SMTP_PORT: ${SMTP_PORT:-465}
|
||||||
|
SMTP_USERNAME: ${SMTP_USERNAME:-}
|
||||||
|
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
|
||||||
|
SMTP_FROM_EMAIL: ${SMTP_FROM_EMAIL:-}
|
||||||
|
SMTP_FROM_NAME: ${SMTP_FROM_NAME:-}
|
||||||
|
PORTAL_URL: ${PORTAL_URL:-https://stend.4mont.ru}
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
volumes:
|
volumes:
|
||||||
@@ -113,6 +120,13 @@ services:
|
|||||||
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
|
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
|
||||||
TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID:-}
|
TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID:-}
|
||||||
TELEGRAM_API_URL: ${TELEGRAM_API_URL:-https://api.telegram.org/bot}
|
TELEGRAM_API_URL: ${TELEGRAM_API_URL:-https://api.telegram.org/bot}
|
||||||
|
SMTP_HOST: ${SMTP_HOST:-mail.hosting.reg.ru}
|
||||||
|
SMTP_PORT: ${SMTP_PORT:-465}
|
||||||
|
SMTP_USERNAME: ${SMTP_USERNAME:-}
|
||||||
|
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
|
||||||
|
SMTP_FROM_EMAIL: ${SMTP_FROM_EMAIL:-}
|
||||||
|
SMTP_FROM_NAME: ${SMTP_FROM_NAME:-}
|
||||||
|
PORTAL_URL: ${PORTAL_URL:-https://stend.4mont.ru}
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
Reference in New Issue
Block a user