Build and Push Docker Image / build-and-push (push) Successful in 5m37s
Routes move to server/routes/ (auth, account, projects, reader, sharing, admin); index.js keeps setup only. A smoke test walks 90 API calls against the real server and compares every status with the pre-split behaviour. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
155 lines
7.1 KiB
JavaScript
155 lines
7.1 KiB
JavaScript
// Reading data synced between devices: ink, preferences, bookmarks and highlights, prayers, reading history, plan progress.
|
|
import { getAllReaderInk, getPrayers, getReaderHistory, getReaderMarks, getReadingProgress, getUserPrefs, setPrayers, setReaderHistory, setReaderInkPage, setReaderMarks, setReadingProgress, setUserPrefs } from '../db.js';
|
|
import { requireAuth } from '../auth.js';
|
|
import { mergePrayers } from '../prayers.js';
|
|
import { mergeHistory } from '../readerHistory.js';
|
|
import { cleanMarks } from '../readerMarks.js';
|
|
import { cleanPrefsPatch, mergePrefs } from '../prefs.js';
|
|
|
|
export function registerReaderRoutes(app) {
|
|
// ---------------------------------------------------------------------------
|
|
// Reader ink — cross-device draw annotations on Bible chapters
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// GET /api/reader/ink — all saved ink pages for the current user
|
|
app.get('/api/reader/ink', requireAuth, (req, res) => {
|
|
try {
|
|
res.json(getAllReaderInk(req.session.userId));
|
|
} catch (err) {
|
|
console.error('GET /api/reader/ink error:', err);
|
|
res.status(500).json({ error: 'Failed to load reader ink.' });
|
|
}
|
|
});
|
|
|
|
// PUT /api/reader/ink/:book/:chapter — save (upsert) one page's ink
|
|
app.put('/api/reader/ink/:book/:chapter', requireAuth, (req, res) => {
|
|
try {
|
|
const { book, chapter } = req.params;
|
|
const { strokes } = req.body ?? {};
|
|
if (!/^[1-3A-Za-z]{3}$/.test(book) || !/^\d{1,3}$/.test(chapter)) return res.status(400).json({ error: 'Invalid book or chapter.' });
|
|
if (!Array.isArray(strokes)) return res.status(400).json({ error: 'strokes must be an array.' });
|
|
if (strokes.length > 20000) return res.status(413).json({ error: 'Too much ink on one page.' });
|
|
setReaderInkPage(req.session.userId, book, chapter, strokes);
|
|
res.json({ ok: true });
|
|
} catch (err) {
|
|
console.error('PUT /api/reader/ink error:', err);
|
|
res.status(500).json({ error: 'Failed to save reader ink.' });
|
|
}
|
|
});
|
|
|
|
// GET /api/prefs — the user's synced display preferences ({ prefs: { key: { v, at } } })
|
|
app.get('/api/prefs', requireAuth, (req, res) => {
|
|
try {
|
|
res.json({ prefs: getUserPrefs(req.session.userId) });
|
|
} catch (err) {
|
|
console.error('GET /api/prefs error:', err);
|
|
res.status(500).json({ error: 'Failed to load preferences.' });
|
|
}
|
|
});
|
|
|
|
// PUT /api/prefs — merge changed preferences in (newest change per key wins); returns the result
|
|
app.put('/api/prefs', requireAuth, (req, res) => {
|
|
try {
|
|
const merged = mergePrefs(getUserPrefs(req.session.userId), cleanPrefsPatch(req.body));
|
|
setUserPrefs(req.session.userId, merged);
|
|
res.json({ prefs: merged });
|
|
} catch (err) {
|
|
console.error('PUT /api/prefs error:', err);
|
|
res.status(500).json({ error: 'Failed to save preferences.' });
|
|
}
|
|
});
|
|
|
|
// GET /api/reader/marks — the user's bookmarks + highlights, with an updatedAt to compare against
|
|
app.get('/api/reader/marks', requireAuth, (req, res) => {
|
|
try {
|
|
res.json(getReaderMarks(req.session.userId));
|
|
} catch (err) {
|
|
console.error('GET /api/reader/marks error:', err);
|
|
res.status(500).json({ error: 'Failed to load bookmarks and highlights.' });
|
|
}
|
|
});
|
|
|
|
// GET /api/prayers — the prayer journal; PUT merges this device's copy in (newer prayer wins) and returns the whole
|
|
app.get('/api/prayers', requireAuth, (req, res) => {
|
|
try { res.json({ prayers: mergePrayers(getPrayers(req.session.userId), []) }); } catch (err) {
|
|
console.error('GET /api/prayers error:', err);
|
|
res.status(500).json({ error: 'Failed to load your prayers.' });
|
|
}
|
|
});
|
|
app.put('/api/prayers', requireAuth, (req, res) => {
|
|
try {
|
|
const merged = mergePrayers(getPrayers(req.session.userId), req.body?.prayers);
|
|
setPrayers(req.session.userId, merged);
|
|
res.json({ prayers: merged });
|
|
} catch (err) {
|
|
console.error('PUT /api/prayers error:', err);
|
|
res.status(500).json({ error: 'Failed to save your prayers.' });
|
|
}
|
|
});
|
|
|
|
// GET /api/reader/history — what has been read, and PUT merges this device's copy in and returns the whole
|
|
app.get('/api/reader/history', requireAuth, (req, res) => {
|
|
try { res.json(mergeHistory(getReaderHistory(req.session.userId), {})); } catch (err) {
|
|
console.error('GET /api/reader/history error:', err);
|
|
res.status(500).json({ error: 'Failed to load your reading history.' });
|
|
}
|
|
});
|
|
app.put('/api/reader/history', requireAuth, (req, res) => {
|
|
try {
|
|
const merged = mergeHistory(getReaderHistory(req.session.userId), req.body);
|
|
setReaderHistory(req.session.userId, merged);
|
|
res.json(merged);
|
|
} catch (err) {
|
|
console.error('PUT /api/reader/history error:', err);
|
|
res.status(500).json({ error: 'Failed to save your reading history.' });
|
|
}
|
|
});
|
|
|
|
// PUT /api/reader/marks — replace the user's bookmarks + highlights ({ bookmarks, highlights })
|
|
app.put('/api/reader/marks', requireAuth, (req, res) => {
|
|
try {
|
|
const marks = cleanMarks(req.body);
|
|
if (!marks) return res.status(400).json({ error: 'Body must be an object.' });
|
|
res.json({ ok: true, updatedAt: setReaderMarks(req.session.userId, marks) });
|
|
} catch (err) {
|
|
console.error('PUT /api/reader/marks error:', err);
|
|
res.status(500).json({ error: 'Failed to save bookmarks and highlights.' });
|
|
}
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Reading plan progress — cross-device sync for started/completed reading plans
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// GET /api/reading-plans — the current user's whole progress list
|
|
|
|
app.get('/api/reading-plans', requireAuth, (req, res) => {
|
|
try {
|
|
res.json(getReadingProgress(req.session.userId));
|
|
} catch (err) {
|
|
console.error('GET /api/reading-plans error:', err);
|
|
res.status(500).json({ error: 'Failed to load reading plan progress.' });
|
|
}
|
|
});
|
|
|
|
// PUT /api/reading-plans — overwrite the current user's whole progress list
|
|
// (an object: { progress: [...], customPlans: [...] })
|
|
app.put('/api/reading-plans', requireAuth, (req, res) => {
|
|
try {
|
|
if (!req.body || typeof req.body !== 'object' || Array.isArray(req.body)) {
|
|
return res.status(400).json({ error: 'Body must be an object.' });
|
|
}
|
|
const { progress, customPlans, activity } = req.body;
|
|
if ([progress, customPlans, activity].some((v) => v !== undefined && !Array.isArray(v))
|
|
|| (progress?.length ?? 0) > 500 || (customPlans?.length ?? 0) > 200 || (activity?.length ?? 0) > 20000) {
|
|
return res.status(400).json({ error: 'progress, customPlans and activity must be reasonably sized lists.' });
|
|
}
|
|
setReadingProgress(req.session.userId, { progress: progress ?? [], customPlans: customPlans ?? [], activity: activity ?? [] });
|
|
res.json({ ok: true });
|
|
} catch (err) {
|
|
console.error('PUT /api/reading-plans error:', err);
|
|
res.status(500).json({ error: 'Failed to save reading plan progress.' });
|
|
}
|
|
});
|
|
}
|