11 Commits

Author SHA1 Message Date
ruslan f5c53f01bf Reduce login logo size 72px -> 54px 2026-08-03 14:01:24 +00:00
ruslan de6753b077 Increase login page logo size 36px -> 72px 2026-08-03 13:54:50 +00:00
ruslan cbfafe4471 Replace logo with new MONT|4MONT design, white version fitted to existing size 2026-08-03 13:39:04 +00:00
ruslan 3251ce3380 Fix favicon HEAD 405 and unify favicon design across ico/png/svg 2026-08-03 13:00:29 +00:00
ruslan b7ec4b81a5 fix: regenerate favicon sizes from original favicon.png (not logo) 2026-07-26 15:30:38 +00:00
ruslan 06643e2d50 seo: proper favicon sizes (32/192/512px) + web app manifest for Yandex 2026-07-26 13:21:59 +00:00
ruslan 0fe2f7d9a9 ui: replace logo with logo2.png, bump cache buster to v=3 2026-07-26 13:10:32 +00:00
ruslan 6f7f28408d fix: iframe probe accepts only 2xx (reject portal 404 with X-Frame-Options) 2026-07-23 12:24:20 +00:00
ruslan c3efdc5a8f security: add brute-force rate limiting to /login
5 failed attempts per IP within 5 minutes triggers 15-minute block.
Counter resets on successful login. State is per-worker (in-memory).
2026-07-23 11:41:08 +00:00
ruslan 3092645408 security: disable FastAPI docs/redoc/openapi endpoints in production 2026-07-23 11:38:13 +00:00
ruslan fc4b73c536 chore: remove unused files
- app/runtime.py.bak, app/static/style.css.bak — temp backups
- app/static/service-icons/svc_*_2026042*.png, svc_*_20260306*.png — old icon versions superseded by newer uploads
- app/static/parallax/ — images not referenced in any template
2026-07-23 11:20:34 +00:00
28 changed files with 85 additions and 59 deletions
+42
View File
@@ -100,3 +100,45 @@ def has_access(db: Session, user_id: int, service_id: int) -> bool:
UserServiceAccess.service_id == service_id,
)
return db.scalar(q) is not None
import threading
import time
_login_attempts: dict = {}
_login_lock = threading.Lock()
_LOGIN_MAX_ATTEMPTS = 5
_LOGIN_WINDOW = 300 # сброс счётчика через 5 минут
_LOGIN_BLOCK = 900 # блокировка на 15 минут
def check_login_rate_limit(ip: str) -> Optional[float]:
"""Возвращает blocked_until (time.monotonic) если IP заблокирован, иначе None."""
now = time.monotonic()
with _login_lock:
entry = _login_attempts.get(ip)
if not entry:
return None
if entry["blocked_until"] > now:
return entry["blocked_until"]
if now - entry["first"] > _LOGIN_WINDOW:
del _login_attempts[ip]
return None
def record_login_failure(ip: str) -> None:
now = time.monotonic()
with _login_lock:
entry = _login_attempts.get(ip)
if not entry or now - entry["first"] > _LOGIN_WINDOW:
_login_attempts[ip] = {"count": 1, "first": now, "blocked_until": 0.0}
entry = _login_attempts[ip]
else:
entry["count"] += 1
if entry["count"] >= _LOGIN_MAX_ATTEMPTS:
entry["blocked_until"] = now + _LOGIN_BLOCK
def record_login_success(ip: str) -> None:
with _login_lock:
_login_attempts.pop(ip, None)
+12 -3
View File
@@ -42,6 +42,9 @@ from utils import (
from auth import (
get_current_user, has_access, issue_auth_cookie, issue_csrf_cookie,
require_admin, require_user, user_is_valid, validate_csrf, verify_password, hash_password,
check_login_rate_limit,
record_login_failure,
record_login_success,
)
from runtime import (
acquire_universal_slot, acquire_web_pool_slot, allocator_lock,
@@ -380,7 +383,7 @@ async def _process_callback_query(cq: dict):
finally:
db.close()
app = FastAPI(title="MONT - инфрастуктурный полигон")
app = FastAPI(title="MONT - инфрастуктурный полигон", docs_url=None, redoc_url=None, openapi_url=None)
app.mount("/static", StaticFiles(directory="static"), name="static")
@@ -959,7 +962,7 @@ async def telegram_webhook(request: Request, db: Session = Depends(get_db)):
return {"ok": True}
@app.get("/favicon.ico", include_in_schema=False)
@app.api_route("/favicon.ico", methods=["GET", "HEAD"], include_in_schema=False)
def favicon():
from fastapi.responses import FileResponse
return FileResponse("static/favicon.ico", media_type="image/x-icon")
@@ -1160,8 +1163,13 @@ def login(
if not cookie_csrf or csrf_token != cookie_csrf:
raise HTTPException(status_code=403, detail="CSRF failed")
ip = _get_real_ip(request)
if check_login_rate_limit(ip):
raise HTTPException(status_code=429, detail="Слишком много попыток входа. Попробуйте через 15 минут.")
user = db.scalar(select(User).where(User.username == username))
if not user or not verify_password(password, user.password_hash):
record_login_failure(ip)
csrf = request.cookies.get(CSRF_COOKIE) or secrets.token_urlsafe(24)
response = templates.TemplateResponse(
"login.html",
@@ -1189,6 +1197,7 @@ def login(
response.set_cookie(CSRF_COOKIE, csrf, httponly=False, secure=True, samesite="lax", path="/")
return response
record_login_success(ip)
response = RedirectResponse(url="/", status_code=303)
issue_auth_cookie(response, user)
issue_csrf_cookie(response)
@@ -1679,7 +1688,7 @@ def session_view_page(session_id: str, request: Request, user: User = Depends(re
if (data.ready) {{
try {{
const probe = await fetch(iframeSrc, {{method:'HEAD', credentials:'include'}});
if (probe.status < 500) {{
if (probe.status >= 200 && probe.status < 300) {{
const f = document.getElementById('app-frame');
f.src = iframeSrc;
f.style.display = '';
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 27 KiB

+2 -24
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 35 KiB

+11
View File
@@ -0,0 +1,11 @@
{
"name": "Инфраструктурный полигон MONT",
"short_name": "MONT Полигон",
"description": "Демонстрация и пилотное тестирование российского ПО",
"start_url": "/",
"display": "browser",
"icons": [
{ "src": "/static/favicon_192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/static/favicon_512.png", "sizes": "512x512", "type": "image/png" }
]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 420 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

+1 -1
View File
@@ -937,7 +937,7 @@ button {
.login-corner-logo {
display: block;
height: 36px;
height: 54px;
width: auto;
margin-bottom: 1.5rem;
filter: brightness(0) invert(1);
+5 -2
View File
@@ -7,7 +7,10 @@
<link rel="stylesheet" href="/static/style.css?v=2" />
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
<link rel="icon" type="image/png" href="/static/favicon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon_32.png" />
<link rel="icon" type="image/png" sizes="192x192" href="/static/favicon_192.png" />
<link rel="apple-touch-icon" sizes="192x192" href="/static/favicon_192.png" />
<link rel="manifest" href="/static/manifest.json" />
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){
@@ -24,7 +27,7 @@
<body>
<header class="header">
<div style="display:flex; align-items:center; gap:0.6rem;">
<a href="https://4mont.ru"><img src="/static/logo.png?v=2" alt="MONT" class="header-logo" /></a>
<a href="https://4mont.ru"><img src="/static/logo.png?v=4" alt="MONT" class="header-logo" /></a>
<div>MONT - инфрастуктурный полигон | Админ: {{ admin.username }}</div>
</div>
<a href="/" class="btn-link secondary">Главная панель</a>
+6 -3
View File
@@ -7,7 +7,10 @@
<link rel="stylesheet" href="/static/style.css?v=2" />
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
<link rel="icon" type="image/png" href="/static/favicon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon_32.png" />
<link rel="icon" type="image/png" sizes="192x192" href="/static/favicon_192.png" />
<link rel="apple-touch-icon" sizes="192x192" href="/static/favicon_192.png" />
<link rel="manifest" href="/static/manifest.json" />
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){
@@ -33,7 +36,7 @@
.mw-footer{position:absolute;bottom:1.2rem;left:0;width:100%;text-align:center;font-size:clamp(.65rem,2.8vw,.78rem);color:rgba(160,184,204,.45);font-family:sans-serif}
</style>{% endraw %}
<div id="mobile-wall">
<a href="https://4mont.ru"><img src="/static/logo.png?v=2" alt="MONT" style="position:absolute;top:1.2rem;left:50%;transform:translateX(-50%);height:clamp(4rem,16vw,6rem);opacity:.9"></a>
<a href="https://4mont.ru"><img src="/static/logo.png?v=4" alt="MONT" style="position:absolute;top:1.2rem;left:50%;transform:translateX(-50%);height:clamp(4rem,16vw,6rem);opacity:.9"></a>
<div class="mw-icon">🖥️</div>
<div class="mw-title">Только для компьютера</div>
<div class="mw-sub">Инфраструктурный полигон MONT оптимизирован для работы на ПК.<br>Пожалуйста, откройте портал с настольного компьютера или ноутбука.</div>
@@ -59,7 +62,7 @@
</div>
</header>
<div class="page-logo-wrap">
<a href="https://4mont.ru"><img src="/static/logo.png?v=2" alt="MONT" class="page-logo" /></a>
<a href="https://4mont.ru"><img src="/static/logo.png?v=4" alt="MONT" class="page-logo" /></a>
</div>
<main class="admin-layout">
<section class="panel">
+6 -3
View File
@@ -14,7 +14,7 @@
<meta property="og:url" content="{{ base_url }}" />
<meta property="og:title" content="Инфраструктурный полигон MONT — демо и пилоты российского ПО" />
<meta property="og:description" content="Демонстрация и тестирование российского ПО для партнёров и заказчиков MONT. Доступ к рабочим стендам прямо в браузере." />
<meta property="og:image" content="{{ base_url }}static/logo.png?v=2" />
<meta property="og:image" content="{{ base_url }}static/logo.png?v=4" />
<meta property="og:locale" content="ru_RU" />
<meta property="og:site_name" content="Полигон MONT" />
<meta name="twitter:card" content="summary" />
@@ -37,7 +37,10 @@
<link rel="stylesheet" href="/static/style.css?v=2" />
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
<link rel="icon" type="image/png" href="/static/favicon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon_32.png" />
<link rel="icon" type="image/png" sizes="192x192" href="/static/favicon_192.png" />
<link rel="apple-touch-icon" sizes="192x192" href="/static/favicon_192.png" />
<link rel="manifest" href="/static/manifest.json" />
<style>
body { background: #070f1c; overflow: hidden; height: 100vh; }
@media (max-width: 820px) { body { overflow: auto; height: auto; } }
@@ -61,7 +64,7 @@
<div class="login-left-glow login-left-glow-top"></div>
<div class="login-left-glow login-left-glow-bottom"></div>
<div class="login-left-inner">
<a href="#" onclick="window.open(location.hostname==='stand.mont.ru'?'https://www.mont.ru':'https://4mont.ru','_blank');return false;"><img src="/static/logo.png?v=2" alt="MONT" class="login-corner-logo" /></a>
<a href="#" onclick="window.open(location.hostname==='stand.mont.ru'?'https://www.mont.ru':'https://4mont.ru','_blank');return false;"><img src="/static/logo.png?v=4" alt="MONT" class="login-corner-logo" /></a>
<h1 class="login-left-title">Инфраструктурный<br>полигон MONT</h1>
<p class="login-left-desc">Платформа для демонстрации и пилотного тестирования российского ПО. Партнеры MONT и их заказчики получают браузерный доступ к рабочим стендам с отечественными ОС, платформами виртуализации, СРК и другими решениями — без установки и настройки.</p>
<ul class="login-features">