diff --git a/acore/settings/base.py b/acore/settings/base.py index 1cfb353..0b7b89c 100644 --- a/acore/settings/base.py +++ b/acore/settings/base.py @@ -32,6 +32,7 @@ CARTO_BASEMAPS_API_KEY = env('CARTO_BASEMAPS_API_KEY', default='') # ACORE → PNMS identity sync (portal_connector) PNMS_BASE_URL = env('PNMS_BASE_URL', default='') PNMS_ACORE_TOKEN = env('PNMS_ACORE_TOKEN', default='') +MAINTENANCE_MODE = env.bool('MAINTENANCE_MODE', default=False) # режим техработ: всем 503, кроме суперпользователей PNMS_SYNC_ENABLED = env.bool('PNMS_SYNC_ENABLED', default=False) # ACORE → MSORTER: только сотрудники в группе «Почта» (админы UI — «Администратор почты»). @@ -121,6 +122,7 @@ MIDDLEWARE = [ 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'core.middleware.MaintenanceModeMiddleware', # режим техработ (503), после Auth 'apps.employees.middleware.TwoFactorMiddleware', # обязательная 2FA для staff (после Auth) 'core.middleware.DevAccessPreviewMiddleware', 'core.middleware.PortalSectionAccessMiddleware', # барьер разделов (после preview) diff --git a/core/middleware.py b/core/middleware.py index bd97bfe..ac837be 100644 --- a/core/middleware.py +++ b/core/middleware.py @@ -31,6 +31,39 @@ class DevAccessPreviewMiddleware: 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. diff --git a/core/tests_error_pages.py b/core/tests_error_pages.py new file mode 100644 index 0000000..6360b3a --- /dev/null +++ b/core/tests_error_pages.py @@ -0,0 +1,63 @@ +from django.template.loader import render_to_string +from django.test import TestCase, override_settings + +from core.test_utils import login_2fa +from core.tests_portal_access import make_employee + + +class ErrorTemplateRenderTests(TestCase): + """Шаблоны ошибок валидны и содержат свой код/заголовок (500 рендерится + без request/context-процессоров — проверяем автономность).""" + + def test_403_template(self): + html = render_to_string('403.html') + self.assertIn('403', html) + self.assertIn('Доступ запрещён', html) + + def test_500_template(self): + html = render_to_string('500.html') + self.assertIn('500', html) + self.assertIn('Внутренняя ошибка сервера', html) + + def test_503_template(self): + html = render_to_string('503.html') + self.assertIn('503', html) + self.assertIn('технические работы', html) + + def test_404_template(self): + html = render_to_string('404.html') + self.assertIn('404', html) + self.assertIn('Страница не найдена', html) + + +class Custom404Tests(TestCase): + def test_unknown_url_uses_custom_404(self): + resp = self.client.get('/no-such-page-xyz-42/') + self.assertEqual(resp.status_code, 404) + self.assertContains(resp, 'Страница не найдена', status_code=404) + + +@override_settings(MAINTENANCE_MODE=True) +class MaintenanceModeTests(TestCase): + def test_anonymous_gets_503(self): + resp = self.client.get('/dashboard/') + self.assertEqual(resp.status_code, 503) + self.assertContains(resp, 'технические работы', status_code=503) + + def test_login_path_allowed(self): + resp = self.client.get('/accounts/login/') + self.assertNotEqual(resp.status_code, 503) + + def test_superuser_bypasses(self): + admin = make_employee(username='maint.super', is_staff=True, is_superuser=True) + login_2fa(self.client, admin) + resp = self.client.get('/dashboard/') + self.assertNotEqual(resp.status_code, 503) + + +class MaintenanceOffByDefaultTests(TestCase): + def test_no_503_when_flag_off(self): + user = make_employee() + login_2fa(self.client, user) + resp = self.client.get('/dashboard/', HTTP_HX_REQUEST='true') + self.assertEqual(resp.status_code, 200) diff --git a/templates/403.html b/templates/403.html new file mode 100644 index 0000000..71701b9 --- /dev/null +++ b/templates/403.html @@ -0,0 +1,10 @@ +{% extends "error_base.html" %} +{% block page_title %}Доступ запрещён{% endblock %} +{% block accent_color %}var(--color-destructive){% endblock %} +{% block code %}403{% endblock %} +{% block heading %}Доступ запрещён{% endblock %} +{% block message %}У вас нет прав для этого действия или раздела. Если это ошибка — обратитесь к администратору или в IT-поддержку.{% endblock %} +{% block actions %} +На главную +Поддержка IT +{% endblock %} diff --git a/templates/404.html b/templates/404.html new file mode 100644 index 0000000..cd93165 --- /dev/null +++ b/templates/404.html @@ -0,0 +1,5 @@ +{% extends "error_base.html" %} +{% block page_title %}Страница не найдена{% endblock %} +{% block code %}404{% endblock %} +{% block heading %}Страница не найдена{% endblock %} +{% block message %}Такой страницы нет или у вас нет к ней доступа. Проверьте адрес или вернитесь на главную.{% endblock %} diff --git a/templates/500.html b/templates/500.html new file mode 100644 index 0000000..10262b1 --- /dev/null +++ b/templates/500.html @@ -0,0 +1,9 @@ +{% extends "error_base.html" %} +{% block page_title %}Внутренняя ошибка{% endblock %} +{% block accent_color %}var(--color-destructive){% endblock %} +{% block code %}500{% endblock %} +{% block heading %}Внутренняя ошибка сервера{% endblock %} +{% block message %}На нашей стороне что-то сломалось. Мы уже знаем об этом. Попробуйте обновить страницу позже.{% endblock %} +{% block actions %} +На главную +{% endblock %} diff --git a/templates/503.html b/templates/503.html new file mode 100644 index 0000000..1f8dfdc --- /dev/null +++ b/templates/503.html @@ -0,0 +1,9 @@ +{% extends "error_base.html" %} +{% block page_title %}Технические работы{% endblock %} +{% block accent_color %}hsl(38 92% 50%){% endblock %} +{% block code %}503{% endblock %} +{% block heading %}Идут технические работы{% endblock %} +{% block message %}Портал временно недоступен — проводим обслуживание. Пожалуйста, зайдите чуть позже. Обычно это занимает несколько минут.{% endblock %} +{% block actions %} + +{% endblock %} diff --git a/templates/error_base.html b/templates/error_base.html new file mode 100644 index 0000000..ecf063e --- /dev/null +++ b/templates/error_base.html @@ -0,0 +1,49 @@ +{% load static %} + + +
+ + +