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>
105 lines
5.1 KiB
JavaScript
105 lines
5.1 KiB
JavaScript
// Studies: list, load, save (with conflict checks), earlier versions, delete.
|
|
import { canEditProject, deleteProject, getAllProjects, getProject, getProjectEdited, getVersion, listVersions, restoreVersion, upsertProject } from '../db.js';
|
|
import { requireAuth } from '../auth.js';
|
|
import { isSaveConflict, parseBase } from '../projectSync.js';
|
|
|
|
export function registerProjectRoutes(app) {
|
|
// GET /api/projects — list all project summaries owned by the current user
|
|
// ---------------------------------------------------------------------------
|
|
app.get('/api/projects', requireAuth, (req, res) => {
|
|
try {
|
|
const projects = getAllProjects(req.session.userId);
|
|
res.json(projects);
|
|
} catch (err) {
|
|
console.error('GET /api/projects error:', err);
|
|
res.status(500).json({ error: 'Failed to list projects.' });
|
|
}
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GET /api/projects/:id — fetch a single full project owned by the current user
|
|
// ---------------------------------------------------------------------------
|
|
app.get('/api/projects/:id', requireAuth, (req, res) => {
|
|
try {
|
|
const project = getProject(req.params.id, req.session.userId);
|
|
if (!project) return res.status(404).json({ error: 'Project not found.' });
|
|
res.json(project);
|
|
} catch (err) {
|
|
console.error('GET /api/projects/:id error:', err);
|
|
res.status(500).json({ error: 'Failed to load project.' });
|
|
}
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// PUT /api/projects/:id — create or update a project owned by the current user
|
|
// ---------------------------------------------------------------------------
|
|
app.put('/api/projects/:id', requireAuth, (req, res) => {
|
|
try {
|
|
const body = req.body;
|
|
if (!body || typeof body !== 'object') {
|
|
return res.status(400).json({ error: 'Invalid JSON body.' });
|
|
}
|
|
if (!body.id || !body.title || !Array.isArray(body.chapters)) {
|
|
return res.status(400).json({ error: 'Missing required fields: id, title, chapters.' });
|
|
}
|
|
if (body.id !== req.params.id) {
|
|
return res.status(400).json({ error: 'URL id does not match body id.' });
|
|
}
|
|
if (body.id.length > 100) return res.status(400).json({ error: 'Invalid project id.' });
|
|
if (body.title.length > 500) return res.status(400).json({ error: 'Project title is too long.' });
|
|
// Refuse to overwrite a copy another device saved after the one this device last saw
|
|
const stored = getProjectEdited(body.id, req.session.userId);
|
|
if (isSaveConflict({ stored, base: parseBase(req.get('X-Base-Edited')), force: req.query.force === '1' })) {
|
|
return res.status(409).json({ error: 'This study was changed on another device.', code: 'conflict', serverEdited: stored });
|
|
}
|
|
const saved = upsertProject(body, req.session.userId);
|
|
if (!saved) {
|
|
return res.status(403).json({ error: 'That project belongs to a different account.' });
|
|
}
|
|
res.json(saved);
|
|
} catch (err) {
|
|
console.error('PUT /api/projects/:id error:', err);
|
|
res.status(500).json({ error: 'Failed to save project.' });
|
|
}
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Earlier versions of a study: list, look at one, and put one back (the copy it replaces is kept too)
|
|
// ---------------------------------------------------------------------------
|
|
app.get('/api/projects/:id/versions', requireAuth, (req, res) => {
|
|
if (!canEditProject(req.params.id, req.session.userId)) return res.status(404).json({ error: 'Project not found.' });
|
|
res.json({ versions: listVersions(req.params.id) });
|
|
});
|
|
|
|
app.get('/api/projects/:id/versions/:versionId', requireAuth, (req, res) => {
|
|
if (!canEditProject(req.params.id, req.session.userId)) return res.status(404).json({ error: 'Project not found.' });
|
|
const version = getVersion(req.params.id, Number(req.params.versionId));
|
|
if (!version) return res.status(404).json({ error: 'That version is gone.' });
|
|
res.json(version);
|
|
});
|
|
|
|
app.post('/api/projects/:id/versions/:versionId/restore', requireAuth, (req, res) => {
|
|
try {
|
|
const restored = restoreVersion(req.params.id, Number(req.params.versionId), req.session.userId);
|
|
if (!restored) return res.status(404).json({ error: 'That version is gone.' });
|
|
res.json(restored);
|
|
} catch (err) {
|
|
console.error('POST restore error:', err);
|
|
res.status(500).json({ error: 'Failed to restore that version.' });
|
|
}
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// DELETE /api/projects/:id — remove a project owned by the current user
|
|
// ---------------------------------------------------------------------------
|
|
app.delete('/api/projects/:id', requireAuth, (req, res) => {
|
|
try {
|
|
deleteProject(req.params.id, req.session.userId);
|
|
res.json({ ok: true });
|
|
} catch (err) {
|
|
console.error('DELETE /api/projects/:id error:', err);
|
|
res.status(500).json({ error: 'Failed to delete project.' });
|
|
}
|
|
});
|
|
}
|