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
+40
View File
@@ -0,0 +1,40 @@
/* Simple in-memory rate limiter — no external dependency needed */
// Clean stale entries every 5 minutes across all stores
const _allStores = new Set();
setInterval(() => {
const now = Date.now();
for (const store of _allStores) {
for (const [key, entry] of store) {
if (now > entry.resetAt) store.delete(key);
}
}
}, 5 * 60 * 1000).unref();
module.exports = function rateLimit({ windowMs = 60_000, max = 10, message = 'Too many requests, please try again later' } = {}) {
// Skip rate limiting in test environment
if (process.env.NODE_ENV === 'test') return (_req, _res, next) => next();
// Each rateLimit() call gets its own isolated store — counters don't bleed between limiters
const store = new Map();
_allStores.add(store);
return (req, res, next) => {
const key = req.ip || req.socket?.remoteAddress || 'unknown';
const now = Date.now();
let entry = store.get(key);
if (!entry || now > entry.resetAt) {
entry = { count: 0, resetAt: now + windowMs };
}
entry.count++;
store.set(key, entry);
if (entry.count > max) {
const retryAfter = Math.ceil((entry.resetAt - now) / 1000);
res.set('Retry-After', retryAfter);
return res.status(429).json({ error: message });
}
next();
};
};