0057f367a2
Единый стиль портала (логотип, тема, кнопки) через error_base.html: - 404 — страница не найдена - 403 — доступ запрещён (+ ссылка на IT-поддержку) - 500 — внутренняя ошибка (автономна: без request/context-процессоров) - 503 — технические работы MaintenanceModeMiddleware: при MAINTENANCE_MODE=True отдаёт 503 всем, кроме суперпользователей и путей входа/админки/статики (чтобы админ мог войти и выключить режим). По умолчанию выключен — поведение не меняется. - tests: рендер всех шаблонов, кастомный 404, режим техработ вкл/выкл/супер Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
131 lines
5.8 KiB
Python
131 lines
5.8 KiB
Python
from datetime import timedelta
|
||
|
||
from django.utils import timezone
|
||
|
||
from core.access_preview import SESSION_KEY as ACCESS_PREVIEW_SESSION_KEY
|
||
from core.access_preview import access_preview_available, attach_preview_to_user
|
||
|
||
# Не пишем last_seen на каждый запрос — обновляем не чаще раза в этот интервал.
|
||
LAST_SEEN_WRITE_THROTTLE = timedelta(seconds=60)
|
||
|
||
# Порог, после которого сотрудник считается офлайн (используется в core.views.dashboard_view).
|
||
ONLINE_WINDOW = timedelta(minutes=5)
|
||
|
||
|
||
class DevAccessPreviewMiddleware:
|
||
"""В DEBUG вешает на request.user пресет «смотреть как…» из session."""
|
||
|
||
def __init__(self, get_response):
|
||
self.get_response = get_response
|
||
|
||
def __call__(self, request):
|
||
user = getattr(request, 'user', None)
|
||
preview_id = ''
|
||
if user is not None and access_preview_available(user):
|
||
preview_id = (request.session.get(ACCESS_PREVIEW_SESSION_KEY) or '').strip()
|
||
attach_preview_to_user(user, preview_id)
|
||
else:
|
||
if user is not None and getattr(user, 'is_authenticated', False):
|
||
attach_preview_to_user(user, '')
|
||
request.access_preview = preview_id if preview_id else ''
|
||
return self.get_response(request)
|
||
|
||
|
||
class MaintenanceModeMiddleware:
|
||
"""Режим техработ: при MAINTENANCE_MODE=True отдаёт 503 всем, кроме
|
||
суперпользователей (чтобы чинить), страниц входа/админки и статики.
|
||
По умолчанию выключен — поведение не меняется.
|
||
"""
|
||
|
||
def __init__(self, get_response):
|
||
self.get_response = get_response
|
||
|
||
def __call__(self, request):
|
||
from django.conf import settings
|
||
if getattr(settings, 'MAINTENANCE_MODE', False):
|
||
user = getattr(request, 'user', None)
|
||
if not (user and getattr(user, 'is_superuser', False)) and not self._allowed(request):
|
||
from django.shortcuts import render
|
||
return render(request, '503.html', status=503)
|
||
return self.get_response(request)
|
||
|
||
@staticmethod
|
||
def _allowed(request) -> bool:
|
||
# Логин/2FA/статика — чтобы суперпользователь смог войти и выключить режим.
|
||
path = request.path
|
||
if path.startswith(('/accounts/login', '/login', '/2fa/', '/static/', '/logout')):
|
||
return True
|
||
try:
|
||
from django.urls import reverse
|
||
if path.startswith(reverse('admin:index')):
|
||
return True
|
||
except Exception:
|
||
pass
|
||
return False
|
||
|
||
|
||
class PortalSectionAccessMiddleware:
|
||
"""Барьер доступа к разделам портала по префиксу URL.
|
||
|
||
Ранее «открытые» разделы (новости, задачи, чат и т.п.) были доступны
|
||
любому вошедшему. Теперь сотрудник без назначенной группы их не видит и
|
||
не может открыть даже прямым URL. Проверка централизована здесь, чтобы
|
||
покрыть и вложенные страницы (например /tasks/5/), а не только лендинги.
|
||
|
||
Базовые разделы (BASELINE_OPEN_KEYS) и разделы с собственным
|
||
@section_required сюда не входят.
|
||
"""
|
||
|
||
# первый сегмент пути -> ключ раздела портала
|
||
PATH_SECTION = {
|
||
'news': 'news',
|
||
'tasks': 'tasks',
|
||
'documents': 'documents',
|
||
'chat': 'chat',
|
||
'calendar': 'calendar',
|
||
'dislocation': 'dislocation',
|
||
'hr-portal': 'hr_portal',
|
||
'help': 'help',
|
||
}
|
||
# /settings/ не гейтим: это лишь редирект на свой профиль (2FA/пароль).
|
||
# Пункт меню «Настройки» скрывается в шаблоне через can_section.
|
||
|
||
def __init__(self, get_response):
|
||
self.get_response = get_response
|
||
|
||
def __call__(self, request):
|
||
user = getattr(request, 'user', None)
|
||
if user is not None and getattr(user, 'is_authenticated', False):
|
||
segment = request.path.strip('/').split('/', 1)[0]
|
||
key = self.PATH_SECTION.get(segment)
|
||
if key is not None:
|
||
from core.portal_access import deny_section_access, user_can_access
|
||
if not user_can_access(user, key):
|
||
return deny_section_access(request)
|
||
return self.get_response(request)
|
||
|
||
|
||
class UpdateLastSeenMiddleware:
|
||
"""
|
||
Обновляет Employee.last_seen для авторизованных пользователей — основа для
|
||
метрики «онлайн зараз» на дашборде. Пишет через .update(), а не save(),
|
||
чтобы не дёргать post_save-сигналы синхронизации с AD/Mattermost на каждый запрос.
|
||
"""
|
||
|
||
def __init__(self, get_response):
|
||
self.get_response = get_response
|
||
|
||
def __call__(self, request):
|
||
response = self.get_response(request)
|
||
|
||
user = getattr(request, 'user', None)
|
||
if user is not None and user.is_authenticated:
|
||
now = timezone.now()
|
||
if not user.last_seen or now - user.last_seen >= LAST_SEEN_WRITE_THROTTLE:
|
||
# user — SimpleLazyObject; user.__class__ (в отличие от type(user))
|
||
# корректно возвращает класс обёрнутого объекта (Employee).
|
||
user.__class__.objects.filter(pk=user.pk).update(last_seen=now)
|
||
user.last_seen = now
|
||
|
||
return response
|