Fix: чаты и LDAP

- Добавлен CSRF токен в модальные окна (createDepartmentModal, createChatModal)
- Исправлена работа групповых чатов (group/channel/announcement):
  * Добавлена проверка доступа через ChatMembership в views и consumers
  * Групповые чаты теперь отображаются в списке
  * WebSocket подключается корректно без бесконечных перезагрузок
- API create_chat теперь читает JSON вместо POST данных
- Изменена отправка сообщений: Enter вместо Ctrl+Enter, Shift+Enter для переноса строки
- Добавлен сертификат LDAP (corp-ca.pem)
- Отключен LDAP в локальном .env для разработки (USE_SQLITE=True, LDAP_ENABLED=False)
This commit is contained in:
admin i 2025-11-27 17:23:13 +03:00
parent 783ea4f31f
commit eb2fbd7c8c
7 changed files with 114 additions and 9 deletions

View File

@ -25,15 +25,28 @@ from .models import (
@require_POST
def create_chat(request):
"""Создание нового чата (group, channel, announcement)"""
chat_type = request.POST.get('type', 'group')
name = request.POST.get('name', '').strip()
description = request.POST.get('description', '').strip()
import json
# Читаем JSON из тела запроса
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'ok': False, 'error': 'Invalid JSON'}, status=400)
chat_type = data.get('type', 'group')
name = data.get('name', '').strip()
description = data.get('description', '').strip()
participant_ids = data.get('participant_ids', [])
if chat_type not in ['group', 'channel', 'announcement']:
return JsonResponse({'ok': False, 'error': 'Invalid chat type'}, status=400)
return JsonResponse(
{'ok': False, 'error': 'Invalid chat type'}, status=400
)
if not name:
return JsonResponse({'ok': False, 'error': 'Name is required'}, status=400)
return JsonResponse(
{'ok': False, 'error': 'Name is required'}, status=400
)
chat = Chat.objects.create(
type=chat_type,
@ -53,6 +66,22 @@ def create_chat(request):
can_pin_messages=True
)
# Добавляем участников для группового чата
if chat_type == 'group' and participant_ids:
from employees.models import Employee
for emp_id in participant_ids:
try:
employee = Employee.objects.get(id=emp_id)
if employee != request.user: # Создателя уже добавили
ChatMembership.objects.create(
chat=chat,
user=employee,
role='member',
can_send_messages=True
)
except Employee.DoesNotExist:
pass
return JsonResponse({
'ok': True,
'chat_id': chat.id,

View File

@ -328,7 +328,15 @@ class ChatConsumer(AsyncJsonWebsocketConsumer):
or dept.head_id == user.id
or dept.active_employees.filter(id=user.id).exists()
)
return chat.participants.filter(id=user.id).exists()
if chat.type == "private":
return chat.participants.filter(id=user.id).exists()
if chat.type in ["group", "channel", "announcement"]:
# Проверка через ChatMembership
from communications.models import ChatMembership
return ChatMembership.objects.filter(
chat=chat, user=user
).exists()
return False
@database_sync_to_async
def _create_message(self, chat: Chat, user, text: str, reply_to_id=None) -> Message:

View File

@ -70,11 +70,19 @@ class ChatListView(LoginRequiredMixin, ListView):
datetime.datetime(1970, 1, 1), timezone.get_current_timezone()
).astimezone(dt_tz.utc)
# Получаем ID чатов, где пользователь является участником
# через membership
from communications.models import ChatMembership
membership_chat_ids = ChatMembership.objects.filter(
user=user
).values_list('chat_id', flat=True)
qs = (
Chat.objects.filter(
Q(type="global")
| Q(type="department", department__in=departments)
| Q(type="private", participants=user)
| Q(id__in=membership_chat_ids) # group/channel/announcement
)
.distinct()
.select_related("department")
@ -126,6 +134,12 @@ class ChatDetailView(LoginRequiredMixin, DetailView, FormView):
return chat.participants.filter(pk=user.pk).exists()
if chat.type == "department" and chat.department_id:
return chat.get_participants.filter(pk=user.pk).exists()
if chat.type in ["group", "channel", "announcement"]:
# Проверяем membership для групповых чатов, каналов и объявлений
from communications.models import ChatMembership
return ChatMembership.objects.filter(
chat=chat, user=user
).exists()
return False
def get_context_data(self, **kwargs):

View File

@ -278,10 +278,12 @@ export function initChatMarkRead(options = {}) {
// События textarea
ta?.addEventListener('input', autosize);
ta?.addEventListener('keydown', e => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
if (e.key === 'Enter' && !e.shiftKey) {
// Enter без Shift - отправка сообщения
e.preventDefault();
form?.requestSubmit();
}
// Shift+Enter - перенос строки (стандартное поведение)
});
// События скролла

View File

@ -195,13 +195,21 @@ export function initCreateDepartmentModal(options = {}) {
}
try {
console.log('Sending request to:', submitUrl);
console.log('Headers:', headers);
console.log('Data:', formData);
const response = await fetch(submitUrl, {
method: 'POST',
headers: headers,
body: JSON.stringify(formData)
});
console.log('Response status:', response.status);
console.log('Response ok:', response.ok);
const data = await response.json();
console.log('Response data:', data);
if (response.ok) {
// Успех - закрываем модал

View File

@ -561,10 +561,34 @@
}
});
// Функция получения CSRF токена
function getCSRFToken() {
// Сначала пробуем найти в скрытом поле формы
const tokenInput = document.querySelector('[name=csrfmiddlewaretoken]');
if (tokenInput) {
return tokenInput.value;
}
// Иначе берем из cookie
const name = 'csrftoken';
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
// Обработка создания чата
document.getElementById('createChatBtn')?.addEventListener('click', async () => {
const type = chatTypeSelect.value;
const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
const csrftoken = getCSRFToken();
if(type === 'private') {
// Создание личного чата
@ -611,7 +635,7 @@
if(res.ok) {
const data = await res.json();
window.location.href = `/communications/chat/${data.chat_id}/`;
window.location.href = `/communications/chats/${data.chat_id}/`;
} else {
const err = await res.json();
alert('Ошибка: ' + (err.error || 'Неизвестная ошибка'));

20
corp-ca.pem Normal file
View File

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDSDCCAjCgAwIBAgIQFm5AzaadxJFKu/4k0cYTPTANBgkqhkiG9w0BAQsFADAe
MRwwGgYDVQQDDBNSb2JvdGFpbC1EZXYtUm9vdENBMB4XDTI1MDkyMzA5MzQ1N1oX
DTMwMDkyMzA5NDQ1N1owHjEcMBoGA1UEAwwTUm9ib3RhaWwtRGV2LVJvb3RDQTCC
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM3mKhrAwi4uHYdr2MUEdcbO
37SEz7i/+DjQbxAaZxYvQgdSO12s5hk2YZ4sUSELV/ML92iPYr+h1X23+BgKLP9W
q+37PUGpE9c9rLqNnzpQ0rBCyS3LsaKqJ1V2CJE1dVkbvMaxmnOd43t5NbFrjri2
P/0YJP5EGSBhnwWtVYw30Mu3SXzO2URPlxVL7o5UlHzMIjE03hpMMrYv/WMaVH2F
XvhbtTEh0O1tEQvHRKtegeg5UBNKLVdiha1dYNGQPvbGuLmYZNmfhp5p7vB3PCL7
q3YNigkq87JsEzsT4zah2EtRG9oFy+0/ll2V9STBxCYUoS6y7xH+apJYNlW7e9kC
AwEAAaOBgTB/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHSUEFjAUBggrBgEFBQcDAgYI
KwYBBQUHAwEwHgYDVR0RBBcwFYITUm9ib3RhaWwtRGV2LVJvb3RDQTAPBgNVHRMB
Af8EBTADAQH/MB0GA1UdDgQWBBSIt2Yb2d2WFe+7ELNmRxpCy/H2+jANBgkqhkiG
9w0BAQsFAAOCAQEAYQnWajz9IT8gjRHrUx0FDp04l4vrvDSfpxexHVrPafk8/8YL
i+g8U/YHnJz9QPahfBKDnzcxONAUkHysHe8Ju8NzIcZP8twB2sIFZu6px+5ozxgz
x26ztOoVN1RsqnCSn+/mlPIbWDWE1a6SV4mK2OaveNgszqZn+e9+Nd5fvhs/hZUZ
8scMVpTdFzqYEB3urKpgIx4xbNzdjn8Kp6affeYtct4i6flcIk5REjMp0+hxFt/E
hTMJJvp8qPOEfxRilIAiKeJ9/roq16EtjwHn7evEmdJqA7WxZNbrfx2tUbizUfWN
ZR8uwKUZYLV5axRvtUSY6nENMuFJGUQS1yFEsQ==
-----END CERTIFICATE-----