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,48 @@
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware, requireRole } = require('../middleware/auth');
|
||||
const ctrl = require('../controllers/adminController');
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
/* Features — teachers may read (need to know what's enabled for their classes) */
|
||||
router.get('/features', requireRole('admin', 'teacher'), ctrl.getFeatures);
|
||||
router.patch('/features', requireRole('admin'), ctrl.updateFeatures);
|
||||
router.get('/free-student-features', requireRole('admin', 'teacher'), ctrl.getFreeStudentFeatures);
|
||||
router.patch('/free-student-features', requireRole('admin'), ctrl.updateFreeStudentFeatures);
|
||||
|
||||
/* Everything below is admin-only */
|
||||
router.use(requireRole('admin'));
|
||||
|
||||
router.get('/stats', ctrl.getStats);
|
||||
router.get('/users', ctrl.getUsers);
|
||||
router.patch('/users/:id/role', ctrl.updateRole);
|
||||
router.get('/users/:id/sessions', ctrl.getUserSessions);
|
||||
router.delete('/users/:id/sessions', ctrl.clearUserSessions);
|
||||
router.post('/users/:id/sessions/clear', ctrl.clearUserSessions);
|
||||
router.patch('/users/:id', ctrl.updateUser);
|
||||
router.patch('/users/:id/ban', ctrl.banUser);
|
||||
router.delete('/users/:id', ctrl.deleteUser);
|
||||
router.get('/sessions', ctrl.getAllSessions);
|
||||
router.get('/sessions/:id', ctrl.getSessionDetail);
|
||||
|
||||
/* Audit log */
|
||||
router.get('/audit-log', ctrl.getAuditLog);
|
||||
router.delete('/audit-log', ctrl.clearAuditLog);
|
||||
|
||||
/* Error log */
|
||||
router.get('/error-log', ctrl.getErrorLog);
|
||||
router.delete('/error-log', ctrl.clearErrorLog);
|
||||
|
||||
/* System health */
|
||||
router.get('/health', ctrl.getHealth);
|
||||
|
||||
/* Topics CRUD */
|
||||
router.get('/topics', ctrl.getTopics);
|
||||
router.post('/topics', ctrl.createTopic);
|
||||
router.patch('/topics/:id', ctrl.updateTopic);
|
||||
router.delete('/topics/:id', ctrl.deleteTopic);
|
||||
|
||||
/* Broadcast notifications */
|
||||
router.post('/broadcast', ctrl.broadcast);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,7 @@
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware, requireRole } = require('../middleware/auth');
|
||||
const { teacherOverview } = require('../controllers/analyticsController');
|
||||
|
||||
router.get('/teacher', authMiddleware, requireRole('teacher', 'admin'), teacherOverview);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,56 @@
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware, requireRole } = require('../middleware/auth');
|
||||
const validate = require('../middleware/validate');
|
||||
const ctrl = require('../controllers/assignmentController');
|
||||
|
||||
const MODES = ['exam', 'practice', 'repeat', 'ct'];
|
||||
|
||||
const createSchema = { body: {
|
||||
title: { type: 'string', required: true, minLen: 1, maxLen: 200 },
|
||||
subject_slug: { type: 'string', maxLen: 100 },
|
||||
mode: { type: 'string', oneOf: MODES },
|
||||
count: { type: 'number', min: 1, max: 200 },
|
||||
deadline: { type: 'string', maxLen: 30 },
|
||||
}};
|
||||
|
||||
const updateSchema = { body: {
|
||||
title: { type: 'string', required: true, minLen: 1, maxLen: 200 },
|
||||
subject_slug: { type: 'string', maxLen: 100 },
|
||||
mode: { type: 'string', oneOf: MODES },
|
||||
count: { type: 'number', min: 1, max: 200 },
|
||||
deadline: { type: 'string', maxLen: 30 },
|
||||
}};
|
||||
|
||||
const directSchema = { body: {
|
||||
title: { type: 'string', required: true, minLen: 1, maxLen: 200 },
|
||||
subject_slug: { type: 'string', maxLen: 100 },
|
||||
mode: { type: 'string', oneOf: MODES },
|
||||
count: { type: 'number', min: 1, max: 200 },
|
||||
deadline: { type: 'string', maxLen: 30 },
|
||||
student_email: { type: 'string', maxLen: 255 },
|
||||
}};
|
||||
|
||||
const bulkSchema = { body: {
|
||||
title: { type: 'string', required: true, minLen: 1, maxLen: 200 },
|
||||
class_id: { type: 'number', required: true, min: 1 },
|
||||
mode: { type: 'string', oneOf: MODES },
|
||||
count: { type: 'number', min: 1, max: 200 },
|
||||
}};
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
router.get('/my', ctrl.myAssignments);
|
||||
router.get('/teacher', requireRole('teacher','admin'), ctrl.teacherAssignments);
|
||||
router.get('/templates', requireRole('teacher','admin'), ctrl.listTemplates);
|
||||
router.post('/templates', requireRole('teacher','admin'), ctrl.saveTemplate);
|
||||
router.delete('/templates/:id', requireRole('teacher','admin'), ctrl.deleteTemplate);
|
||||
router.post('/bulk', requireRole('teacher','admin'), validate(bulkSchema), ctrl.bulkCreateAssignment);
|
||||
router.post('/', requireRole('teacher','admin'), validate(directSchema), ctrl.createDirectAssignment);
|
||||
router.post('/:id/start', ctrl.startAssignment);
|
||||
router.get('/:id/results', requireRole('teacher','admin'), ctrl.assignmentResults);
|
||||
router.get('/:id/question-stats', requireRole('teacher','admin'), ctrl.assignmentQuestionStats);
|
||||
router.get('/:id/sessions/:session_id/review', requireRole('teacher','admin'), ctrl.assignmentSessionReview);
|
||||
router.put('/:id', requireRole('teacher','admin'), validate(updateSchema), ctrl.updateAssignment);
|
||||
router.delete('/:id', requireRole('teacher','admin'), ctrl.deleteAssignment);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,32 @@
|
||||
const router = require('express').Router();
|
||||
const { register, login, me, updateProfile } = require('../controllers/authController');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const rateLimit = require('../middleware/rateLimit');
|
||||
const validate = require('../middleware/validate');
|
||||
|
||||
const loginLimiter = rateLimit({ windowMs: 60_000, max: 10, message: 'Слишком много попыток входа, подождите минуту' });
|
||||
const registerLimiter = rateLimit({ windowMs: 60_000, max: 5, message: 'Слишком много регистраций, подождите минуту' });
|
||||
const profileLimiter = rateLimit({ windowMs: 60_000, max: 10, message: 'Слишком много запросов, подождите минуту' });
|
||||
|
||||
const registerSchema = { body: {
|
||||
email: { type: 'string', required: true, maxLen: 255, match: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ },
|
||||
password: { type: 'string', required: true, minLen: 6, maxLen: 128 },
|
||||
name: { type: 'string', required: true, minLen: 1, maxLen: 100 },
|
||||
}};
|
||||
const loginSchema = { body: {
|
||||
email: { type: 'string', required: true, maxLen: 255 },
|
||||
password: { type: 'string', required: true, minLen: 1, maxLen: 128 },
|
||||
}};
|
||||
const profileSchema = { body: {
|
||||
name: { type: 'string', minLen: 1, maxLen: 100 },
|
||||
newPassword: { type: 'string', minLen: 6, maxLen: 128 },
|
||||
currentPassword: { type: 'string', maxLen: 128 },
|
||||
}};
|
||||
|
||||
router.post('/register', registerLimiter, validate(registerSchema), register);
|
||||
router.post('/login', loginLimiter, validate(loginSchema), login);
|
||||
router.get('/me', authMiddleware, me);
|
||||
router.get('/profile', authMiddleware, me);
|
||||
router.patch('/profile', authMiddleware, profileLimiter, validate(profileSchema), updateProfile);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,18 @@
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const c = require('../controllers/biochemController');
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
router.get('/elements', c.getElements);
|
||||
router.get('/molecules', c.getMolecules);
|
||||
router.get('/molecules/:id', c.getMolecule);
|
||||
router.post('/validate', c.validate);
|
||||
router.get('/reactions', c.getReactions);
|
||||
router.get('/challenges', c.getChallenges);
|
||||
router.post('/challenges/:id/solve', c.solveChallenge);
|
||||
router.get('/saved', c.getSaved);
|
||||
router.post('/saved', c.saveMolecule);
|
||||
router.delete('/saved/:id', c.deleteSaved);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,13 @@
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const ctrl = require('../controllers/bookmarkController');
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
router.get('/', ctrl.list);
|
||||
router.post('/', ctrl.add);
|
||||
router.delete('/:id', ctrl.remove);
|
||||
router.delete('/entity/:type/:entityId', ctrl.removeByEntity);
|
||||
router.get('/check/:type/:entityId', ctrl.check);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,48 @@
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware, requireRole, requirePermission } = require('../middleware/auth');
|
||||
const rateLimit = require('../middleware/rateLimit');
|
||||
const validate = require('../middleware/validate');
|
||||
const ctrl = require('../controllers/classController');
|
||||
const assignCtrl = require('../controllers/assignmentController');
|
||||
|
||||
const joinLimiter = rateLimit({ windowMs: 60_000, max: 10, message: 'Слишком много попыток, подождите минуту' });
|
||||
const createLimiter = rateLimit({ windowMs: 60_000, max: 5, message: 'Слишком много запросов, подождите минуту' });
|
||||
const joinSchema = { body: { invite_code: { type: 'string', required: true, minLen: 1, maxLen: 20 } } };
|
||||
const createSchema = { body: { name: { type: 'string', required: true, minLen: 1, maxLen: 200 } } };
|
||||
const updateSchema = { body: {
|
||||
name: { type: 'string', minLen: 1, maxLen: 200 },
|
||||
description: { type: 'string', maxLen: 1000 },
|
||||
}};
|
||||
const assignmentSchema = { body: {
|
||||
title: { type: 'string', required: true, minLen: 1, maxLen: 200 },
|
||||
subject_slug: { type: 'string', maxLen: 100 },
|
||||
mode: { type: 'string', oneOf: ['exam', 'practice', 'repeat', 'ct'] },
|
||||
count: { type: 'number', min: 1, max: 200 },
|
||||
deadline: { type: 'string', maxLen: 30 },
|
||||
}};
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
/* ── Student (must be before /:id to avoid route conflicts) ── */
|
||||
router.post('/join', requireRole('student','free_student'), joinLimiter, validate(joinSchema), ctrl.joinClass);
|
||||
router.get('/student/my', ctrl.myClasses);
|
||||
router.get('/students', requireRole('teacher','admin'), ctrl.listStudents);
|
||||
|
||||
/* ── Teacher / Admin ── */
|
||||
router.get('/', requireRole('teacher','admin'), ctrl.listClasses);
|
||||
router.post('/', requireRole('teacher','admin'), requirePermission('classes.manage'), createLimiter, validate(createSchema), ctrl.createClass);
|
||||
router.get('/:id', requireRole('teacher','admin'), ctrl.getClass);
|
||||
router.patch('/:id', requireRole('teacher','admin'), requirePermission('classes.manage'), validate(updateSchema), ctrl.updateClass);
|
||||
router.delete('/:id', requireRole('teacher','admin'), requirePermission('classes.manage'), ctrl.deleteClass);
|
||||
router.post('/:id/new-code', requireRole('teacher','admin'), requirePermission('classes.manage'), ctrl.regenerateCode);
|
||||
router.get('/:id/journal', requireRole('teacher','admin'), ctrl.classJournal);
|
||||
router.get('/:id/journal/csv', requireRole('teacher','admin'), ctrl.classJournalCsv);
|
||||
router.post('/:id/members', requireRole('teacher','admin'), ctrl.addMember);
|
||||
router.delete('/:id/members/:uid', requireRole('teacher','admin'), ctrl.kickMember);
|
||||
router.post('/:id/assignments', requireRole('teacher','admin'), validate(assignmentSchema), assignCtrl.createAssignment);
|
||||
router.get('/:id/announcements', requireRole('teacher','admin'), ctrl.getAnnouncements);
|
||||
router.post('/:id/announcements', requireRole('teacher','admin'), requirePermission('announcements.send'), ctrl.createAnnouncement);
|
||||
router.delete('/:id/announcements/:aid', requireRole('teacher','admin'), requirePermission('announcements.send'), ctrl.deleteAnnouncement);
|
||||
router.get('/:id/feed', ctrl.classFeed);
|
||||
|
||||
module.exports = router;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,7 @@
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const c = require('../controllers/collectionController');
|
||||
|
||||
router.get('/', authMiddleware, c.getCollection);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,34 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { authMiddleware, requireRole, requirePermission } = require('../middleware/auth');
|
||||
const c = require('../controllers/courseController');
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
// Course listing & special
|
||||
router.get('/', c.list);
|
||||
router.get('/search', c.search);
|
||||
router.get('/continue', c.continueLesson);
|
||||
router.get('/:id', c.get);
|
||||
router.get('/:id/stats', requireRole('teacher','admin'), c.stats);
|
||||
router.get('/:id/analytics', requireRole('teacher','admin'), c.analytics);
|
||||
|
||||
// Course mutations
|
||||
router.post('/', requireRole('teacher','admin'), requirePermission('courses.manage'), c.create);
|
||||
router.post('/:id/duplicate', requireRole('teacher','admin'), requirePermission('courses.manage'), c.duplicate);
|
||||
router.patch('/:id/publish-all', requireRole('teacher','admin'), requirePermission('courses.manage'), c.publishAll);
|
||||
router.put('/:id', requireRole('teacher','admin'), requirePermission('courses.manage'), c.update);
|
||||
router.delete('/:id', requireRole('teacher','admin'), requirePermission('courses.manage'), c.remove);
|
||||
|
||||
// Sections
|
||||
router.get('/:id/sections', requireRole('teacher','admin'), c.listSections);
|
||||
router.post('/:id/sections', requireRole('teacher','admin'), c.createSection);
|
||||
router.put('/:id/sections/:sid', requireRole('teacher','admin'), c.updateSection);
|
||||
router.delete('/:id/sections/:sid', requireRole('teacher','admin'), c.deleteSection);
|
||||
|
||||
// Class courses
|
||||
router.get('/class/:classId', c.listClassCourses);
|
||||
router.post('/class/:classId/assign', requireRole('teacher','admin'), c.assignCourseToClass);
|
||||
router.delete('/class/:classId/:courseId', requireRole('teacher','admin'), c.unassignCourseFromClass);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,73 @@
|
||||
const router = require('express').Router();
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const { v4: uuidv4 } = require('crypto').randomUUID ? { v4: () => require('crypto').randomBytes(16).toString('hex') } : {};
|
||||
const { authMiddleware, requireRole, requirePermission } = require('../middleware/auth');
|
||||
const ctrl = require('../controllers/fileController');
|
||||
const { fixUtf8Name } = require('../utils/fixUtf8');
|
||||
|
||||
/* ── multer config ─────────────────────────────────────────────────────── */
|
||||
const UPLOADS_DIR = path.join(__dirname, '../../uploads');
|
||||
const ALLOWED = ['application/pdf','image/png','image/jpeg','image/gif','image/webp',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'text/plain'];
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: UPLOADS_DIR,
|
||||
filename: (_req, file, cb) => {
|
||||
const ext = path.extname(file.originalname);
|
||||
const name = require('crypto').randomBytes(16).toString('hex') + ext;
|
||||
cb(null, name);
|
||||
},
|
||||
});
|
||||
|
||||
const SAFE_EXTS = new Set(['.pdf','.png','.jpg','.jpeg','.gif','.webp','.doc','.docx','.ppt','.pptx','.xls','.xlsx','.txt']);
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: 50 * 1024 * 1024 }, // 50 MB
|
||||
fileFilter: (_req, file, cb) => {
|
||||
if (!ALLOWED.includes(file.mimetype)) return cb(null, false);
|
||||
// Reject double extensions (.php.jpg, .exe.pdf, etc.)
|
||||
const name = file.originalname;
|
||||
const parts = name.split('.');
|
||||
if (parts.length > 2) {
|
||||
const inner = '.' + parts[parts.length - 2].toLowerCase();
|
||||
if (['.php','.exe','.sh','.bat','.cmd','.ps1','.js','.html','.htm'].includes(inner)) return cb(null, false);
|
||||
}
|
||||
// Verify file extension is allowed
|
||||
const ext = path.extname(name).toLowerCase();
|
||||
if (ext && !SAFE_EXTS.has(ext)) return cb(null, false);
|
||||
cb(null, true);
|
||||
},
|
||||
});
|
||||
|
||||
/* ── routes ─────────────────────────────────────────────────────────────── */
|
||||
router.use(authMiddleware);
|
||||
|
||||
router.get('/', ctrl.listFiles);
|
||||
router.post('/', requireRole('teacher','admin'), requirePermission('library.upload'), upload.single('file'), fixUtf8Name, ctrl.uploadFile);
|
||||
|
||||
/* ── folder routes (must be before /:id to avoid conflicts) ── */
|
||||
router.get('/folders', ctrl.listFolders);
|
||||
router.post('/folders', requireRole('teacher','admin'), requirePermission('library.folders'), ctrl.createFolder);
|
||||
router.put('/folders/:id',requireRole('teacher','admin'), requirePermission('library.folders'), ctrl.renameFolder);
|
||||
router.delete('/folders/:id', requireRole('teacher','admin'), requirePermission('library.folders'), ctrl.deleteFolder);
|
||||
router.get('/folders/:id/access', requireRole('teacher','admin'), requirePermission('library.folders'), ctrl.getFolderAccess);
|
||||
router.delete('/folders/:id/access', requireRole('teacher','admin'), requirePermission('library.folders'), ctrl.clearFolderAccess);
|
||||
router.post('/folders/:id/assign', requireRole('teacher','admin'), requirePermission('library.folders'), ctrl.assignFolder);
|
||||
router.delete('/folders/:id/assign/:type/:targetId', requireRole('teacher','admin'), requirePermission('library.folders'), ctrl.unassignFolder);
|
||||
|
||||
router.patch('/:id/move', requireRole('teacher','admin'), ctrl.moveFile);
|
||||
router.get('/:id/download', ctrl.downloadFile);
|
||||
router.delete('/:id', requireRole('teacher','admin'), ctrl.deleteFile);
|
||||
router.get('/:id/access', requireRole('teacher','admin'), ctrl.getFileAccess);
|
||||
router.post('/:id/assign', requireRole('teacher','admin'), ctrl.assignFile);
|
||||
router.delete('/:id/assign/:type/:targetId', requireRole('teacher','admin'), ctrl.unassignFile);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,21 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const fc = require('../controllers/flashcardController');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
router.get ('/decks', fc.listDecks);
|
||||
router.post ('/decks', fc.createDeck);
|
||||
router.put ('/decks/:id', fc.updateDeck);
|
||||
router.delete('/decks/:id', fc.deleteDeck);
|
||||
router.get ('/decks/:id/cards', fc.getCards);
|
||||
router.post ('/decks/:id/cards', fc.addCard);
|
||||
router.post ('/decks/:id/cards/bulk', fc.addCardsBulk);
|
||||
router.get ('/decks/:id/study', fc.getStudySession);
|
||||
router.put ('/cards/:id', fc.updateCard);
|
||||
router.delete('/cards/:id', fc.deleteCard);
|
||||
router.post ('/cards/:id/review', fc.submitReview);
|
||||
router.get ('/stats', fc.getStats);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,10 @@
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const c = require('../controllers/gamesController');
|
||||
|
||||
router.get('/hangman/word', authMiddleware, c.hangmanWord);
|
||||
router.post('/hangman/complete', authMiddleware, c.hangmanComplete);
|
||||
router.get('/crossword/generate', authMiddleware, c.crosswordGenerate);
|
||||
router.post('/crossword/complete', authMiddleware, c.crosswordComplete);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,40 @@
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware, requireRole, requirePermission } = require('../middleware/auth');
|
||||
const validate = require('../middleware/validate');
|
||||
const rateLimit = require('../middleware/rateLimit');
|
||||
const {
|
||||
getMe, getAchievements, getLeaderboard, getXPHistory,
|
||||
getChallenges, claimChallenge, setGoalTier, getFrames, setFrame,
|
||||
onLabExperiment,
|
||||
adminAward, adminReset, adminGamStats, adminGetUser
|
||||
} = require('../controllers/gamificationController');
|
||||
|
||||
const labLimiter = rateLimit({ windowMs: 60_000, max: 30, message: 'Слишком частые запросы лаборатории' });
|
||||
const labSchema = { body: { reactionsDiscovered: { type: 'number', min: 0, max: 100, integer: true } } };
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
router.get('/me', getMe);
|
||||
router.get('/achievements', getAchievements);
|
||||
router.get('/leaderboard', getLeaderboard);
|
||||
router.get('/xp-history', getXPHistory);
|
||||
router.get('/challenges', getChallenges);
|
||||
router.post('/challenges/:id/claim', requirePermission('gamification.challenges'), claimChallenge);
|
||||
router.post('/goal-tier', requirePermission('gamification.challenges'), setGoalTier);
|
||||
router.get('/frames', getFrames);
|
||||
router.post('/frame', requirePermission('shop.purchase'), setFrame);
|
||||
|
||||
/* Lab experiment tracking */
|
||||
router.post('/lab-activity', requirePermission('simulations.access'), labLimiter, validate(labSchema), (req, res) => {
|
||||
const discovered = Number(req.body.reactionsDiscovered) || 0;
|
||||
onLabExperiment(req.user.id, discovered);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
/* Admin routes */
|
||||
router.post('/admin/award', requireRole('admin', 'teacher'), adminAward);
|
||||
router.post('/admin/reset', requireRole('admin'), adminReset);
|
||||
router.get('/admin/stats', requireRole('admin', 'teacher'), adminGamStats);
|
||||
router.get('/admin/user/:id', requireRole('admin', 'teacher'), adminGetUser);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,7 @@
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const c = require('../controllers/knowledgeMapController');
|
||||
|
||||
router.get('/', authMiddleware, c.getMap);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,21 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { authMiddleware, requireRole } = require('../middleware/auth');
|
||||
const c = require('../controllers/lessonController');
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
router.get('/:id', c.get);
|
||||
router.post('/:id/complete', c.markComplete);
|
||||
router.put('/:id/note', c.saveNote);
|
||||
router.get('/:id/comments', c.listComments);
|
||||
router.post('/:id/comments', c.addComment);
|
||||
router.delete('/:id/comments/:cid', c.deleteComment);
|
||||
|
||||
// Teacher/admin only
|
||||
router.post('/', requireRole('teacher','admin'), c.create);
|
||||
router.put('/:id', requireRole('teacher','admin'), c.update);
|
||||
router.delete('/:id', requireRole('teacher','admin'), c.remove);
|
||||
router.put('/:id/blocks', requireRole('teacher','admin'), c.saveBlocks);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,16 @@
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware, requireRole } = require('../middleware/auth');
|
||||
const c = require('../controllers/liveController');
|
||||
|
||||
const teacher = [authMiddleware, requireRole('teacher', 'admin')];
|
||||
|
||||
router.post('/', ...teacher, c.create);
|
||||
router.get('/:id', ...teacher, c.getSession);
|
||||
router.put('/:id/question', ...teacher, c.setQuestion);
|
||||
router.get('/:id/results', ...teacher, c.results);
|
||||
router.delete('/:id', ...teacher, c.end);
|
||||
|
||||
router.post('/:id/answer', authMiddleware, c.answer);
|
||||
router.get('/class/:classId/active', authMiddleware, c.getActive);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,13 @@
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const ctrl = require('../controllers/notificationController');
|
||||
|
||||
// SSE stream — auth done inside handler via ?token param (EventSource can't set headers)
|
||||
router.get('/stream', ctrl.stream);
|
||||
|
||||
router.use(authMiddleware);
|
||||
router.get('/', ctrl.list);
|
||||
router.post('/read-all', ctrl.markAllRead);
|
||||
router.patch('/:id/read', ctrl.markRead);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,26 @@
|
||||
const router = require('express').Router();
|
||||
const rateLimit = require('../middleware/rateLimit');
|
||||
const { authMiddleware, requireRole, parentAuth } = require('../middleware/auth');
|
||||
const ctrl = require('../controllers/parentController');
|
||||
|
||||
/* ── Rate limits ───────────────────────────────────────────────────── */
|
||||
const authLimiter = rateLimit({ windowMs: 60_000, max: 5, message: 'Слишком много попыток, подождите минуту' });
|
||||
const parentLimiter = rateLimit({ windowMs: 60_000, max: 30, message: 'Слишком много запросов' });
|
||||
|
||||
/* ── Public: token exchange ────────────────────────────────────────── */
|
||||
router.post('/auth', authLimiter, ctrl.exchangeToken);
|
||||
|
||||
/* ── Student-side: manage parent links ─────────────────────────────── */
|
||||
router.get('/my-links', authMiddleware, requireRole('student', 'free_student'), ctrl.getMyLinks);
|
||||
router.post('/links', authMiddleware, requireRole('student', 'free_student'), ctrl.createLink);
|
||||
router.patch('/links/:id', authMiddleware, requireRole('student', 'free_student'), ctrl.updateLink);
|
||||
router.delete('/links/:id', authMiddleware, requireRole('student', 'free_student'), ctrl.deleteLink);
|
||||
|
||||
/* ── Parent-side: read-only dashboard (rate-limited) ───────────────── */
|
||||
router.use(parentAuth, parentLimiter);
|
||||
router.get('/dashboard', ctrl.getDashboard);
|
||||
router.get('/history', ctrl.getHistory);
|
||||
router.get('/notifications', ctrl.getNotifications);
|
||||
router.patch('/notifications/:id/read', ctrl.markRead);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,20 @@
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware, requireRole } = require('../middleware/auth');
|
||||
const { getPermissions, setPermission, getMyPermissions, getUserPermissions, setUserPermission, resetUserPermissions } = require('../controllers/permissionsController');
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
/* Any authenticated user can fetch their own effective permissions */
|
||||
router.get('/me', getMyPermissions);
|
||||
|
||||
router.use(requireRole('admin'));
|
||||
|
||||
router.get('/', getPermissions);
|
||||
router.post('/', setPermission);
|
||||
|
||||
/* ── Per-user overrides ── */
|
||||
router.get('/users/:id', getUserPermissions);
|
||||
router.post('/users/:id', setUserPermission);
|
||||
router.delete('/users/:id/reset', resetUserPermissions);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,15 @@
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const c = require('../controllers/petController');
|
||||
|
||||
router.get('/', authMiddleware, c.getPet);
|
||||
router.patch('/name', authMiddleware, c.renamePet);
|
||||
router.post('/pet', authMiddleware, c.petAction);
|
||||
router.patch('/color', authMiddleware, c.updateColor);
|
||||
router.post('/star', authMiddleware, c.starCatch);
|
||||
router.get('/shop', authMiddleware, c.getShop);
|
||||
router.post('/shop/buy', authMiddleware, c.buyBg);
|
||||
router.patch('/bg', authMiddleware, c.setBg);
|
||||
router.post('/feed', authMiddleware, c.feedPet);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,18 @@
|
||||
const router = require('express').Router();
|
||||
const multer = require('multer');
|
||||
const { authMiddleware, requireRole, requirePermission } = require('../middleware/auth');
|
||||
const { list, create, duplicate, update, remove, importCSV } = require('../controllers/questionController');
|
||||
|
||||
const csvUpload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 2 * 1024 * 1024 } });
|
||||
|
||||
router.use(authMiddleware);
|
||||
router.use(requireRole('admin', 'teacher'));
|
||||
|
||||
router.get('/', list);
|
||||
router.post('/import', requirePermission('questions.manage'), csvUpload.single('file'), importCSV);
|
||||
router.post('/', requirePermission('questions.manage'), create);
|
||||
router.post('/:id/copy', requirePermission('questions.manage'), duplicate);
|
||||
router.put('/:id', requirePermission('questions.manage'), update);
|
||||
router.delete('/:id', requireRole('admin', 'teacher'), requirePermission('questions.delete'), remove);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,25 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const ctrl = require('../controllers/redBookController');
|
||||
const { requireAuth, optionalAuth } = require('../middleware/auth');
|
||||
|
||||
// Public (or optional auth for collection status)
|
||||
router.get('/groups', ctrl.getGroups);
|
||||
router.get('/habitats', ctrl.getHabitats);
|
||||
router.get('/stats', optionalAuth, ctrl.getStats);
|
||||
router.get('/map-data', ctrl.getMapData);
|
||||
router.get('/food-web', ctrl.getFoodWeb);
|
||||
router.get('/daily', optionalAuth, ctrl.getDaily);
|
||||
router.get('/species', optionalAuth, ctrl.getSpecies);
|
||||
router.get('/species/:id', optionalAuth, ctrl.getSpeciesById);
|
||||
router.get('/biome/:habitatId', ctrl.getBiomeSpecies);
|
||||
|
||||
// Auth required
|
||||
router.post('/species/:id/collect', requireAuth, ctrl.collectSpecies);
|
||||
router.get('/collection', requireAuth, ctrl.getCollection);
|
||||
router.get('/quests', optionalAuth, ctrl.getQuests);
|
||||
router.post('/quests/:id/start', requireAuth, ctrl.startQuest);
|
||||
router.get('/sightings', optionalAuth, ctrl.getSightings);
|
||||
router.post('/sightings', requireAuth, ctrl.addSighting);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,8 @@
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const ctrl = require('../controllers/searchController');
|
||||
|
||||
router.use(authMiddleware);
|
||||
router.get('/', ctrl.search);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,28 @@
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const rateLimit = require('../middleware/rateLimit');
|
||||
const validate = require('../middleware/validate');
|
||||
const { start, answer, finish, result, history, weakTopics, getSessionQuestions, stats } = require('../controllers/sessionController');
|
||||
|
||||
const sessionLimiter = rateLimit({ windowMs: 60_000, max: 10, message: 'Слишком много сессий, подождите минуту' });
|
||||
const startSchema = { body: {
|
||||
subject_slug: { type: 'string', required: true, minLen: 1, maxLen: 100 },
|
||||
mode: { type: 'string', oneOf: ['exam', 'practice', 'repeat', 'ct'] },
|
||||
count: { type: 'number', min: 1, max: 200, integer: true },
|
||||
}};
|
||||
const answerSchema = { body: {
|
||||
question_id: { type: 'number', required: true, min: 1, integer: true },
|
||||
}};
|
||||
|
||||
router.use(authMiddleware); // все маршруты требуют авторизации
|
||||
|
||||
router.post('/', sessionLimiter, validate(startSchema), start);
|
||||
router.post('/:id/answer', validate(answerSchema), answer);
|
||||
router.post('/:id/finish', finish);
|
||||
router.get('/history', history);
|
||||
router.get('/stats', stats);
|
||||
router.get('/weak-topics', weakTopics);
|
||||
router.get('/:id/result', result);
|
||||
router.get('/:id/questions', getSessionQuestions);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,12 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { authMiddleware, requireRole } = require('../middleware/auth');
|
||||
const { getSimSettings, updateSimSettings } = require('../controllers/settingsController');
|
||||
|
||||
// GET is open to any authenticated user (students need to check disabled sims)
|
||||
router.get('/sims', authMiddleware, getSimSettings);
|
||||
|
||||
// PUT requires admin
|
||||
router.put('/sims', authMiddleware, requireRole('admin'), updateSimSettings);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,39 @@
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware, requireRole, requirePermission } = require('../middleware/auth');
|
||||
const rateLimit = require('../middleware/rateLimit');
|
||||
const validate = require('../middleware/validate');
|
||||
const {
|
||||
getItems, purchaseItem, getPurchases, getCoins, getMyActive, activateItem,
|
||||
adminGetItems, adminCreateItem, adminUpdateItem, adminDeleteItem, adminAwardCoins, adminShopStats
|
||||
} = require('../controllers/shopController');
|
||||
|
||||
const purchaseLimiter = rateLimit({ windowMs: 60_000, max: 10, message: 'Слишком много покупок, подождите минуту' });
|
||||
const activateSchema = { body: { type: { type: 'string', oneOf: ['frame', 'title', 'effect'] } } };
|
||||
const adminItemSchema = { body: {
|
||||
name: { type: 'string', required: true, minLen: 1, maxLen: 200 },
|
||||
type: { type: 'string', required: true, oneOf: ['frame', 'title', 'effect'] },
|
||||
price: { type: 'number', required: true, min: 0 },
|
||||
}};
|
||||
const awardCoinsSchema = { body: {
|
||||
userId: { type: 'number', required: true, min: 1, integer: true },
|
||||
amount: { type: 'number', required: true, min: 1, integer: true },
|
||||
}};
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
router.get('/items', getItems);
|
||||
router.post('/items/:id/purchase', requirePermission('shop.purchase'), purchaseLimiter, purchaseItem);
|
||||
router.get('/purchases', getPurchases);
|
||||
router.get('/coins', getCoins);
|
||||
router.get('/my-active', getMyActive);
|
||||
router.post('/activate', validate(activateSchema), activateItem);
|
||||
|
||||
/* Admin routes */
|
||||
router.get('/admin/items', requireRole('admin', 'teacher'), adminGetItems);
|
||||
router.post('/admin/items', requireRole('admin'), validate(adminItemSchema), adminCreateItem);
|
||||
router.put('/admin/items/:id', requireRole('admin'), adminUpdateItem);
|
||||
router.delete('/admin/items/:id',requireRole('admin'), adminDeleteItem);
|
||||
router.post('/admin/award-coins',requireRole('admin', 'teacher'), validate(awardCoinsSchema), adminAwardCoins);
|
||||
router.get('/admin/stats', requireRole('admin', 'teacher'), adminShopStats);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,44 @@
|
||||
const router = require('express').Router();
|
||||
const db = require('../db/db');
|
||||
const { authMiddleware, requireRole } = require('../middleware/auth');
|
||||
|
||||
router.get('/', (_req, res) => {
|
||||
res.json(db.prepare(`
|
||||
SELECT s.*, (SELECT COUNT(*) FROM questions q WHERE q.subject_id = s.id) AS question_count
|
||||
FROM subjects s ORDER BY s.id
|
||||
`).all());
|
||||
});
|
||||
|
||||
router.patch('/:slug', authMiddleware, requireRole('admin'), (req, res) => {
|
||||
const { default_mode, default_count, default_test_id } = req.body;
|
||||
const valid_modes = ['exam', 'practice', 'topic', 'random'];
|
||||
if (default_mode && !valid_modes.includes(default_mode))
|
||||
return res.status(400).json({ error: 'Invalid mode' });
|
||||
|
||||
const subj = db.prepare('SELECT id FROM subjects WHERE slug = ?').get(req.params.slug);
|
||||
if (!subj) return res.status(404).json({ error: 'Subject not found' });
|
||||
|
||||
const updates = [];
|
||||
const args = [];
|
||||
if (default_mode !== undefined) { updates.push('default_mode = ?'); args.push(default_mode); }
|
||||
if (default_count !== undefined) { updates.push('default_count = ?'); args.push(Number(default_count) || 25); }
|
||||
if (default_test_id !== undefined) { updates.push('default_test_id = ?'); args.push(default_test_id || null); }
|
||||
if (!updates.length) return res.status(400).json({ error: 'Nothing to update' });
|
||||
|
||||
args.push(req.params.slug);
|
||||
db.prepare(`UPDATE subjects SET ${updates.join(', ')} WHERE slug = ?`).run(...args);
|
||||
res.json(db.prepare('SELECT * FROM subjects WHERE slug = ?').get(req.params.slug));
|
||||
});
|
||||
|
||||
router.get('/:slug/topics', (req, res) => {
|
||||
const subject = db.prepare('SELECT id FROM subjects WHERE slug = ?').get(req.params.slug);
|
||||
if (!subject) return res.status(404).json({ error: 'Subject not found' });
|
||||
|
||||
const topics = db.prepare(
|
||||
'SELECT MIN(id) AS id, subject_id, name, MIN(order_index) AS order_index FROM topics WHERE subject_id = ? GROUP BY name ORDER BY MIN(order_index)'
|
||||
).all(subject.id);
|
||||
|
||||
res.json(topics);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,60 @@
|
||||
const router = require('express').Router();
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const { authMiddleware, requireRole } = require('../middleware/auth');
|
||||
const ctrl = require('../controllers/submissionsController');
|
||||
const { fixUtf8Name } = require('../utils/fixUtf8');
|
||||
|
||||
/* ── multer — same dir/types as library uploads ─────────────────────── */
|
||||
const UPLOADS_DIR = path.join(__dirname, '../../uploads');
|
||||
const ALLOWED = [
|
||||
'application/pdf', 'image/png', 'image/jpeg', 'image/gif', 'image/webp',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'text/plain',
|
||||
];
|
||||
const SAFE_EXTS = new Set(['.pdf','.png','.jpg','.jpeg','.gif','.webp','.doc','.docx','.ppt','.pptx','.xls','.xlsx','.txt']);
|
||||
const storage = multer.diskStorage({
|
||||
destination: UPLOADS_DIR,
|
||||
filename: (_req, file, cb) => {
|
||||
const ext = path.extname(file.originalname);
|
||||
const name = require('crypto').randomBytes(16).toString('hex') + ext;
|
||||
cb(null, name);
|
||||
},
|
||||
});
|
||||
const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: 50 * 1024 * 1024 },
|
||||
fileFilter: (_req, file, cb) => {
|
||||
if (!ALLOWED.includes(file.mimetype)) return cb(null, false);
|
||||
// Reject double extensions (.php.jpg, .exe.pdf, etc.)
|
||||
const name = file.originalname;
|
||||
const parts = name.split('.');
|
||||
if (parts.length > 2) {
|
||||
const inner = '.' + parts[parts.length - 2].toLowerCase();
|
||||
if (['.php','.exe','.sh','.bat','.cmd','.ps1','.js','.html','.htm'].includes(inner)) return cb(null, false);
|
||||
}
|
||||
const ext = path.extname(name).toLowerCase();
|
||||
if (ext && !SAFE_EXTS.has(ext)) return cb(null, false);
|
||||
cb(null, true);
|
||||
},
|
||||
});
|
||||
|
||||
/* ── routes ─────────────────────────────────────────────────────────── */
|
||||
router.use(authMiddleware);
|
||||
|
||||
router.post('/', requireRole('student', 'free_student'), upload.single('file'), fixUtf8Name, ctrl.submit);
|
||||
router.get('/my', requireRole('student', 'free_student'), ctrl.getMySubmissions);
|
||||
router.get('/log', requireRole('admin'), ctrl.getSubmissionLog);
|
||||
router.delete('/log', requireRole('admin'), ctrl.clearSubmissionLog);
|
||||
router.get('/', requireRole('teacher', 'admin'), ctrl.getClassSubmissions);
|
||||
router.patch('/:id', requireRole('teacher', 'admin'), ctrl.reviewSubmission);
|
||||
router.get('/:id/download', ctrl.downloadSubmission);
|
||||
router.delete('/:id', ctrl.deleteSubmission);
|
||||
router.post('/:id/resubmit', requireRole('student', 'free_student'), upload.single('file'), fixUtf8Name, ctrl.resubmit);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,17 @@
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware, requirePermission } = require('../middleware/auth');
|
||||
const ctrl = require('../controllers/templateController');
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
router.get('/courses', ctrl.listCourseTemplates);
|
||||
router.post('/courses', requirePermission('templates.manage'), ctrl.saveCourseTemplate);
|
||||
router.post('/courses/:id/create', requirePermission('templates.manage'), ctrl.createFromCourseTemplate);
|
||||
router.delete('/courses/:id', requirePermission('templates.manage'), ctrl.deleteCourseTemplate);
|
||||
|
||||
router.get('/lessons', ctrl.listLessonTemplates);
|
||||
router.post('/lessons', requirePermission('templates.manage'), ctrl.saveLessonTemplate);
|
||||
router.post('/lessons/:id/create', requirePermission('templates.manage'), ctrl.createFromLessonTemplate);
|
||||
router.delete('/lessons/:id', requirePermission('templates.manage'), ctrl.deleteLessonTemplate);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,19 @@
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware, requireRole } = require('../middleware/auth');
|
||||
const { requireOwnership } = require('../middleware/ownership');
|
||||
const ctrl = require('../controllers/testController');
|
||||
|
||||
const ownsTest = requireOwnership({ table: 'tests', ownerField: 'created_by' });
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
router.get('/', ctrl.list);
|
||||
router.post('/', requireRole('teacher','admin'), ctrl.create);
|
||||
router.get('/:id', ctrl.getOne);
|
||||
router.put('/:id', requireRole('teacher','admin'), ownsTest, ctrl.update);
|
||||
router.delete('/:id', requireRole('teacher','admin'), ownsTest, ctrl.remove);
|
||||
router.post('/:id/questions', requireRole('teacher','admin'), ownsTest, ctrl.addQuestions);
|
||||
router.patch('/:id/questions/reorder', requireRole('teacher','admin'), ownsTest, ctrl.reorderQuestions);
|
||||
router.delete('/:id/questions/:qid', requireRole('teacher','admin'), ownsTest, ctrl.removeQuestion);
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user