'use strict'; const db = require('../../db/db'); const crypto = require('crypto'); const { emitToUser, invalidateSession } = require('../../ws-server'); const { emitToSession, hasAccess, canDraw } = require('./_shared'); function createSession(req, res) { const { class_id, user_ids, title = '' } = req.body; const teacher = req.user; const classroomEnabled = db.prepare("SELECT value FROM app_settings WHERE key='feature_classroom_enabled'").get(); if (classroomEnabled?.value === '0') { return res.status(403).json({ error: 'Модуль онлайн-уроков отключён администратором' }); } if (!class_id && (!user_ids || !user_ids.length)) { return res.status(400).json({ error: 'Укажите class_id или user_ids' }); } if (class_id) { const cls = teacher.role === 'admin' ? db.prepare('SELECT id, name FROM classes WHERE id=?').get(class_id) : db.prepare('SELECT id, name FROM classes WHERE id=? AND teacher_id=?').get(class_id, teacher.id); if (!cls) return res.status(403).json({ error: 'Нет доступа к классу' }); db.prepare(`UPDATE classroom_sessions SET status='ended', ended_at=datetime('now') WHERE class_id=? AND status='active'`).run(class_id); } const { lastInsertRowid } = db.prepare( `INSERT INTO classroom_sessions (class_id, teacher_id, title) VALUES (?,?,?)` ).run(class_id || null, teacher.id, title); const sessionId = Number(lastInsertRowid); db.prepare('INSERT INTO classroom_pages (session_id, page_num) VALUES (?,1)').run(sessionId); if (!class_id && user_ids) { const ins = db.prepare('INSERT OR IGNORE INTO classroom_invites (session_id, user_id) VALUES (?,?)'); for (const uid of user_ids) ins.run(sessionId, uid); } const session = db.prepare('SELECT * FROM classroom_sessions WHERE id=?').get(sessionId); emitToSession(sessionId, { type: 'classroom_started', sessionId, title, classId: class_id || null, teacherName: teacher.name }); // Баннер «идёт онлайн-урок» на дашбордах — через SSE-канал (доска работает по WS, // дашборд по SSE, поэтому нужен отдельный сигнал ученикам класса / приглашённым / учителю). try { const sse = require('../../sse'); const payload = { type: 'classroom_live', state: 'started', sessionId, title, classId: class_id || null }; if (class_id) sse.emitToClass(class_id, payload); else if (user_ids) for (const uid of user_ids) sse.emit(uid, payload); sse.emit(teacher.id, payload); } catch { /* SSE недоступен — не критично */ } res.json(session); } function getSession(req, res) { const sessionId = Number(req.params.id); const session = db.prepare('SELECT * FROM classroom_sessions WHERE id=?').get(sessionId); if (!session) return res.status(404).json({ error: 'Не найдено' }); if (!hasAccess(session, req.user.id, req.user.role)) return res.status(403).json({ error: 'Нет доступа' }); const pageCount = db.prepare('SELECT COUNT(*) AS c FROM classroom_pages WHERE session_id=?').get(sessionId).c; const attendance = db.prepare(` SELECT a.user_id, u.name, a.joined_at, a.left_at FROM classroom_attendance a JOIN users u ON u.id = a.user_id WHERE a.session_id=? ORDER BY a.joined_at `).all(sessionId); const drawAllowed = canDraw(sessionId, req.user.id, session); res.json({ ...session, pageCount, attendance, canDraw: drawAllowed }); } function endSession(req, res) { const sessionId = Number(req.params.id); const session = db.prepare('SELECT * FROM classroom_sessions WHERE id=?').get(sessionId); if (!session) return res.status(404).json({ error: 'Не найдено' }); if (session.teacher_id !== req.user.id && req.user.role !== 'admin') return res.status(403).json({ error: 'Нет доступа' }); db.prepare(`UPDATE classroom_sessions SET status='ended', ended_at=datetime('now') WHERE id=?`).run(sessionId); db.prepare(`UPDATE classroom_attendance SET left_at=datetime('now') WHERE session_id=? AND left_at IS NULL`).run(sessionId); db.prepare('DELETE FROM classroom_hands WHERE session_id=?').run(sessionId); db.prepare('DELETE FROM classroom_draw_permissions WHERE session_id=?').run(sessionId); db.prepare('DELETE FROM classroom_muted WHERE session_id=?').run(sessionId); emitToSession(sessionId, { type: 'classroom_ended', sessionId }); // Снять баннер «идёт онлайн-урок» с дашбордов (SSE-канал). try { const sse = require('../../sse'); const payload = { type: 'classroom_live', state: 'ended', sessionId }; if (session.class_id) sse.emitToClass(session.class_id, payload); else { const invited = db.prepare('SELECT user_id FROM classroom_invites WHERE session_id=?').all(sessionId); for (const r of invited) sse.emit(r.user_id, payload); } sse.emit(session.teacher_id, payload); } catch { /* SSE недоступен — не критично */ } res.json({ ok: true }); } function getActiveSession(req, res) { const classId = Number(req.params.classId); if (req.user.role !== 'teacher' && req.user.role !== 'admin') { const isMember = db.prepare('SELECT 1 FROM class_members WHERE class_id=? AND user_id=?').get(classId, req.user.id); if (!isMember) return res.status(403).json({ error: 'Нет доступа' }); } const session = db.prepare( `SELECT * FROM classroom_sessions WHERE class_id=? AND status='active' ORDER BY id DESC LIMIT 1` ).get(classId); if (!session) return res.json({ active: false }); res.json({ active: true, session }); } function getMyActive(req, res) { const userId = req.user.id; const sessions = db.prepare(` SELECT s.* FROM classroom_sessions s JOIN classroom_invites i ON i.session_id = s.id WHERE i.user_id=? AND s.status='active' ORDER BY s.id DESC `).all(userId); res.json({ sessions }); } function joinSession(req, res) { const sessionId = Number(req.params.id); const session = db.prepare(`SELECT * FROM classroom_sessions WHERE id=? AND status='active'`).get(sessionId); if (!session) return res.status(404).json({ error: 'Сессия не активна' }); if (!hasAccess(session, req.user.id, req.user.role)) return res.status(403).json({ error: 'Нет доступа' }); db.prepare(` INSERT INTO classroom_attendance (session_id, user_id) VALUES (?,?) ON CONFLICT(session_id, user_id) DO UPDATE SET joined_at=datetime('now'), left_at=NULL `).run(sessionId, req.user.id); invalidateSession(sessionId); emitToSession(sessionId, { type: 'classroom_user_joined', sessionId, userId: req.user.id, userName: req.user.name }); const drawAllowed = canDraw(sessionId, req.user.id, session) && session.teacher_id !== req.user.id; if (drawAllowed) emitToUser(req.user.id, { type: 'classroom_draw_permitted', sessionId }); const isMuted = !!db.prepare('SELECT 1 FROM classroom_muted WHERE session_id=? AND user_id=?').get(sessionId, req.user.id); if (isMuted) emitToUser(req.user.id, { type: 'classroom_muted', sessionId, by: null }); res.json({ ok: true, canDraw: drawAllowed, muted: isMuted }); } function leaveSession(req, res) { const sessionId = Number(req.params.id); db.prepare(`UPDATE classroom_attendance SET left_at=datetime('now') WHERE session_id=? AND user_id=? AND left_at IS NULL`).run(sessionId, req.user.id); invalidateSession(sessionId); const session = db.prepare('SELECT * FROM classroom_sessions WHERE id=?').get(sessionId); if (session) emitToSession(sessionId, { type: 'classroom_user_left', sessionId, userId: req.user.id }); res.json({ ok: true }); } function signal(req, res) { const sessionId = Number(req.params.id); const { target_user_id, payload } = req.body; if (!target_user_id || !payload) return res.status(400).json({ error: 'target_user_id и payload обязательны' }); const session = db.prepare(`SELECT * FROM classroom_sessions WHERE id=? AND status='active'`).get(sessionId); if (!session) return res.status(404).json({ error: 'Сессия не активна' }); if (!hasAccess(session, req.user.id, req.user.role)) return res.status(403).json({ error: 'Нет доступа' }); if (!hasAccess(session, target_user_id, 'student')) return res.status(403).json({ error: 'Цель не является участником сессии' }); emitToUser(target_user_id, { type: 'classroom_signal', sessionId, from: req.user.id, payload }); res.json({ ok: true }); } function getMySession(req, res) { const userId = req.user.id; const role = req.user.role; let session = null; if (role === 'teacher' || role === 'admin') { session = db.prepare( `SELECT * FROM classroom_sessions WHERE teacher_id=? AND status='active' ORDER BY id DESC LIMIT 1` ).get(userId); } else { session = db.prepare(` SELECT cs.* FROM classroom_sessions cs JOIN class_members cm ON cm.class_id = cs.class_id WHERE cm.user_id=? AND cs.status='active' ORDER BY cs.id DESC LIMIT 1 `).get(userId); if (!session) { session = db.prepare(` SELECT cs.* FROM classroom_sessions cs JOIN classroom_invites ci ON ci.session_id = cs.id WHERE ci.user_id=? AND cs.status='active' ORDER BY cs.id DESC LIMIT 1 `).get(userId); } } if (!session) return res.json({ session: null }); const pageCount = db.prepare('SELECT COUNT(*) AS c FROM classroom_pages WHERE session_id=?').get(session.id).c; const attendance = db.prepare(` SELECT a.user_id, u.name, a.joined_at, a.left_at FROM classroom_attendance a JOIN users u ON u.id = a.user_id WHERE a.session_id=? ORDER BY a.joined_at `).all(session.id); const wasJoined = attendance.some(a => a.user_id === userId); res.json({ session: { ...session, pageCount, attendance }, wasJoined }); } function generateGuestToken(req, res) { const sessionId = Number(req.params.id); const session = db.prepare('SELECT id, teacher_id FROM classroom_sessions WHERE id=?').get(sessionId); if (!session) return res.status(404).json({ error: 'Not found' }); if (session.teacher_id !== req.user.id && req.user.role !== 'admin') return res.status(403).json({ error: 'Forbidden' }); const token = crypto.randomBytes(24).toString('base64url'); db.prepare('UPDATE classroom_sessions SET guest_token=? WHERE id=?').run(token, sessionId); res.json({ token, url: `/guest-board.html?token=${token}` }); } function revokeGuestToken(req, res) { const sessionId = Number(req.params.id); const session = db.prepare('SELECT id, teacher_id FROM classroom_sessions WHERE id=?').get(sessionId); if (!session) return res.status(404).json({ error: 'Not found' }); if (session.teacher_id !== req.user.id && req.user.role !== 'admin') return res.status(403).json({ error: 'Forbidden' }); db.prepare('UPDATE classroom_sessions SET guest_token=NULL WHERE id=?').run(sessionId); res.json({ ok: true }); } function getGuestToken(req, res) { const sessionId = Number(req.params.id); const session = db.prepare('SELECT id, teacher_id, guest_token FROM classroom_sessions WHERE id=?').get(sessionId); if (!session) return res.status(404).json({ error: 'Not found' }); if (session.teacher_id !== req.user.id && req.user.role !== 'admin') return res.status(403).json({ error: 'Forbidden' }); if (!session.guest_token) return res.json({ token: null, url: null }); res.json({ token: session.guest_token, url: `/guest-board.html?token=${session.guest_token}` }); } module.exports = { createSession, getSession, endSession, getActiveSession, getMyActive, joinSession, leaveSession, signal, getMySession, generateGuestToken, revokeGuestToken, getGuestToken, };