Add curator roles/permissions with Users admin, and fix lineage branch joins.

Staff accounts use admin/curator roles and fine-grained flags; transitions connect source-to-target with color gradients and stream cutout masks so overlaps stay seamless.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila Khodjaef
2026-07-27 18:04:17 +03:00
co-authored by Cursor
parent bfa21989c9
commit 0466b77328
31 changed files with 1492 additions and 269 deletions
+19 -17
View File
@@ -8,9 +8,10 @@ require('dotenv').config();
const pool = require('./db');
const { createSessionMiddleware } = require('./middleware/session');
const { requireCurator } = require('./middleware/auth');
const { requirePermission } = require('./middleware/auth');
const { logCuratorAction } = require('./audit-log');
const authRoutes = require('./routes/auth');
const usersRoutes = require('./routes/users');
const { ensurePaintingImages, preloadArtistImagesLocal, replacePaintingImageFromUrl, replaceArtistPortraitFromUrl, clearPaintingImage, deletePainting, clearArtistPortrait, replacePaintingImageFromBuffer, replaceArtistPortraitFromBuffer, enrichPaintingRow, enrichArtistRow, IMAGE_DIR } = require('./image-service');
const { getVersionInfo } = require('./version-info');
const { searchCatalog } = require('./search-service');
@@ -44,6 +45,7 @@ app.use(compression());
app.use(express.json({ limit: '20mb' }));
app.use(createSessionMiddleware());
app.use('/api/auth', authRoutes);
app.use('/api/users', usersRoutes);
app.use('/api/translations', translationRoutes);
app.use('/api/influences', influenceRoutes);
app.use('/api/tours', tourRoutes);
@@ -469,7 +471,7 @@ app.get('/api/artists/:id/navigation', async (req, res) => {
});
// Update artist portrait checkup flags (checked / fixed)
app.patch('/api/artists/:id/checkup-flags', requireCurator, async (req, res) => {
app.patch('/api/artists/:id/checkup-flags', requirePermission('checkup'), async (req, res) => {
try {
const artistId = parseInt(req.params.id, 10);
const { checked, fixed } = req.body ?? {};
@@ -537,7 +539,7 @@ app.patch('/api/artists/:id/checkup-flags', requireCurator, async (req, res) =>
});
// Developer debug: portrait image search for artist bio
app.get('/api/artists/:id/debug-portrait-search/more', requireCurator, async (req, res) => {
app.get('/api/artists/:id/debug-portrait-search/more', requirePermission('images'), async (req, res) => {
try {
const artistId = parseInt(req.params.id, 10);
const limit = Math.min(20, Math.max(1, parseInt(req.query.limit, 10) || 20));
@@ -555,7 +557,7 @@ app.get('/api/artists/:id/debug-portrait-search/more', requireCurator, async (re
}
});
app.get('/api/artists/:id/debug-portrait-search', requireCurator, async (req, res) => {
app.get('/api/artists/:id/debug-portrait-search', requirePermission('images'), async (req, res) => {
try {
const artistId = parseInt(req.params.id, 10);
const result = await pool.query(`SELECT name FROM artists WHERE id = $1`, [artistId]);
@@ -573,7 +575,7 @@ app.get('/api/artists/:id/debug-portrait-search', requireCurator, async (req, re
});
// Developer debug: replace artist portrait with a search result URL
app.post('/api/artists/:id/fix-portrait', requireCurator, async (req, res) => {
app.post('/api/artists/:id/fix-portrait', requirePermission('images'), async (req, res) => {
try {
const artistId = parseInt(req.params.id, 10);
const { imageUrl, searchUrl, source, pageUrl, thumbUrl } = req.body ?? {};
@@ -607,7 +609,7 @@ app.post('/api/artists/:id/fix-portrait', requireCurator, async (req, res) => {
}
});
app.post('/api/artists/:id/clear-portrait', requireCurator, async (req, res) => {
app.post('/api/artists/:id/clear-portrait', requirePermission('images'), async (req, res) => {
try {
const artistId = parseInt(req.params.id, 10);
const updated = await clearArtistPortrait(artistId);
@@ -630,7 +632,7 @@ app.post('/api/artists/:id/clear-portrait', requireCurator, async (req, res) =>
}
});
app.post('/api/artists/:id/upload-portrait', requireCurator, async (req, res) => {
app.post('/api/artists/:id/upload-portrait', requirePermission('images'), async (req, res) => {
try {
const artistId = parseInt(req.params.id, 10);
const { imageData, mimeType } = req.body ?? {};
@@ -720,7 +722,7 @@ app.get('/api/artists/:id', async (req, res) => {
});
// Painting image checkup (developer audit table) — must be before /api/paintings/:id
app.get('/api/paintings/checkup', requireCurator, async (_req, res) => {
app.get('/api/paintings/checkup', requirePermission('checkup'), async (_req, res) => {
try {
const { rows } = await pool.query(
`SELECT p.id, p.title, p.year, p.image_path, p.thumbnail_path,
@@ -768,7 +770,7 @@ app.get('/api/paintings/checkup', requireCurator, async (_req, res) => {
});
// Update checkup workflow flags (checked / fixed)
app.patch('/api/paintings/:id/checkup-flags', requireCurator, async (req, res) => {
app.patch('/api/paintings/:id/checkup-flags', requirePermission('checkup'), async (req, res) => {
try {
const paintingId = parseInt(req.params.id, 10);
const { checked, fixed } = req.body ?? {};
@@ -837,7 +839,7 @@ app.patch('/api/paintings/:id/checkup-flags', requireCurator, async (req, res) =
});
// Update public curator notes on a painting
app.patch('/api/paintings/:id/curator-notes', requireCurator, async (req, res) => {
app.patch('/api/paintings/:id/curator-notes', requirePermission('curator_notes'), async (req, res) => {
try {
const paintingId = parseInt(req.params.id, 10);
if (!Number.isFinite(paintingId)) {
@@ -941,7 +943,7 @@ app.post('/api/artists/:id/preload-images', async (req, res) => {
});
// Developer debug: Google Images first result for image audit
app.get('/api/paintings/:id/debug-image-search/more', requireCurator, async (req, res) => {
app.get('/api/paintings/:id/debug-image-search/more', requirePermission('images'), async (req, res) => {
try {
const paintingId = parseInt(req.params.id, 10);
const limit = Math.min(20, Math.max(1, parseInt(req.query.limit, 10) || 20));
@@ -965,7 +967,7 @@ app.get('/api/paintings/:id/debug-image-search/more', requireCurator, async (req
}
});
app.get('/api/paintings/:id/debug-image-search', requireCurator, async (req, res) => {
app.get('/api/paintings/:id/debug-image-search', requirePermission('images'), async (req, res) => {
try {
const paintingId = parseInt(req.params.id, 10);
const result = await pool.query(
@@ -989,7 +991,7 @@ app.get('/api/paintings/:id/debug-image-search', requireCurator, async (req, res
});
// Developer debug: replace painting image with a search result URL
app.post('/api/paintings/:id/fix-image', requireCurator, async (req, res) => {
app.post('/api/paintings/:id/fix-image', requirePermission('images'), async (req, res) => {
try {
const paintingId = parseInt(req.params.id, 10);
const { imageUrl, searchUrl, source, pageUrl, thumbUrl } = req.body ?? {};
@@ -1023,7 +1025,7 @@ app.post('/api/paintings/:id/fix-image', requireCurator, async (req, res) => {
}
});
app.delete('/api/paintings/:id', requireCurator, async (req, res) => {
app.delete('/api/paintings/:id', requirePermission('images'), async (req, res) => {
try {
const paintingId = parseInt(req.params.id, 10);
if (!Number.isFinite(paintingId)) {
@@ -1047,7 +1049,7 @@ app.delete('/api/paintings/:id', requireCurator, async (req, res) => {
}
});
app.post('/api/paintings/:id/clear-image', requireCurator, async (req, res) => {
app.post('/api/paintings/:id/clear-image', requirePermission('images'), async (req, res) => {
try {
const paintingId = parseInt(req.params.id, 10);
const updated = await clearPaintingImage(paintingId);
@@ -1070,7 +1072,7 @@ app.post('/api/paintings/:id/clear-image', requireCurator, async (req, res) => {
}
});
app.post('/api/paintings/:id/upload-image', requireCurator, async (req, res) => {
app.post('/api/paintings/:id/upload-image', requirePermission('images'), async (req, res) => {
try {
const paintingId = parseInt(req.params.id, 10);
const { imageData, mimeType } = req.body ?? {};
@@ -1111,7 +1113,7 @@ app.post('/api/paintings/:id/upload-image', requireCurator, async (req, res) =>
});
// Proxy remote image for debug preview (avoids hotlink / CORS blocks)
app.get('/api/debug/image-proxy', requireCurator, async (req, res) => {
app.get('/api/debug/image-proxy', requirePermission('images'), async (req, res) => {
try {
const imageUrl = req.query.url;
const searchUrl = req.query.searchUrl;