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:
Maxim Dolgolyov
2026-04-12 10:10:37 +03:00
commit be4d43105e
204 changed files with 118117 additions and 0 deletions
+104
View File
@@ -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;