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
+77
View File
@@ -0,0 +1,77 @@
'use strict';
/* ── Error handling middleware ─────────────────────────────────────────────
requestId — attach X-Request-Id to every request (use early in middleware chain)
errorHandler — 4-arg Express error handler (use as last app.use)
──────────────────────────────────────────────────────────────────────── */
const crypto = require('crypto');
const logger = require('../utils/logger');
let _errorLogStmt = null;
function getErrorLogStmt() {
if (!_errorLogStmt) {
try {
const db = require('../db/db');
_errorLogStmt = db.prepare(
'INSERT INTO error_log (level, message, stack, route, method, user_id) VALUES (?, ?, ?, ?, ?, ?)'
);
} catch {}
}
return _errorLogStmt;
}
/**
* Attaches a unique request ID to req.requestId and sets X-Request-Id header.
* Honour an incoming X-Request-Id from trusted proxies/gateways when present.
*/
function requestId(req, res, next) {
const id = req.headers['x-request-id'] || crypto.randomUUID();
req.requestId = id;
res.setHeader('X-Request-Id', id);
next();
}
/**
* Global error handler — must be registered AFTER all routes.
*
* Classifies errors:
* operational (4xx) — expected client errors → warn level, message returned as-is
* programmer (5xx) — unexpected bugs → error level, stack logged, message hidden in prod
*/
function errorHandler(err, req, res, _next) {
const status = err.status || err.statusCode || 500;
const isOperational = status >= 400 && status < 500;
const isProd = process.env.NODE_ENV === 'production';
const meta = {
requestId: req.requestId,
method: req.method,
path: req.path,
status,
userId: req.user?.id,
role: req.user?.role,
};
if (isOperational) {
logger.warn(err.message || 'Client error', meta);
} else {
logger.error(err.message || 'Unhandled error', {
...meta,
stack: !isProd ? err.stack : undefined,
});
// Persist to error_log table for admin dashboard
try {
const s = getErrorLogStmt();
if (s) s.run('error', (err.message || 'Unknown').slice(0, 1000), (err.stack || '').slice(0, 4000), req.path, req.method, req.user?.id || null);
} catch {}
}
const message = isProd && !isOperational
? 'Internal server error'
: (err.message || 'Server error');
if (res.headersSent) return;
res.status(status).json({ error: message, requestId: req.requestId });
}
module.exports = { requestId, errorHandler };