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
+8 -11
View File
@@ -1,6 +1,6 @@
const express = require('express');
const pool = require('../db');
const { requireCurator } = require('../middleware/auth');
const { requirePermission, loadStaffUser } = require('../middleware/auth');
const { logCuratorAction } = require('../audit-log');
const { enrichPaintingRow } = require('../image-service');
const { localizePaintings, resolveLocale, translationStatuses } = require('../translation-service');
@@ -87,7 +87,7 @@ router.get('/', async (req, res) => {
}
});
router.get('/admin', requireCurator, async (_req, res) => {
router.get('/admin', requirePermission('tours'), async (_req, res) => {
try {
const { rows } = await pool.query(
`SELECT t.*,
@@ -125,11 +125,8 @@ router.get('/:id', async (req, res) => {
const tour = rows[0];
let isCurator = false;
if (req.session?.userId) {
const { rows: users } = await pool.query(
'SELECT id FROM users WHERE id = $1',
[req.session.userId],
);
isCurator = users.length > 0;
const user = await loadStaffUser(req.session.userId);
isCurator = Boolean(user && user.is_active);
}
if (tour.status !== 'published' && !isCurator) {
return res.status(404).json({ error: 'Tour not found' });
@@ -150,7 +147,7 @@ router.get('/:id', async (req, res) => {
}
});
router.post('/', requireCurator, async (req, res) => {
router.post('/', requirePermission('tours'), async (req, res) => {
try {
const title = typeof req.body?.title === 'string' ? req.body.title.trim() : '';
if (!title) return res.status(400).json({ error: 'title required' });
@@ -180,7 +177,7 @@ router.post('/', requireCurator, async (req, res) => {
}
});
router.patch('/:id', requireCurator, async (req, res) => {
router.patch('/:id', requirePermission('tours'), async (req, res) => {
try {
const id = parseId(req.params.id);
if (!id) return res.status(400).json({ error: 'Invalid id' });
@@ -236,7 +233,7 @@ router.patch('/:id', requireCurator, async (req, res) => {
}
});
router.delete('/:id', requireCurator, async (req, res) => {
router.delete('/:id', requirePermission('tours'), async (req, res) => {
try {
const id = parseId(req.params.id);
if (!id) return res.status(400).json({ error: 'Invalid id' });
@@ -260,7 +257,7 @@ router.delete('/:id', requireCurator, async (req, res) => {
}
});
router.put('/:id/stops', requireCurator, async (req, res) => {
router.put('/:id/stops', requirePermission('tours'), async (req, res) => {
const client = await pool.connect();
try {
const id = parseId(req.params.id);