diff --git a/app/main.py b/app/main.py
index a66fa47..0712553 100644
--- a/app/main.py
+++ b/app/main.py
@@ -142,6 +142,223 @@ def _make_approval_keyboard(req_id: str) -> dict:
]
}
+
+_tg_poll_offset: int = 0
+_tg_poll_lock_file = None
+
+async def _telegram_poll_loop():
+ """Poll Telegram for callback_query updates every 5 minutes."""
+ import asyncio as _asyncio
+ import json as _jp
+ global _tg_poll_offset
+ await _asyncio.sleep(10) # wait for app to fully start
+ while True:
+ try:
+ result = _tg_api("getUpdates", {
+ "offset": _tg_poll_offset,
+ "timeout": 0,
+ "allowed_updates": ["callback_query"],
+ })
+ updates = result.get("result", [])
+ for upd in updates:
+ _tg_poll_offset = upd["update_id"] + 1
+ cq = upd.get("callback_query")
+ if cq:
+ try:
+ await _process_callback_query(cq)
+ except Exception as ex:
+ log_event("tg_callback_error", error=str(ex))
+ except Exception as ex:
+ log_event("tg_poll_error", error=str(ex))
+ await _asyncio.sleep(30) # 30 seconds
+
+async def _process_callback_query(cq: dict):
+ import json as _jc
+ import datetime as _dtc
+ import re as _rec
+ from database import SessionLocal as _SL
+
+ 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
+
+ approve_match = _rec.match(r'^a(\d+)_(.+)$', cb_data)
+ reject_match = _rec.match(r'^r_(.+)$', cb_data)
+
+ if not approve_match and not reject_match:
+ return
+
+ req_id = approve_match.group(2) if approve_match else reject_match.group(1)
+
+ db = _SL()
+ try:
+ pending = db.get(PendingAccessRequest, req_id)
+ if not pending:
+ _tg_api("editMessageText", {
+ "chat_id": chat_id, "message_id": msg_id,
+ "text": "Запрос не найден (возможно уже обработан).",
+ })
+ return
+
+ if pending.status != "pending":
+ _tg_api("editMessageText", {
+ "chat_id": chat_id, "message_id": msg_id,
+ "text": f"Запрос уже обработан: {pending.status}.",
+ })
+ return
+
+ products = _jc.loads(pending.products_json or "[]")
+ portal_url = pending.portal_url or PORTAL_URL
+
+ if approve_match:
+ days = int(approve_match.group(1))
+ password = _generate_password()
+ username = pending.email
+
+ if db.scalar(select(User).where(User.username == username)):
+ import secrets as _sec
+ username = pending.email.split("@")[0] + "_" + _sec.token_hex(3)
+
+ expires = _dtc.datetime.now(_dtc.timezone.utc) + _dtc.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()
+
+ if products:
+ from sqlalchemy import func as _func2
+ matched = db.scalars(
+ select(Service).where(
+ _func2.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()
+
+ products_html = ""
+ if products:
+ items = "".join(f"
{p}" for p in products)
+ products_html = (
+ 'Предоставлен доступ к продуктам:
'
+ f''
+ )
+
+ day_word = "день" if days == 1 else ("дня" if days < 5 else "дней")
+ html_email = f"""
+
+
+
+
+
+ 
+ Доступ к Инфраструктурному полигону MONT
+ Ваш запрос одобрен
+
+ |
+ |
+ Здравствуйте, {pending.name}!
+ Вам предоставлен доступ к полигону на {days} {day_word}.
+
+ | Адрес портала |
+ {portal_url} |
+ | Логин |
+ {username} |
+ | Пароль |
+ {password} |
+ | Доступ до |
+ {expires.strftime("%d.%m.%Y")} |
+
+ {products_html}
+
+ |
+ |
+ Если у вас возникли вопросы, свяжитесь с вашим менеджером MONT или напишите на mont@mont.ru
+ |
+ |
+"""
+
+ 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}"
+
+ _tg_api("editMessageText", {
+ "chat_id": chat_id, "message_id": msg_id,
+ "text": (
+ f"✅ Одобрено на {days} {day_word}\n"
+ f"👤 Логин: `{username}`\n"
+ f"🔑 Пароль: `{password}`\n"
+ f"📧 {email_status}"
+ ),
+ "parse_mode": "Markdown",
+ })
+
+ elif reject_match:
+ manager_contact = pending.manager if pending.manager else "менеджера MONT"
+ html_email = f"""
+
+
+
+
+
+ 
+ Запрос на доступ к полигону MONT
+
+ |
+ |
+ Здравствуйте, {pending.name}!
+ К сожалению, на данный момент мы не можем предоставить доступ к полигону.
+
+ Для уточнения деталей свяжитесь с {manager_contact}.
+ Если не знаете кто ваш менеджер — напишите на mont@mont.ru.
+ |
+ |
+ С уважением, команда MONT
+ |
+ |
+"""
+
+ 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}"
+
+ _tg_api("editMessageText", {
+ "chat_id": chat_id, "message_id": msg_id,
+ "text": f"❌ Отклонено. {email_status}",
+ })
+ finally:
+ db.close()
+
app = FastAPI(title="MONT - инфрастуктурный полигон")
app.mount("/static", StaticFiles(directory="static"), name="static")
@@ -241,8 +458,17 @@ async def mobile_block_middleware(request: Request, call_next):
@app.on_event("startup")
-def startup_event():
+async def startup_event():
+ global _tg_poll_lock_file
on_startup()
+ import asyncio as _aio, fcntl as _fcntl
+ _lf = open("/tmp/portal-tg-poll.lock", "w")
+ try:
+ _fcntl.flock(_lf.fileno(), _fcntl.LOCK_EX | _fcntl.LOCK_NB)
+ _tg_poll_lock_file = _lf
+ _aio.create_task(_telegram_poll_loop())
+ except BlockingIOError:
+ _lf.close()
@app.get("/", response_class=HTMLResponse)
@@ -550,7 +776,7 @@ async def telegram_webhook(request: Request, db: Session = Depends(get_db)):
return {"ok": True}
products = _jw.loads(pending.products_json or "[]")
- portal_url = PORTAL_URL
+ portal_url = pending.portal_url or PORTAL_URL
if approve_match:
days = int(approve_match.group(1))
@@ -593,7 +819,7 @@ async def telegram_webhook(request: Request, db: Session = Depends(get_db)):
products_html = ""
if products:
items = "".join(f"{p}" for p in products)
- products_html = f"Предоставлен доступ к продуктам:
"
+ products_html = f"Предоставлен доступ к продуктам:
"
html_email = f"""
@@ -601,7 +827,7 @@ async def telegram_webhook(request: Request, db: Session = Depends(get_db)):
- 
+ 
Доступ к Инфраструктурному полигону MONT
Ваш запрос одобрен
@@ -662,7 +888,7 @@ async def telegram_webhook(request: Request, db: Session = Depends(get_db)):
- 
+ 
Запрос на доступ к полигону MONT
|
@@ -793,10 +1019,16 @@ async def request_access(request: Request, db: Session = Depends(get_db)):
# save pending request
import json as _j2
req_id = _secrets.token_urlsafe(8)[:12]
+ _origin = request.headers.get('origin', '')
+ if 'stand.mont.ru' in _origin:
+ _req_portal_url = 'https://stand.mont.ru'
+ else:
+ _req_portal_url = PORTAL_URL
pending = PendingAccessRequest(
id=req_id, name=name, company=company, email=email,
phone=phone, manager=manager,
products_json=_j2.dumps(products, ensure_ascii=False),
+ portal_url=_req_portal_url,
)
db.add(pending)
db.commit()
diff --git a/app/models.py b/app/models.py
index dac7a96..96ebb9a 100644
--- a/app/models.py
+++ b/app/models.py
@@ -127,5 +127,6 @@ class PendingAccessRequest(Base):
phone: Mapped[str] = mapped_column(String(64))
manager: Mapped[str] = mapped_column(String(256), default="")
products_json: Mapped[str] = mapped_column(Text, default="[]")
+ portal_url: Mapped[str] = mapped_column(String(256), 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))
diff --git a/app/runtime.py b/app/runtime.py
index 16b4107..8b74e10 100644
--- a/app/runtime.py
+++ b/app/runtime.py
@@ -808,6 +808,7 @@ def ensure_schema_compatibility() -> None:
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS svc_password VARCHAR(256) NOT NULL DEFAULT ''"))
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS svc_cred_hint TEXT NOT NULL DEFAULT ''"))
conn.execute(text("ALTER TABLE services ADD COLUMN IF NOT EXISTS icon_path TEXT NOT NULL DEFAULT ''"))
+ conn.execute(text("ALTER TABLE pending_access_requests ADD COLUMN IF NOT EXISTS portal_url VARCHAR(256) NOT NULL DEFAULT ''"))
conn.execute(
text(
"""
|
| |