Files
ACORE/core/middleware.py
T
lokses 0057f367a2 core: кастомные страницы ошибок 404/403/500/503 + режим техработ
Единый стиль портала (логотип, тема, кнопки) через 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>
2026-09-21 23:54:34 +03:00

131 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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