feat(chat): правка и удаление сообщений

- PUT/DELETE /api/chat/threads/:id/messages/:messageId под requireAuth+chatLimiter,
  доступ через chatMessageFor (тред + принадлежность сообщения)
- править может только автор (админу чужое сообщение 403), удалять — автор или админ
- мягкое удаление: body='', вложения каскадом из БД + safeUnlink по path,
  счётчик непрочитанных уменьшается на 1 у своей стороны (GREATEST(col-1,0))
- refreshChatThreadPreview() пересчитывает превью треда по последнему живому сообщению
- SSE: publishChat с type, список типов валидируется через CHAT_EVENTS; новые
  message_update/message_delete подписаны в connectChatStream (admin.js)
- аудит chat.message.update с textDiff и chat.message.delete без текста
- колонки edited_at/deleted_at в db/init.sql, db/migration.sql, ensureChatTables()
  и в normalizeRestoreData + restore-INSERT — удалённые сообщения не воскресают
- фронтенд: кнопки правки/удаления по наведению, модалка правки (Ctrl+Enter),
  метка «изменено», плейсхолдер «Сообщение удалено», обновление по SSE
- покрытие в api.smoketest.js (правка/удаление/403/400/404) и backup.selftest.js
This commit is contained in:
dev
2026-10-05 18:32:58 +03:00
parent 7e4b1a1324
commit 1740c341a0
11 changed files with 478 additions and 12 deletions
+9
View File
@@ -79,6 +79,15 @@ const ch = normalizeRestoreData({
ok('chat_threads нормализуются', ch.chat_threads[0].tutor_id === 1 && ch.chat_threads[0].tutor_unread === 2, ch.chat_threads[0]);
ok('chat_messages нормализуются', ch.chat_messages[0].thread_id === 3 && ch.chat_messages[0].body === 'привет', ch.chat_messages[0]);
ok('chat_attachments нормализуются', ch.chat_attachments[0].name === 'photo.png' && ch.chat_attachments[0].size === 1234, ch.chat_attachments[0]);
const chEdits = normalizeRestoreData({
...base,
chat_messages: [
{ id: 10, thread_id: 3, sender_id: 1, body: 'правлено', edited_at: '2026-01-02T10:00:00.000Z' },
{ id: 11, thread_id: 3, sender_id: 1, body: '', deleted_at: '2026-01-03T10:00:00.000Z' },
],
});
ok('edited_at сообщения сохраняется', chEdits.chat_messages[0].edited_at === '2026-01-02T10:00:00.000Z', chEdits.chat_messages[0]);
ok('deleted_at сообщения сохраняется', chEdits.chat_messages[1].deleted_at === '2026-01-03T10:00:00.000Z', chEdits.chat_messages[1]);
let chatBad = false;
try {
normalizeRestoreData({ ...base, chat_messages: [{ id: 1, thread_id: 3, sender_id: 1, body: 'x'.repeat(5001) }] });