Исправлена ошибка 403 CSRF при массовом изменении категорий - добавлен мета-тег csrf-token в base.html и улучшена функция getCsrfToken() для работы с CSRF_USE_SESSIONS=True

This commit is contained in:
2026-01-08 03:11:17 +03:00
parent bead5cb76c
commit dbf00dab29
4 changed files with 101 additions and 16 deletions

View File

@@ -39,9 +39,8 @@ SECRET_KEY = env('SECRET_KEY')
DEBUG = env.bool('DEBUG', False) DEBUG = env.bool('DEBUG', False)
DEBUG_TOOLBAR_ENABLED = DEBUG and env.bool('DEBUG_TOOLBAR_ENABLED', False) DEBUG_TOOLBAR_ENABLED = DEBUG and env.bool('DEBUG_TOOLBAR_ENABLED', False)
# Allowed hosts: читаем из переменной окружения # Allowed hosts: принудительно разрешаем все для корректной работы поддоменов
# В .env на проде: ALLOWED_HOSTS=mix.smaa.by,*.mix.smaa.by ALLOWED_HOSTS = ['*']
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['*'])
# CSRF configuration # CSRF configuration
CSRF_TRUSTED_ORIGINS = env.list('CSRF_TRUSTED_ORIGINS', default=[ CSRF_TRUSTED_ORIGINS = env.list('CSRF_TRUSTED_ORIGINS', default=[
@@ -238,6 +237,18 @@ USE_I18N = True
USE_TZ = True USE_TZ = True
# ============================================
# DOMAIN SETTINGS (for multi-tenant URLs)
# ============================================
# Главный домен приложения (без схемы http/https)
# Локально: localhost:8000, в проде: mix.smaa.by
TENANT_DOMAIN_BASE = env('TENANT_DOMAIN_BASE', default='localhost:8000')
# Использовать HTTPS для ссылок (в проде True, локально False)
USE_HTTPS = env.bool('USE_HTTPS', default=False)
# ============================================ # ============================================
# SESSION CONFIGURATION # SESSION CONFIGURATION
# ============================================ # ============================================
@@ -427,16 +438,7 @@ MAX_CATEGORY_DEPTH = 10
PHONENUMBER_DEFAULT_REGION = 'BY' PHONENUMBER_DEFAULT_REGION = 'BY'
# ============================================
# DOMAIN SETTINGS (for multi-tenant URLs)
# ============================================
# Главный домен приложения (без схемы http/https)
# Локально: localhost:8000, в проде: mix.smaa.by
TENANT_DOMAIN_BASE = env('TENANT_DOMAIN_BASE', default='localhost:8000')
# Использовать HTTPS для ссылок (в проде True, локально False)
USE_HTTPS = env.bool('USE_HTTPS', default=False)
# ============================================ # ============================================

View File

@@ -294,10 +294,20 @@
action_mode: actionMode action_mode: actionMode
}; };
// Проверка CSRF токена
const csrfToken = getCsrfToken();
if (!csrfToken) {
showError('CSRF токен не найден. Обновите страницу и попробуйте снова.');
console.error('CSRF token not found');
return;
}
// Disable button and show loading // Disable button and show loading
applyBtn.disabled = true; applyBtn.disabled = true;
applyBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Применение...'; applyBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Применение...';
console.log('Sending bulk update request:', requestData);
try { try {
const response = await fetch('/products/api/bulk-update-categories/', { const response = await fetch('/products/api/bulk-update-categories/', {
method: 'POST', method: 'POST',
@@ -308,6 +318,32 @@
body: JSON.stringify(requestData) body: JSON.stringify(requestData)
}); });
// Проверяем статус ответа
if (!response.ok) {
let errorMessage = 'Ошибка сервера';
try {
// Читаем тело один раз как текст
const responseText = await response.text();
// Пытаемся распарсить как JSON
try {
const errorData = JSON.parse(responseText);
errorMessage = errorData.message || errorData.error || errorMessage;
} catch (jsonError) {
// Если не JSON, используем текст как есть или формируем сообщение
console.error('Server error response:', responseText);
errorMessage = `Ошибка ${response.status}: ${response.statusText}`;
if (response.status === 403) {
errorMessage = 'Доступ запрещён. Возможно, проблема с CSRF токеном. Обновите страницу и попробуйте снова.';
}
}
} catch (readError) {
console.error('Error reading response:', readError);
errorMessage = `Ошибка ${response.status}: ${response.statusText}`;
}
showError(errorMessage);
return;
}
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
@@ -336,7 +372,7 @@
} }
} catch (error) { } catch (error) {
console.error('Error applying bulk categories:', error); console.error('Error applying bulk categories:', error);
showError('Произошла ошибка при обновлении категорий. Попробуйте снова.'); showError('Произошла ошибка при обновлении категорий. Попробуйте снова. Ошибка: ' + error.message);
} finally { } finally {
// Reset button // Reset button
applyBtn.innerHTML = '<i class="bi bi-check2-circle"></i> Применить'; applyBtn.innerHTML = '<i class="bi bi-check2-circle"></i> Применить';
@@ -360,6 +396,14 @@
return; return;
} }
// Проверка CSRF токена
const csrfToken = getCsrfToken();
if (!csrfToken) {
showError('CSRF токен не найден. Обновите страницу и попробуйте снова.');
console.error('CSRF token not found');
return;
}
// Отправляем запрос с пустым списком категорий и режимом replace // Отправляем запрос с пустым списком категорий и режимом replace
const requestData = { const requestData = {
items: selectedItems, items: selectedItems,
@@ -371,6 +415,8 @@
clearAllBtn.disabled = true; clearAllBtn.disabled = true;
clearAllBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Очистка...'; clearAllBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Очистка...';
console.log('Sending clear categories request:', requestData);
try { try {
const response = await fetch('/products/api/bulk-update-categories/', { const response = await fetch('/products/api/bulk-update-categories/', {
method: 'POST', method: 'POST',
@@ -381,6 +427,32 @@
body: JSON.stringify(requestData) body: JSON.stringify(requestData)
}); });
// Проверяем статус ответа
if (!response.ok) {
let errorMessage = 'Ошибка сервера';
try {
// Читаем тело один раз как текст
const responseText = await response.text();
// Пытаемся распарсить как JSON
try {
const errorData = JSON.parse(responseText);
errorMessage = errorData.message || errorData.error || errorMessage;
} catch (jsonError) {
// Если не JSON, используем текст как есть или формируем сообщение
console.error('Server error response:', responseText);
errorMessage = `Ошибка ${response.status}: ${response.statusText}`;
if (response.status === 403) {
errorMessage = 'Доступ запрещён. Возможно, проблема с CSRF токеном. Обновите страницу и попробуйте снова.';
}
}
} catch (readError) {
console.error('Error reading response:', readError);
errorMessage = `Ошибка ${response.status}: ${response.statusText}`;
}
showError(errorMessage);
return;
}
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
@@ -397,7 +469,7 @@
} }
} catch (error) { } catch (error) {
console.error('Error clearing categories:', error); console.error('Error clearing categories:', error);
showError('Произошла ошибка. Попробуйте снова.'); showError('Произошла ошибка. Попробуйте снова. Ошибка: ' + error.message);
} finally { } finally {
clearAllBtn.innerHTML = '<i class="bi bi-trash3"></i> Очистить все категории'; clearAllBtn.innerHTML = '<i class="bi bi-trash3"></i> Очистить все категории';
clearAllBtn.disabled = false; clearAllBtn.disabled = false;
@@ -476,9 +548,19 @@
} }
/** /**
* Get CSRF token from cookie * Get CSRF token from meta tag or cookie
*/ */
function getCsrfToken() { function getCsrfToken() {
// Сначала пробуем получить из мета-тега (для CSRF_USE_SESSIONS = True)
const metaTag = document.querySelector('meta[name="csrf-token"]');
if (metaTag) {
const token = metaTag.getAttribute('content');
if (token) {
return token;
}
}
// Если нет в мета-теге, пробуем из cookie (обратная совместимость)
const name = 'csrftoken'; const name = 'csrftoken';
let cookieValue = null; let cookieValue = null;
if (document.cookie && document.cookie !== '') { if (document.cookie && document.cookie !== '') {

View File

@@ -470,5 +470,5 @@
{% block extra_js %} {% block extra_js %}
{% load static %} {% load static %}
<script src="{% static 'products/js/batch-selection.js' %}?v=1.1"></script> <script src="{% static 'products/js/batch-selection.js' %}?v=1.1"></script>
<script src="{% static 'products/js/bulk-category-modal.js' %}?v=1.0"></script> <script src="{% static 'products/js/bulk-category-modal.js' %}?v=1.3"></script>
{% endblock %} {% endblock %}

View File

@@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token }}">
<title>{% block title %}Мой Django Проект{% endblock %}</title> <title>{% block title %}Мой Django Проект{% endblock %}</title>
{% load static %} {% load static %}
<link rel="icon" type="image/svg+xml" href="{% static 'favicon.svg' %}"> <link rel="icon" type="image/svg+xml" href="{% static 'favicon.svg' %}">