LearnSpace: full-stack educational whiteboard platform
Node.js/Express backend + vanilla JS frontend. Features: real-time collaborative whiteboard (SSE), multi-page support, LaTeX formulas, shapes/connectors, coordinate systems, number lines, compass, zoom/pan, Catmull-Rom pencil smoothing, ruler/protractor with rotation & resize controls, minimap navigation overlay, auto-measurements, multi-page thumbnails sidebar, PNG export, page templates. Student/teacher workflows: classes, assignments, library, dashboard. Mobile responsive. SQLite (better-sqlite3). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
const router = require('express').Router();
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { authMiddleware, requireRole } = require('../middleware/auth');
|
||||
const rateLimit = require('../middleware/rateLimit');
|
||||
const c = require('../controllers/classroomController');
|
||||
|
||||
/* ── multer for chat image attachments ─────────────────────────────────── */
|
||||
const _chatUploadsDir = path.join(__dirname, '../../uploads/chat');
|
||||
const _chatStorage = multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, _chatUploadsDir),
|
||||
filename: (req, file, cb) => {
|
||||
const ext = path.extname(file.originalname).toLowerCase().replace(/[^.a-z0-9]/g, '');
|
||||
cb(null, crypto.randomBytes(14).toString('hex') + ext);
|
||||
},
|
||||
});
|
||||
const chatUpload = multer({
|
||||
storage: _chatStorage,
|
||||
limits: { fileSize: 5 * 1024 * 1024 },
|
||||
fileFilter: (req, file, cb) => {
|
||||
const ok = ['image/jpeg','image/png','image/gif','image/webp'].includes(file.mimetype);
|
||||
cb(null, ok);
|
||||
},
|
||||
});
|
||||
|
||||
const teacher = [authMiddleware, requireRole('teacher', 'admin')];
|
||||
const auth = [authMiddleware];
|
||||
|
||||
const chatLimiter = rateLimit({ windowMs: 5_000, max: 5, message: 'Слишком много сообщений, подождите' });
|
||||
|
||||
// Template library — MUST be before /:id to avoid shadowing
|
||||
router.get('/templates', ...teacher, c.getTemplates);
|
||||
router.delete('/templates/:tid', ...teacher, c.deleteTemplate);
|
||||
|
||||
// Session lifecycle
|
||||
router.post('/', ...teacher, c.createSession);
|
||||
router.get('/online-students', ...teacher, c.getOnlineStudents);
|
||||
router.get('/my/session', ...auth, c.getMySession);
|
||||
router.get('/class/:classId/active', ...auth, c.getActiveSession);
|
||||
router.get('/my/active', ...auth, c.getMyActive);
|
||||
router.get('/:id', ...auth, c.getSession);
|
||||
router.delete('/:id', ...teacher, c.endSession);
|
||||
|
||||
// Attendance
|
||||
router.post('/:id/join', ...auth, c.joinSession);
|
||||
router.post('/:id/leave', ...auth, c.leaveSession);
|
||||
router.get('/:id/participants', ...auth, c.getParticipants);
|
||||
router.get('/:id/attendance', ...teacher, c.getAttendance);
|
||||
|
||||
// Chat
|
||||
router.post('/:id/chat', ...auth, chatLimiter, c.sendChat);
|
||||
router.get('/:id/chat', ...auth, c.getChat);
|
||||
router.post('/:id/chat/upload', ...auth, chatUpload.single('file'), c.uploadChatAttachment);
|
||||
router.post('/:id/chat/:msgId/react', ...auth, c.reactToMessage);
|
||||
|
||||
// WebRTC signaling
|
||||
router.post('/:id/signal', ...auth, 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);
|
||||
|
||||
// Multi-page
|
||||
router.post('/:id/pages', ...teacher, c.addPage);
|
||||
router.put('/:id/page', ...teacher, c.changePage);
|
||||
router.patch('/:id/page-template', ...teacher, c.updatePageTemplate);
|
||||
|
||||
// Hand raise
|
||||
router.post('/:id/hand', ...auth, c.raiseHand);
|
||||
router.delete('/:id/hand', ...auth, c.lowerHand);
|
||||
router.get('/:id/hands', ...auth, c.getHands);
|
||||
|
||||
// Whiteboard: clear page
|
||||
router.post('/:id/clear-page', ...teacher, c.clearPage);
|
||||
|
||||
// WebRTC: mute peer, screen share broadcast
|
||||
router.post('/:id/mute', ...teacher, c.mutePeer);
|
||||
router.post('/:id/screen', ...teacher, c.screenStart);
|
||||
router.delete('/:id/screen', ...teacher, c.screenStop);
|
||||
|
||||
// Cursor broadcast (all participants)
|
||||
router.post('/:id/cursor', ...auth, c.broadcastCursor);
|
||||
|
||||
// Message pin (teacher only)
|
||||
router.post('/:id/chat/:msgId/pin', ...teacher, c.pinMessage);
|
||||
|
||||
// Collaborative drawing permissions
|
||||
router.post('/:id/allow-draw/:userId', ...teacher, c.allowDraw);
|
||||
router.delete('/:id/allow-draw/:userId', ...teacher, c.revokeDraw);
|
||||
|
||||
// Session notes (per user)
|
||||
router.get('/:id/notes', ...auth, c.getNotes);
|
||||
router.put('/:id/notes', ...auth, c.saveNotes);
|
||||
|
||||
// Save current session as template
|
||||
router.post('/:id/save-template', ...teacher, c.saveTemplate);
|
||||
// Load template into current session
|
||||
router.post('/:id/load-template', ...teacher, c.loadTemplate);
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user