Add access request modal with Telegram notification
- Modal on login page: Name, Email, Phone with client-side validation - POST /request-access → sends Telegram message via corporate proxy - Includes IP + geo lookup (ip-api.com) - Success screen after submission, Esc/click-outside closes modal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1427,6 +1427,64 @@ def reply_single(review_id: str):
|
|||||||
return redirect(url_for("index", status="reply_sent", count=1, **redirect_params))
|
return redirect(url_for("index", status="reply_sent", count=1, **redirect_params))
|
||||||
|
|
||||||
|
|
||||||
|
TG_BOT_TOKEN = "8181219074:AAGvqWqb6t10YP4xpMOQnBq_6LrUqAFm5hM"
|
||||||
|
TG_CHAT_ID = 54986411
|
||||||
|
TG_API_URL = f"https://tel.4mont.ru/bot{TG_BOT_TOKEN}/sendMessage"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_real_ip() -> str:
|
||||||
|
forwarded_for = request.headers.get("x-forwarded-for", "")
|
||||||
|
if forwarded_for:
|
||||||
|
return forwarded_for.split(",")[0].strip()
|
||||||
|
return request.remote_addr or "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_geo(ip: str) -> str:
|
||||||
|
try:
|
||||||
|
if ip in ("unknown", "127.0.0.1", "::1") or ip.startswith("10.") or ip.startswith("192.168."):
|
||||||
|
return ""
|
||||||
|
url = f"http://ip-api.com/json/{ip}?lang=ru&fields=status,country,regionName,city"
|
||||||
|
resp = requests.get(url, timeout=5, headers={"User-Agent": "Mozilla/5.0"})
|
||||||
|
data = resp.json()
|
||||||
|
if data.get("status") == "success":
|
||||||
|
parts = [data.get("country", ""), data.get("regionName", ""), data.get("city", "")]
|
||||||
|
return ", ".join(p for p in parts if p)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _send_telegram(text: str) -> bool:
|
||||||
|
try:
|
||||||
|
resp = requests.post(TG_API_URL, json={"chat_id": TG_CHAT_ID, "text": text, "parse_mode": "HTML"}, timeout=10)
|
||||||
|
return resp.ok
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/request-access", methods=["POST"])
|
||||||
|
def request_access():
|
||||||
|
name = (request.form.get("name") or "").strip()
|
||||||
|
email = (request.form.get("email") or "").strip()
|
||||||
|
phone = (request.form.get("phone") or "").strip()
|
||||||
|
if not name or not email or not phone:
|
||||||
|
return jsonify({"ok": False, "error": "Заполните все поля"}), 400
|
||||||
|
ip = _get_real_ip()
|
||||||
|
geo = _get_geo(ip)
|
||||||
|
geo_line = f"\n📍 <b>Местоположение:</b> {geo}" if geo else ""
|
||||||
|
text = (
|
||||||
|
"🔔 <b>Новый запрос доступа к WBfeed</b>\n"
|
||||||
|
"━━━━━━━━━━━━━━━━━━━━━━\n\n"
|
||||||
|
f"👤 <b>Имя:</b> {name}\n"
|
||||||
|
f"📧 <b>Email:</b> {email}\n"
|
||||||
|
f"📱 <b>Телефон:</b> {phone}"
|
||||||
|
f"{geo_line}\n"
|
||||||
|
f"🖥 <b>IP:</b> {ip}"
|
||||||
|
)
|
||||||
|
_send_telegram(text)
|
||||||
|
return jsonify({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
@app.route("/login", methods=["GET", "POST"])
|
@app.route("/login", methods=["GET", "POST"])
|
||||||
def login():
|
def login():
|
||||||
if g.get("user"):
|
if g.get("user"):
|
||||||
|
|||||||
+140
-1
@@ -85,10 +85,149 @@
|
|||||||
<button type="submit">Войти</button>
|
<button type="submit">Войти</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p class="auth-footer">Нет аккаунта? <a href="{{ url_for('register') }}">Запросить доступ</a></p>
|
<p class="auth-footer">Нет аккаунта? <a href="#" id="open-request-modal">Запросить доступ</a></p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Модал запроса доступа -->
|
||||||
|
<div class="modal-overlay" id="request-modal">
|
||||||
|
<div class="modal-card">
|
||||||
|
<button class="modal-close" id="close-request-modal">×</button>
|
||||||
|
|
||||||
|
<div id="modal-form-view">
|
||||||
|
<div class="auth-kicker" style="margin-bottom:16px">
|
||||||
|
<img src="{{ url_for('static', filename='wb3.png') }}" class="auth-kicker-logo-img" alt="WB">
|
||||||
|
</div>
|
||||||
|
<h2 class="login-form-title">Запросить доступ</h2>
|
||||||
|
<p style="font-size:13px;color:#6b7280;margin-bottom:20px;margin-top:-10px">Оставьте контакты — мы свяжемся и откроем доступ.</p>
|
||||||
|
|
||||||
|
<div class="alert alert-error" id="modal-error" style="display:none"></div>
|
||||||
|
|
||||||
|
<form class="auth-form" id="request-form" novalidate>
|
||||||
|
<label>
|
||||||
|
Имя
|
||||||
|
<input type="text" name="name" placeholder="Иван Иванов" required autocomplete="name">
|
||||||
|
<span class="field-error" id="err-name"></span>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Email
|
||||||
|
<input type="email" name="email" placeholder="ivan@company.ru" required autocomplete="email">
|
||||||
|
<span class="field-error" id="err-email"></span>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Телефон
|
||||||
|
<input type="tel" name="phone" placeholder="+7 999 000-00-00" required autocomplete="tel">
|
||||||
|
<span class="field-error" id="err-phone"></span>
|
||||||
|
</label>
|
||||||
|
<button type="submit" id="request-submit">Отправить запрос</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="modal-success-view" style="display:none;text-align:center;padding:20px 0">
|
||||||
|
<div style="font-size:52px;margin-bottom:16px">✅</div>
|
||||||
|
<h2 style="margin-bottom:10px">Запрос отправлен!</h2>
|
||||||
|
<p style="font-size:14px;color:#6b7280">Мы получили ваши данные и скоро свяжемся с вами.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
.modal-overlay.active { opacity: 1; pointer-events: all; }
|
||||||
|
.modal-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 22px;
|
||||||
|
padding: 40px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
margin: 20px;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 32px 80px rgba(0,0,0,0.22);
|
||||||
|
transform: translateY(20px) scale(0.98);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
.modal-overlay.active .modal-card { transform: translateY(0) scale(1); }
|
||||||
|
.modal-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 14px; right: 14px;
|
||||||
|
width: 32px; height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border: none;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #9ca3af;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
box-shadow: none; padding: 0;
|
||||||
|
}
|
||||||
|
.modal-close:hover { background: #e5e7eb; color: #374151; transform: none; filter: none; }
|
||||||
|
.field-error { display: block; font-size: 11px; color: #dc2626; margin-top: 3px; min-height: 15px; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const modal = document.getElementById('request-modal');
|
||||||
|
const openBtn = document.getElementById('open-request-modal');
|
||||||
|
const closeBtn = document.getElementById('close-request-modal');
|
||||||
|
|
||||||
|
openBtn.addEventListener('click', e => { e.preventDefault(); modal.classList.add('active'); });
|
||||||
|
closeBtn.addEventListener('click', () => modal.classList.remove('active'));
|
||||||
|
modal.addEventListener('click', e => { if (e.target === modal) modal.classList.remove('active'); });
|
||||||
|
document.addEventListener('keydown', e => { if (e.key === 'Escape') modal.classList.remove('active'); });
|
||||||
|
|
||||||
|
document.getElementById('request-form').addEventListener('submit', async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = e.target;
|
||||||
|
const name = form.name.value.trim();
|
||||||
|
const email = form.email.value.trim();
|
||||||
|
const phone = form.phone.value.trim();
|
||||||
|
let valid = true;
|
||||||
|
|
||||||
|
['name','email','phone'].forEach(f => document.getElementById('err-' + f).textContent = '');
|
||||||
|
document.getElementById('modal-error').style.display = 'none';
|
||||||
|
|
||||||
|
if (!name)
|
||||||
|
{ document.getElementById('err-name').textContent = 'Введите имя'; valid = false; }
|
||||||
|
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
|
||||||
|
{ document.getElementById('err-email').textContent = 'Введите корректный email'; valid = false; }
|
||||||
|
if (!phone || phone.replace(/\D/g, '').length < 10)
|
||||||
|
{ document.getElementById('err-phone').textContent = 'Введите корректный телефон'; valid = false; }
|
||||||
|
if (!valid) return;
|
||||||
|
|
||||||
|
const btn = document.getElementById('request-submit');
|
||||||
|
btn.disabled = true; btn.textContent = 'Отправка…';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/request-access', { method: 'POST', body: new FormData(form) });
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.ok) {
|
||||||
|
document.getElementById('modal-form-view').style.display = 'none';
|
||||||
|
document.getElementById('modal-success-view').style.display = 'block';
|
||||||
|
} else {
|
||||||
|
document.getElementById('modal-error').textContent = data.error || 'Ошибка. Попробуйте ещё раз.';
|
||||||
|
document.getElementById('modal-error').style.display = 'block';
|
||||||
|
btn.disabled = false; btn.textContent = 'Отправить запрос';
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
document.getElementById('modal-error').textContent = 'Ошибка сети. Попробуйте ещё раз.';
|
||||||
|
document.getElementById('modal-error').style.display = 'block';
|
||||||
|
btn.disabled = false; btn.textContent = 'Отправить запрос';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user