diff --git a/app/config.py b/app/config.py index b79df52..63e7be4 100644 --- a/app/config.py +++ b/app/config.py @@ -37,3 +37,11 @@ SERVICE_ICONS_DIR = Path("static/service-icons") TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "") TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "") 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") diff --git a/app/main.py b/app/main.py index 4a6bc44..a66fa47 100644 --- a/app/main.py +++ b/app/main.py @@ -27,11 +27,13 @@ from config import ( MAX_ACTIVE_SERVICES_PER_USER, PUBLIC_HOST, SESSION_IDLE_SECONDS, WEB_POOL_BUFFER, WEB_POOL_SIZE, 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 models import ( AuditLog, Category, RdpSlot, Service, ServiceCategory, ServiceType, - SessionModel, SessionStatus, User, UserServiceAccess, + PendingAccessRequest, SessionModel, SessionStatus, User, UserServiceAccess, ) from utils import ( audit, ensure_icons_dir, format_service_comment, log_event, normalize_web_target, @@ -90,6 +92,56 @@ def _get_geo(ip: str) -> str: pass 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.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) + +@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"
Предоставлен доступ к продуктам:
| + |
| + |