fix: classroom review — 11 исправлений из code review
- sessions.js: endSession закрывает classroom_attendance (left_at), чистит classroom_muted - sessions.js: joinSession восстанавливает mute-состояние при реконнекте - strokes.js: updateStroke проверяет авторство штриха (не только canDraw) - strokes.js: clearPage валидирует page_num как положительное целое - strokes.js: postStrokes ограничивает массив 500 штрихами - pages.js: duplicatePage сохраняет user_id при копировании штрихов - pages.js: changePage валидирует page_num - pages.js: updatePageTemplate делает INSERT OR IGNORE перед UPDATE - permissions.js: mutePeer сохраняет в classroom_muted; добавлен unmutePeer - permissions.js: getOnlineStudents не возвращает email - chat.js: exportChat экранирует переводы строк в именах и сообщениях - guestClassroom.js: санитизация имени гостя (убираем HTML-символы) - ws-server.js: mute_peer сохраняет в БД; добавлен обработчик unmute_peer - routes/classroom.js: rate-limit для cursor/preview/signal/strokes; маршрут DELETE /mute - migrations/001_classroom_muted.sql: новая таблица classroom_muted
This commit is contained in:
@@ -27,9 +27,13 @@ const chatUpload = multer({
|
||||
const teacher = [authMiddleware, requireRole('teacher', 'admin')];
|
||||
const auth = [authMiddleware];
|
||||
|
||||
const chatLimiter = rateLimit({ windowMs: 5_000, max: 5, message: 'Слишком много сообщений, подождите', byUser: true });
|
||||
const reactionLimiter = rateLimit({ windowMs: 5_000, max: 15, message: 'Слишком много реакций, подождите', byUser: true });
|
||||
const handLimiter = rateLimit({ windowMs: 5_000, max: 5, message: 'Не так часто', byUser: true });
|
||||
const chatLimiter = rateLimit({ windowMs: 5_000, max: 5, message: 'Слишком много сообщений, подождите', byUser: true });
|
||||
const reactionLimiter = rateLimit({ windowMs: 5_000, max: 15, message: 'Слишком много реакций, подождите', byUser: true });
|
||||
const handLimiter = rateLimit({ windowMs: 5_000, max: 5, message: 'Не так часто', byUser: true });
|
||||
const cursorLimiter = rateLimit({ windowMs: 2_000, max: 60, message: 'Слишком часто', byUser: true });
|
||||
const previewLimiter = rateLimit({ windowMs: 2_000, max: 60, message: 'Слишком часто', byUser: true });
|
||||
const signalLimiter = rateLimit({ windowMs: 10_000, max: 30, message: 'Слишком много сигналов', byUser: true });
|
||||
const strokesLimiter = rateLimit({ windowMs: 5_000, max: 100, message: 'Слишком много штрихов', byUser: true });
|
||||
|
||||
// Template library — MUST be before /:id to avoid shadowing
|
||||
router.get('/admin/active', ...teacher, c.adminGetActiveSessions);
|
||||
@@ -64,14 +68,14 @@ router.post('/:id/chat/upload', ...auth, chatUpload.single('file'),
|
||||
router.post('/:id/chat/:msgId/react', ...auth, reactionLimiter, c.reactToMessage);
|
||||
|
||||
// WebRTC signaling
|
||||
router.post('/:id/signal', ...auth, c.signal);
|
||||
router.post('/:id/signal', ...auth, signalLimiter, c.signal);
|
||||
|
||||
// Whiteboard strokes
|
||||
router.post('/:id/strokes', ...auth, c.postStrokes);
|
||||
router.get('/:id/strokes', ...auth, c.getStrokes);
|
||||
router.delete('/:id/strokes/:strokeId', ...teacher, c.deleteStroke);
|
||||
router.patch('/:id/strokes/:strokeId', ...auth, c.updateStroke);
|
||||
router.post('/:id/stroke-preview', ...auth, c.previewStroke);
|
||||
router.post('/:id/strokes', ...auth, strokesLimiter, c.postStrokes);
|
||||
router.get('/:id/strokes', ...auth, c.getStrokes);
|
||||
router.delete('/:id/strokes/:strokeId', ...teacher, c.deleteStroke);
|
||||
router.patch('/:id/strokes/:strokeId', ...auth, c.updateStroke);
|
||||
router.post('/:id/stroke-preview', ...auth, previewLimiter, c.previewStroke);
|
||||
|
||||
// Multi-page
|
||||
router.get('/:id/pages', ...auth, c.getPages);
|
||||
@@ -91,8 +95,9 @@ router.get('/:id/hands', ...auth, c.getHands);
|
||||
// Whiteboard: clear page
|
||||
router.post('/:id/clear-page', ...teacher, c.clearPage);
|
||||
|
||||
// WebRTC: mute peer, screen share broadcast
|
||||
// WebRTC: mute/unmute peer, screen share broadcast
|
||||
router.post('/:id/mute', ...teacher, c.mutePeer);
|
||||
router.delete('/:id/mute', ...teacher, c.unmutePeer);
|
||||
router.post('/:id/screen', ...teacher, c.screenStart);
|
||||
router.delete('/:id/screen', ...teacher, c.screenStop);
|
||||
|
||||
@@ -104,7 +109,7 @@ router.post('/:id/sim/mode', ...teacher, c.simMode);
|
||||
router.post('/:id/sim/annotate', ...teacher, c.simAnnotate);
|
||||
|
||||
// Cursor broadcast (all participants)
|
||||
router.post('/:id/cursor', ...auth, c.broadcastCursor);
|
||||
router.post('/:id/cursor', ...auth, cursorLimiter, c.broadcastCursor);
|
||||
|
||||
// Message pin (teacher only)
|
||||
router.post('/:id/chat/:msgId/pin', ...teacher, c.pinMessage);
|
||||
|
||||
@@ -64,7 +64,7 @@ router.post('/:token/join', (req, res) => {
|
||||
if (session.status !== 'active')
|
||||
return res.status(403).json({ error: 'Урок ещё не начался или уже завершён' });
|
||||
|
||||
const rawName = (req.body?.name || '').trim().slice(0, 40);
|
||||
const rawName = (req.body?.name || '').trim().slice(0, 40).replace(/[<>"&]/g, '');
|
||||
const name = rawName || 'Гость';
|
||||
|
||||
const guestId = 'g_' + crypto.randomBytes(12).toString('base64url');
|
||||
|
||||
Reference in New Issue
Block a user