fix: conform moved code to eslint indent/quotes, 4-arg mutation callbacks
- eslint --fix on branch-changed backend files (indent shift from the module-wrapper nesting in decomposed files); backend lint now 904 errors vs 1,315 on main - useMutationWithToast forwards all four TanStack v5 callback args (tsc -b strict build flagged the 3-arg passthrough)
This commit is contained in:
@@ -265,7 +265,7 @@ class SecureImageMiddleware {
|
|||||||
'X-Frame-Options': 'DENY',
|
'X-Frame-Options': 'DENY',
|
||||||
'X-XSS-Protection': '1; mode=block',
|
'X-XSS-Protection': '1; mode=block',
|
||||||
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
||||||
'Content-Security-Policy': "default-src 'none'; img-src 'self'",
|
'Content-Security-Policy': 'default-src \'none\'; img-src \'self\'',
|
||||||
|
|
||||||
// Custom security headers
|
// Custom security headers
|
||||||
'X-Protected-Content': 'true',
|
'X-Protected-Content': 'true',
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function secureStatic(basePath, options = {}) {
|
|||||||
// `default-src 'none'` already implies script-src 'none';
|
// `default-src 'none'` already implies script-src 'none';
|
||||||
// style-src + img-src(data:) keep normal SVG rendering working.
|
// style-src + img-src(data:) keep normal SVG rendering working.
|
||||||
if (/\.svg$/i.test(filePath)) {
|
if (/\.svg$/i.test(filePath)) {
|
||||||
resp.setHeader('Content-Security-Policy', "default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:");
|
resp.setHeader('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'; img-src \'self\' data:');
|
||||||
resp.setHeader('X-Content-Type-Options', 'nosniff');
|
resp.setHeader('X-Content-Type-Options', 'nosniff');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ const BULK_DELETE_MAX = 100;
|
|||||||
module.exports = (router) => {
|
module.exports = (router) => {
|
||||||
|
|
||||||
|
|
||||||
// Archive event
|
// Archive event
|
||||||
router.post('/:id/archive', adminAuth, requirePermission('events.archive'), requireEventOwnership, async (req, res) => {
|
router.post('/:id/archive', adminAuth, requirePermission('events.archive'), requireEventOwnership, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
@@ -60,13 +60,13 @@ router.post('/:id/archive', adminAuth, requirePermission('events.archive'), requ
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorResponse(res, error, 500, 'Failed to archive event');
|
errorResponse(res, error, 500, 'Failed to archive event');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bulk archive events
|
// Bulk archive events
|
||||||
router.post('/bulk-archive', adminAuth, requirePermission('events.archive'), [
|
router.post('/bulk-archive', adminAuth, requirePermission('events.archive'), [
|
||||||
body('eventIds').isArray().withMessage('eventIds must be an array'),
|
body('eventIds').isArray().withMessage('eventIds must be an array'),
|
||||||
body('eventIds.*').isInt().withMessage('Each eventId must be an integer')
|
body('eventIds.*').isInt().withMessage('Each eventId must be an integer')
|
||||||
], async (req, res) => {
|
], async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
@@ -138,11 +138,11 @@ router.post('/bulk-archive', adminAuth, requirePermission('events.archive'), [
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorResponse(res, error, 500, 'Failed to perform bulk archive');
|
errorResponse(res, error, 500, 'Failed to perform bulk archive');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
router.post('/bulk-delete', adminAuth, requirePermission('events.delete'), [
|
router.post('/bulk-delete', adminAuth, requirePermission('events.delete'), [
|
||||||
body('eventIds').isArray({ min: 1, max: BULK_DELETE_MAX }).withMessage(`eventIds must be an array of 1-${BULK_DELETE_MAX} ids`),
|
body('eventIds').isArray({ min: 1, max: BULK_DELETE_MAX }).withMessage(`eventIds must be an array of 1-${BULK_DELETE_MAX} ids`),
|
||||||
body('eventIds.*').isInt().withMessage('Each eventId must be an integer')
|
body('eventIds.*').isInt().withMessage('Each eventId must be an integer')
|
||||||
], async (req, res) => {
|
], async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
@@ -191,6 +191,6 @@ router.post('/bulk-delete', adminAuth, requirePermission('events.delete'), [
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorResponse(res, error, 500, 'Failed to perform bulk delete');
|
errorResponse(res, error, 500, 'Failed to perform bulk delete');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ const { validateHeroImageAnchor, getEventFieldRequirements, readBooleanSetting,
|
|||||||
module.exports = (router) => {
|
module.exports = (router) => {
|
||||||
|
|
||||||
|
|
||||||
// Create new event
|
// Create new event
|
||||||
router.post('/', adminAuth, requirePermission('events.create'), [
|
router.post('/', adminAuth, requirePermission('events.create'), [
|
||||||
body('event_type').notEmpty().trim().custom(async (value) => {
|
body('event_type').notEmpty().trim().custom(async (value) => {
|
||||||
const isValid = await eventTypeService.isValidEventType(value);
|
const isValid = await eventTypeService.isValidEventType(value);
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
@@ -123,7 +123,7 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
|||||||
// customer_accounts.id — many-to-many via event_customer_assignments.
|
// customer_accounts.id — many-to-many via event_customer_assignments.
|
||||||
body('customer_account_ids').optional().isArray(),
|
body('customer_account_ids').optional().isArray(),
|
||||||
body('customer_account_ids.*').optional().isInt({ min: 1 })
|
body('customer_account_ids.*').optional().isInt({ min: 1 })
|
||||||
], async (req, res) => {
|
], async (req, res) => {
|
||||||
try {
|
try {
|
||||||
logger.debug('Create event request body', { body: req.body });
|
logger.debug('Create event request body', { body: req.body });
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
@@ -612,10 +612,10 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorResponse(res, error, 500, 'Failed to create event');
|
errorResponse(res, error, 500, 'Failed to create event');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get all events with pagination and filters
|
// Get all events with pagination and filters
|
||||||
router.get('/', adminAuth, requirePermission('events.view'), async (req, res) => {
|
router.get('/', adminAuth, requirePermission('events.view'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const page = parseInt(req.query.page) || 1;
|
const page = parseInt(req.query.page) || 1;
|
||||||
const limit = parseInt(req.query.limit) || 20;
|
const limit = parseInt(req.query.limit) || 20;
|
||||||
@@ -710,10 +710,10 @@ router.get('/', adminAuth, requirePermission('events.view'), async (req, res) =>
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorResponse(res, error, 500, 'Failed to fetch events');
|
errorResponse(res, error, 500, 'Failed to fetch events');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get single event details
|
// Get single event details
|
||||||
router.get('/:id', adminAuth, requirePermission('events.view'), async (req, res) => {
|
router.get('/:id', adminAuth, requirePermission('events.view'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
@@ -792,10 +792,10 @@ router.get('/:id', adminAuth, requirePermission('events.view'), async (req, res)
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorResponse(res, error, 500, 'Failed to fetch event details');
|
errorResponse(res, error, 500, 'Failed to fetch event details');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Publish a draft event (set is_draft=false and queue creation email)
|
// Publish a draft event (set is_draft=false and queue creation email)
|
||||||
router.post('/:id/publish', adminAuth, requirePermission('events.edit'), requireEventOwnership, [
|
router.post('/:id/publish', adminAuth, requirePermission('events.edit'), requireEventOwnership, [
|
||||||
// Optional password the admin re-types in the publish dialog so the
|
// Optional password the admin re-types in the publish dialog so the
|
||||||
// gallery_created email can carry the actual plaintext (#627). When the
|
// gallery_created email can carry the actual plaintext (#627). When the
|
||||||
// event is password-protected and the body carries a password, picpeak
|
// event is password-protected and the body carries a password, picpeak
|
||||||
@@ -805,7 +805,7 @@ router.post('/:id/publish', adminAuth, requirePermission('events.edit'), require
|
|||||||
// compat with API-only consumers.
|
// compat with API-only consumers.
|
||||||
body('password').optional().isString().isLength({ min: 6 })
|
body('password').optional().isString().isLength({ min: 6 })
|
||||||
.withMessage('Password must be at least 6 characters long'),
|
.withMessage('Password must be at least 6 characters long'),
|
||||||
], async (req, res) => {
|
], async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
@@ -950,20 +950,20 @@ router.post('/:id/publish', adminAuth, requirePermission('events.edit'), require
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorResponse(res, error, 500, 'Failed to publish event');
|
errorResponse(res, error, 500, 'Failed to publish event');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Duplicate an event (#626). Creates a new DRAFT gallery that inherits the
|
// Duplicate an event (#626). Creates a new DRAFT gallery that inherits the
|
||||||
// source event's branding, behaviour, hero/header, feedback, and category
|
// source event's branding, behaviour, hero/header, feedback, and category
|
||||||
// configuration — admin then fills in customer + publishes via the publish
|
// configuration — admin then fills in customer + publishes via the publish
|
||||||
// dialog (#627), where the password is set. Photos, hero photo selection,
|
// dialog (#627), where the password is set. Photos, hero photo selection,
|
||||||
// client-access secrets, customer assignments, archive/sent state are NOT
|
// client-access secrets, customer assignments, archive/sent state are NOT
|
||||||
// carried over.
|
// carried over.
|
||||||
router.post('/:id/duplicate', adminAuth, requirePermission('events.create'), requireEventOwnership, [
|
router.post('/:id/duplicate', adminAuth, requirePermission('events.create'), requireEventOwnership, [
|
||||||
body('event_name').trim().notEmpty().withMessage('Event name is required'),
|
body('event_name').trim().notEmpty().withMessage('Event name is required'),
|
||||||
body('event_date').optional({ values: 'falsy' }).isDate(),
|
body('event_date').optional({ values: 'falsy' }).isDate(),
|
||||||
body('customer_name').optional().trim(),
|
body('customer_name').optional().trim(),
|
||||||
body('customer_email').optional({ values: 'falsy' }).isEmail().normalizeEmail(IDENTITY_PRESERVING_NORMALIZE_EMAIL),
|
body('customer_email').optional({ values: 'falsy' }).isEmail().normalizeEmail(IDENTITY_PRESERVING_NORMALIZE_EMAIL),
|
||||||
], async (req, res) => {
|
], async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
@@ -1136,10 +1136,10 @@ router.post('/:id/duplicate', adminAuth, requirePermission('events.create'), req
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorResponse(res, error, 500, 'Failed to duplicate event');
|
errorResponse(res, error, 500, 'Failed to duplicate event');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update event
|
// Update event
|
||||||
router.put('/:id', adminAuth, requirePermission('events.edit'), requireEventOwnership, [
|
router.put('/:id', adminAuth, requirePermission('events.edit'), requireEventOwnership, [
|
||||||
body('event_name').optional().trim().notEmpty(),
|
body('event_name').optional().trim().notEmpty(),
|
||||||
body('event_date').optional({ values: 'falsy' }).isDate(),
|
body('event_date').optional({ values: 'falsy' }).isDate(),
|
||||||
// Migration 137 — calendar time fields. Same regex/range rule as POST.
|
// Migration 137 — calendar time fields. Same regex/range rule as POST.
|
||||||
@@ -1239,7 +1239,7 @@ router.put('/:id', adminAuth, requirePermission('events.edit'), requireEventOwne
|
|||||||
// customer_accounts.id — many-to-many via event_customer_assignments.
|
// customer_accounts.id — many-to-many via event_customer_assignments.
|
||||||
body('customer_account_ids').optional().isArray(),
|
body('customer_account_ids').optional().isArray(),
|
||||||
body('customer_account_ids.*').optional().isInt({ min: 1 })
|
body('customer_account_ids.*').optional().isInt({ min: 1 })
|
||||||
], async (req, res) => {
|
], async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
@@ -1515,10 +1515,10 @@ router.put('/:id', adminAuth, requirePermission('events.edit'), requireEventOwne
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorResponse(res, error, 500, 'Failed to update event');
|
errorResponse(res, error, 500, 'Failed to update event');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete event
|
// Delete event
|
||||||
router.delete('/:id', adminAuth, requirePermission('events.delete'), requireEventOwnership, async (req, res) => {
|
router.delete('/:id', adminAuth, requirePermission('events.delete'), requireEventOwnership, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
await deleteEventCascade(id, { id: req.admin.id, username: req.admin.username });
|
await deleteEventCascade(id, { id: req.admin.id, username: req.admin.username });
|
||||||
@@ -1535,10 +1535,10 @@ router.delete('/:id', adminAuth, requirePermission('events.delete'), requireEven
|
|||||||
}
|
}
|
||||||
res.status(500).json({ error: 'Failed to delete event' });
|
res.status(500).json({ error: 'Failed to delete event' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toggle event status
|
// Toggle event status
|
||||||
router.post('/:id/toggle-status', adminAuth, requirePermission('events.edit'), requireEventOwnership, async (req, res) => {
|
router.post('/:id/toggle-status', adminAuth, requirePermission('events.edit'), requireEventOwnership, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
@@ -1574,6 +1574,6 @@ router.post('/:id/toggle-status', adminAuth, requirePermission('events.edit'), r
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorResponse(res, error, 500, 'Failed to toggle event status');
|
errorResponse(res, error, 500, 'Failed to toggle event status');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ const eventLogoUpload = multer({
|
|||||||
module.exports = (router) => {
|
module.exports = (router) => {
|
||||||
|
|
||||||
|
|
||||||
// Upload event custom logo
|
// Upload event custom logo
|
||||||
router.post('/:id/logo', adminAuth, requirePermission('events.edit'), requireEventOwnership, eventLogoUpload.single('logo'), async (req, res) => {
|
router.post('/:id/logo', adminAuth, requirePermission('events.edit'), requireEventOwnership, eventLogoUpload.single('logo'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
@@ -96,10 +96,10 @@ router.post('/:id/logo', adminAuth, requirePermission('events.edit'), requireEve
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorResponse(res, error, 500, 'Failed to upload event logo');
|
errorResponse(res, error, 500, 'Failed to upload event logo');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete event custom logo
|
// Delete event custom logo
|
||||||
router.delete('/:id/logo', adminAuth, requirePermission('events.edit'), requireEventOwnership, async (req, res) => {
|
router.delete('/:id/logo', adminAuth, requirePermission('events.edit'), requireEventOwnership, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ router.delete('/:id/logo', adminAuth, requirePermission('events.edit'), requireE
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorResponse(res, error, 500, 'Failed to delete event logo');
|
errorResponse(res, error, 500, 'Failed to delete event logo');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ const { requireEventOwnership } = require('../../middleware/ownership');
|
|||||||
module.exports = (router) => {
|
module.exports = (router) => {
|
||||||
|
|
||||||
|
|
||||||
// Reset event password
|
// Reset event password
|
||||||
router.post('/:id/reset-password', adminAuth, requirePermission('events.edit'), requireEventOwnership, async (req, res) => {
|
router.post('/:id/reset-password', adminAuth, requirePermission('events.edit'), requireEventOwnership, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { sendEmail = true, password: clientPassword } = req.body;
|
const { sendEmail = true, password: clientPassword } = req.body;
|
||||||
@@ -102,10 +102,10 @@ router.post('/:id/reset-password', adminAuth, requirePermission('events.edit'),
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorResponse(res, error, 500, 'Failed to reset password');
|
errorResponse(res, error, 500, 'Failed to reset password');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Resend creation email
|
// Resend creation email
|
||||||
router.post('/:id/resend-email', adminAuth, requirePermission('events.edit'), requireEventOwnership, async (req, res) => {
|
router.post('/:id/resend-email', adminAuth, requirePermission('events.edit'), requireEventOwnership, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
@@ -188,6 +188,6 @@ router.post('/:id/resend-email', adminAuth, requirePermission('events.edit'), re
|
|||||||
logger.error('Error resending creation email:', error);
|
logger.error('Error resending creation email:', error);
|
||||||
errorResponse(res, error, 500, 'Failed to resend creation email');
|
errorResponse(res, error, 500, 'Failed to resend creation email');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,10 +40,10 @@ async function loadOwnedEvent(req) {
|
|||||||
module.exports = (router) => {
|
module.exports = (router) => {
|
||||||
|
|
||||||
|
|
||||||
// Generate (or rotate) the slideshow share token. Idempotent in intent: each
|
// Generate (or rotate) the slideshow share token. Idempotent in intent: each
|
||||||
// call mints a fresh token, which both "Generate" (first time) and "Regenerate"
|
// call mints a fresh token, which both "Generate" (first time) and "Regenerate"
|
||||||
// (rotate, kills the old link) use.
|
// (rotate, kills the old link) use.
|
||||||
router.post('/:id/slideshow/generate', adminAuth, requirePermission('events.edit'), requireFeatureFlag('slideshow'), requireEventOwnership, async (req, res) => {
|
router.post('/:id/slideshow/generate', adminAuth, requirePermission('events.edit'), requireFeatureFlag('slideshow'), requireEventOwnership, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const event = await loadOwnedEvent(req);
|
const event = await loadOwnedEvent(req);
|
||||||
if (!event) {
|
if (!event) {
|
||||||
@@ -70,11 +70,11 @@ router.post('/:id/slideshow/generate', adminAuth, requirePermission('events.edit
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorResponse(res, error, 500, 'Failed to generate slideshow link');
|
errorResponse(res, error, 500, 'Failed to generate slideshow link');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Disable the slideshow link (null the token). The public /show/ route dies on
|
// Disable the slideshow link (null the token). The public /show/ route dies on
|
||||||
// its next poll, killing any projector currently pointed at the old link.
|
// its next poll, killing any projector currently pointed at the old link.
|
||||||
router.post('/:id/slideshow/disable', adminAuth, requirePermission('events.edit'), requireEventOwnership, async (req, res) => {
|
router.post('/:id/slideshow/disable', adminAuth, requirePermission('events.edit'), requireEventOwnership, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const event = await loadOwnedEvent(req);
|
const event = await loadOwnedEvent(req);
|
||||||
if (!event) {
|
if (!event) {
|
||||||
@@ -95,18 +95,18 @@ router.post('/:id/slideshow/disable', adminAuth, requirePermission('events.edit'
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorResponse(res, error, 500, 'Failed to disable slideshow link');
|
errorResponse(res, error, 500, 'Failed to disable slideshow link');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update the LIVE slideshow settings (display time / transition style / speed).
|
// Update the LIVE slideshow settings (display time / transition style / speed).
|
||||||
// A running projector picks these up via the show-page settings poll within a
|
// A running projector picks these up via the show-page settings poll within a
|
||||||
// few seconds — no need to regenerate the link.
|
// few seconds — no need to regenerate the link.
|
||||||
router.patch('/:id/slideshow', adminAuth, requirePermission('events.edit'), requireFeatureFlag('slideshow'), requireEventOwnership, [
|
router.patch('/:id/slideshow', adminAuth, requirePermission('events.edit'), requireFeatureFlag('slideshow'), requireEventOwnership, [
|
||||||
body('show_interval_ms').optional().isInt({ min: 1000, max: 120000 }),
|
body('show_interval_ms').optional().isInt({ min: 1000, max: 120000 }),
|
||||||
body('show_transition').optional().isIn(SLIDESHOW_TRANSITIONS),
|
body('show_transition').optional().isIn(SLIDESHOW_TRANSITIONS),
|
||||||
body('show_transition_ms').optional().isInt({ min: 100, max: 5000 }),
|
body('show_transition_ms').optional().isInt({ min: 100, max: 5000 }),
|
||||||
body('show_watermark').optional({ nullable: true }),
|
body('show_watermark').optional({ nullable: true }),
|
||||||
body('show_colorfilter').optional().isIn(SLIDESHOW_COLORFILTERS)
|
body('show_colorfilter').optional().isIn(SLIDESHOW_COLORFILTERS)
|
||||||
], async (req, res) => {
|
], async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
@@ -146,6 +146,6 @@ router.patch('/:id/slideshow', adminAuth, requirePermission('events.edit'), requ
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorResponse(res, error, 500, 'Failed to update slideshow settings');
|
errorResponse(res, error, 500, 'Failed to update slideshow settings');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -74,10 +74,10 @@ router.get(
|
|||||||
'gallery_guests.created_at',
|
'gallery_guests.created_at',
|
||||||
'gallery_guests.last_seen_at',
|
'gallery_guests.last_seen_at',
|
||||||
'gallery_guests.email_verified_at',
|
'gallery_guests.email_verified_at',
|
||||||
db.raw("COUNT(CASE WHEN photo_feedback.feedback_type = 'like' THEN 1 END) AS likes"),
|
db.raw('COUNT(CASE WHEN photo_feedback.feedback_type = \'like\' THEN 1 END) AS likes'),
|
||||||
db.raw("COUNT(CASE WHEN photo_feedback.feedback_type = 'favorite' THEN 1 END) AS favorites"),
|
db.raw('COUNT(CASE WHEN photo_feedback.feedback_type = \'favorite\' THEN 1 END) AS favorites'),
|
||||||
db.raw("COUNT(CASE WHEN photo_feedback.feedback_type = 'comment' THEN 1 END) AS comments"),
|
db.raw('COUNT(CASE WHEN photo_feedback.feedback_type = \'comment\' THEN 1 END) AS comments'),
|
||||||
db.raw("COUNT(CASE WHEN photo_feedback.feedback_type = 'rating' THEN 1 END) AS ratings"),
|
db.raw('COUNT(CASE WHEN photo_feedback.feedback_type = \'rating\' THEN 1 END) AS ratings'),
|
||||||
db.raw('COUNT(DISTINCT photo_feedback.photo_id) AS distinct_photos')
|
db.raw('COUNT(DISTINCT photo_feedback.photo_id) AS distinct_photos')
|
||||||
)
|
)
|
||||||
.orderBy('gallery_guests.created_at', 'desc');
|
.orderBy('gallery_guests.created_at', 'desc');
|
||||||
@@ -117,7 +117,7 @@ router.get(
|
|||||||
const photos = await db('photos')
|
const photos = await db('photos')
|
||||||
.leftJoin('photo_feedback', function () {
|
.leftJoin('photo_feedback', function () {
|
||||||
this.on('photo_feedback.photo_id', '=', 'photos.id')
|
this.on('photo_feedback.photo_id', '=', 'photos.id')
|
||||||
.andOn(db.raw("photo_feedback.feedback_type IN ('like','favorite')"))
|
.andOn(db.raw('photo_feedback.feedback_type IN (\'like\',\'favorite\')'))
|
||||||
.andOnNotNull('photo_feedback.guest_id');
|
.andOnNotNull('photo_feedback.guest_id');
|
||||||
})
|
})
|
||||||
.where('photos.event_id', eventId)
|
.where('photos.event_id', eventId)
|
||||||
|
|||||||
@@ -1251,7 +1251,7 @@ router.post('/:eventId/chunked-upload/init', adminAuth, requirePermission('photo
|
|||||||
// Validate file size (max 10GB)
|
// Validate file size (max 10GB)
|
||||||
const maxSize = 10 * 1024 * 1024 * 1024;
|
const maxSize = 10 * 1024 * 1024 * 1024;
|
||||||
if (fileSize > maxSize) {
|
if (fileSize > maxSize) {
|
||||||
return res.status(400).json({ error: `File too large. Maximum size is 10GB.` });
|
return res.status(400).json({ error: 'File too large. Maximum size is 10GB.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await chunkedUpload.initializeUpload({
|
const result = await chunkedUpload.initializeUpload({
|
||||||
|
|||||||
@@ -308,7 +308,7 @@ class RestoreService {
|
|||||||
this.log('info', 'Post-restore migrations applied');
|
this.log('info', 'Post-restore migrations applied');
|
||||||
} catch (migErr) {
|
} catch (migErr) {
|
||||||
this.log('warn',
|
this.log('warn',
|
||||||
`Post-restore migrate:safe failed — restore data is in place but the schema may lag the running image. ` +
|
'Post-restore migrate:safe failed — restore data is in place but the schema may lag the running image. ' +
|
||||||
`A container restart will retry via wait-for-db.sh. Error: ${migErr.message}`);
|
`A container restart will retry via wait-for-db.sh. Error: ${migErr.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -888,9 +888,9 @@ class RestoreService {
|
|||||||
throw new Error(
|
throw new Error(
|
||||||
`Database backup file not found. Tried: ${candidates.join(', ')}. ` +
|
`Database backup file not found. Tried: ${candidates.join(', ')}. ` +
|
||||||
`Manifest recorded path: ${dbBackupFile}. ` +
|
`Manifest recorded path: ${dbBackupFile}. ` +
|
||||||
`Hint: this usually means the manifest's database.backup_file path no longer ` +
|
'Hint: this usually means the manifest\'s database.backup_file path no longer ' +
|
||||||
`exists on disk (deleted? moved? volume not mounted?). Check ` +
|
'exists on disk (deleted? moved? volume not mounted?). Check ' +
|
||||||
`~/<your-compose-dir>/backup/database/ on the host.`
|
'~/<your-compose-dir>/backup/database/ on the host.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1043,8 +1043,8 @@ class RestoreService {
|
|||||||
await spawnAsync('psql', [
|
await spawnAsync('psql', [
|
||||||
'-h', host, '-p', String(port), '-U', user, '-d', maintenanceDb,
|
'-h', host, '-p', String(port), '-U', user, '-d', maintenanceDb,
|
||||||
'-c',
|
'-c',
|
||||||
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity ` +
|
'SELECT pg_terminate_backend(pid) FROM pg_stat_activity ' +
|
||||||
`WHERE datname = '${database.replace(/'/g, "''")}' AND pid <> pg_backend_pid()`,
|
`WHERE datname = '${database.replace(/'/g, '\'\'')}' AND pid <> pg_backend_pid()`,
|
||||||
], { env });
|
], { env });
|
||||||
|
|
||||||
// Drop and recreate database (extremely dangerous!)
|
// Drop and recreate database (extremely dangerous!)
|
||||||
|
|||||||
@@ -638,7 +638,7 @@ class S3StorageAdapter extends stream.EventEmitter {
|
|||||||
UploadId: uploadId
|
UploadId: uploadId
|
||||||
}));
|
}));
|
||||||
} catch (abortError) {
|
} catch (abortError) {
|
||||||
logger.error(`Failed to abort multipart upload:`, abortError);
|
logger.error('Failed to abort multipart upload:', abortError);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export function useMutationWithToast<
|
|||||||
|
|
||||||
return useMutation<TData, TError, TVariables, TContext>({
|
return useMutation<TData, TError, TVariables, TContext>({
|
||||||
...mutationOptions,
|
...mutationOptions,
|
||||||
onSuccess: (data, variables, context) => {
|
onSuccess: (data, variables, onMutateResult, context) => {
|
||||||
invalidateKeys?.forEach((queryKey) => {
|
invalidateKeys?.forEach((queryKey) => {
|
||||||
queryClient.invalidateQueries({ queryKey });
|
queryClient.invalidateQueries({ queryKey });
|
||||||
});
|
});
|
||||||
@@ -56,9 +56,9 @@ export function useMutationWithToast<
|
|||||||
typeof successMessage === 'function' ? successMessage(data, variables) : successMessage
|
typeof successMessage === 'function' ? successMessage(data, variables) : successMessage
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
onSuccess?.(data, variables, context);
|
onSuccess?.(data, variables, onMutateResult, context);
|
||||||
},
|
},
|
||||||
onError: (error, variables, context) => {
|
onError: (error, variables, onMutateResult, context) => {
|
||||||
const message =
|
const message =
|
||||||
typeof errorMessage === 'function'
|
typeof errorMessage === 'function'
|
||||||
? errorMessage(error)
|
? errorMessage(error)
|
||||||
@@ -67,7 +67,7 @@ export function useMutationWithToast<
|
|||||||
(error instanceof Error ? error.message : undefined) ||
|
(error instanceof Error ? error.message : undefined) ||
|
||||||
'An unexpected error occurred';
|
'An unexpected error occurred';
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
onError?.(error, variables, context);
|
onError?.(error, variables, onMutateResult, context);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user