be4d43105e
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>
67 lines
2.6 KiB
JavaScript
67 lines
2.6 KiB
JavaScript
const jwt = require('jsonwebtoken');
|
|
const db = require('../db/db');
|
|
const { addClient, removeClient } = require('../sse');
|
|
|
|
const _stmts = {
|
|
list: db.prepare('SELECT id, type, message, link, is_read, created_at FROM notifications WHERE user_id = ? ORDER BY created_at DESC LIMIT 50'),
|
|
markOne: db.prepare('UPDATE notifications SET is_read = 1 WHERE id = ? AND user_id = ?'),
|
|
markAll: db.prepare('UPDATE notifications SET is_read = 1 WHERE user_id = ?'),
|
|
getUser: db.prepare('SELECT id, token_version, is_banned FROM users WHERE id = ?'),
|
|
};
|
|
|
|
/* ── GET /api/notifications ─────────────────────────────────────────────── */
|
|
function list(req, res) {
|
|
const rows = _stmts.list.all(req.user.id);
|
|
const unread = rows.filter(r => !r.is_read).length;
|
|
res.json({ notifications: rows, unread });
|
|
}
|
|
|
|
/* ── PATCH /api/notifications/:id/read ──────────────────────────────────── */
|
|
function markRead(req, res) {
|
|
_stmts.markOne.run(req.params.id, req.user.id);
|
|
res.json({ ok: true });
|
|
}
|
|
|
|
/* ── POST /api/notifications/read-all ───────────────────────────────────── */
|
|
function markAllRead(req, res) {
|
|
_stmts.markAll.run(req.user.id);
|
|
res.json({ ok: true });
|
|
}
|
|
|
|
/* ── GET /api/notifications/stream ── SSE (auth via ?token=JWT) ─────────── */
|
|
function stream(req, res) {
|
|
const token = req.query.token;
|
|
if (!token) return res.status(401).end();
|
|
|
|
let userId;
|
|
try {
|
|
const payload = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] });
|
|
const fresh = _stmts.getUser.get(payload.id);
|
|
if (!fresh) return res.status(401).end();
|
|
if (fresh.is_banned) return res.status(403).end();
|
|
if (fresh.token_version != null && payload.tv !== fresh.token_version) return res.status(401).end();
|
|
userId = payload.id;
|
|
} catch {
|
|
return res.status(401).end();
|
|
}
|
|
|
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
res.setHeader('Cache-Control', 'no-cache');
|
|
res.setHeader('Connection', 'keep-alive');
|
|
res.setHeader('X-Accel-Buffering', 'no');
|
|
res.setHeader('Referrer-Policy', 'no-referrer');
|
|
res.flushHeaders();
|
|
|
|
addClient(userId, res);
|
|
res.write(`data: ${JSON.stringify({ type: 'connected' })}\n\n`);
|
|
|
|
const hb = setInterval(() => { try { res.write(':hb\n\n'); } catch {} }, 25_000);
|
|
|
|
req.on('close', () => {
|
|
clearInterval(hb);
|
|
removeClient(userId, res);
|
|
});
|
|
}
|
|
|
|
module.exports = { list, markRead, markAllRead, stream };
|