3a4623a60a
Безопасность: - tests/🆔 скрыть is_correct и explanation для студентов (P0) - SQL injection: limit/offset через placeholder вместо template literal - Stored XSS: stripTags для lesson comments, flashcards, redBook sightings - profile.html: escape e.message в showMsg (XSS через server error) - attachment_url: валидация только /uploads/* путей - requestId: генерировать UUID сервером, не доверять клиенту - register: скрыть token_version из ответа Надёжность: - register: обработка UNIQUE constraint race condition - pet buyBg: re-check баланса внутри транзакции - DB errors: скрыть e.message в testController/questionController/courseController - preferences: лимит 50KB на размер JSON UX: - board.html: debounce 250ms на search input Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
55 lines
2.4 KiB
JavaScript
55 lines
2.4 KiB
JavaScript
const db = require('../db/db');
|
|
|
|
// Recursive deep merge: values from `patch` override `base`, objects are merged
|
|
function deepMerge(base, patch) {
|
|
const result = Object.assign({}, base);
|
|
for (const key of Object.keys(patch)) {
|
|
if (
|
|
patch[key] !== null &&
|
|
typeof patch[key] === 'object' &&
|
|
!Array.isArray(patch[key]) &&
|
|
typeof result[key] === 'object' &&
|
|
result[key] !== null &&
|
|
!Array.isArray(result[key])
|
|
) {
|
|
result[key] = deepMerge(result[key], patch[key]);
|
|
} else {
|
|
result[key] = patch[key];
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/* ── GET /api/preferences ────────────────────────────────────────────────── */
|
|
function getPreferences(req, res) {
|
|
const row = db.prepare('SELECT data FROM user_preferences WHERE user_id = ?').get(req.user.id);
|
|
res.json(JSON.parse(row?.data || '{}'));
|
|
}
|
|
|
|
/* ── PATCH /api/preferences ──────────────────────────────────────────────── */
|
|
function patchPreferences(req, res) {
|
|
if (!req.body || typeof req.body !== 'object' || Array.isArray(req.body)) {
|
|
return res.status(400).json({ error: 'Body must be a JSON object' });
|
|
}
|
|
const current = JSON.parse(
|
|
db.prepare('SELECT data FROM user_preferences WHERE user_id = ?').get(req.user.id)?.data || '{}'
|
|
);
|
|
const merged = deepMerge(current, req.body);
|
|
if (JSON.stringify(merged).length > 50_000)
|
|
return res.status(413).json({ error: 'Preferences too large (max 50KB)' });
|
|
db.prepare(`
|
|
INSERT INTO user_preferences (user_id, data, updated_at)
|
|
VALUES (?, ?, datetime('now'))
|
|
ON CONFLICT(user_id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at
|
|
`).run(req.user.id, JSON.stringify(merged));
|
|
res.json(merged);
|
|
}
|
|
|
|
/* ── DELETE /api/preferences ─────────────────────────────────────────────── */
|
|
function resetPreferences(req, res) {
|
|
db.prepare('DELETE FROM user_preferences WHERE user_id = ?').run(req.user.id);
|
|
res.json({});
|
|
}
|
|
|
|
module.exports = { getPreferences, patchPreferences, resetPreferences };
|