const express = require('express'); const router = express.Router(); const groupRepository = require('../repositories/GroupRepository'); const deletionLogRepository = require('../repositories/DeletionLogRepository'); const dbManager = require('../database/DatabaseManager'); const { rateLimitMiddleware, recordFailedTokenValidation } = require('../middlewares/rateLimiter'); const auditLogMiddleware = require('../middlewares/auditLog'); const TelegramNotificationService = require('../services/TelegramNotificationService'); // Singleton-Instanz des Telegram Service const telegramService = new TelegramNotificationService(); // Apply middleware to all management routes router.use(rateLimitMiddleware); router.use(auditLogMiddleware); // Helper: Validate UUID v4 token format const validateToken = (token) => { const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; return uuidRegex.test(token); }; /** * GET /api/manage/:token * Validate management token and load complete group data with images and consents * * @returns {Object} Complete group data including metadata, images, consents * @throws {404} Token invalid or not found * @throws {500} Server error */ router.get('/:token', async (req, res) => { /* #swagger.tags = ['Management Portal'] #swagger.summary = 'Validate token and load group data' #swagger.description = 'Validates management token and returns complete group data with images and consents' #swagger.parameters['token'] = { in: 'path', required: true, type: 'string', description: 'Management token (UUID v4)', example: '550e8400-e29b-41d4-a716-446655440000' } #swagger.responses[200] = { description: 'Group data loaded successfully', schema: { success: true, data: { groupId: 'abc123', groupName: 'Familie_Mueller', managementToken: '550e8400-e29b-41d4-a716-446655440000', images: [], socialMediaConsents: [], display_in_workshop: true } } } #swagger.responses[404] = { description: 'Invalid token or group deleted' } */ try { const { token } = req.params; // Validate token format if (!validateToken(token)) { recordFailedTokenValidation(req); // Track brute-force attempts return res.status(404).json({ success: false, error: 'Invalid management token format' }); } // Load group by management token const groupData = await groupRepository.getGroupByManagementToken(token); if (!groupData) { recordFailedTokenValidation(req); // Track brute-force attempts await res.auditLog('validate_token', false, null, 'Token not found or group deleted'); return res.status(404).json({ success: false, error: 'Management token not found or group has been deleted' }); } // Log successful token validation await res.auditLog('validate_token', true, groupData.groupId); // Return complete group data res.json({ success: true, data: groupData }); } catch (error) { console.error('Error validating management token:', error); await res.auditLog('validate_token', false, null, error.message); res.status(500).json({ success: false, error: 'Failed to validate management token' }); } }); /** * PUT /api/manage/:token/consents * Revoke or restore individual consents (workshop OR social media platforms) * * Body: * - consentType: 'workshop' | 'social_media' * - action: 'revoke' | 'restore' * - platformId: number (only for social_media) * * @returns {Object} Updated consent status * @throws {400} Invalid request * @throws {404} Token invalid or not found * @throws {500} Server error */ router.put('/:token/consents', async (req, res) => { /* #swagger.tags = ['Management Portal'] #swagger.summary = 'Revoke or restore consents' #swagger.description = 'Updates workshop or social media consents for a group' #swagger.parameters['token'] = { in: 'path', required: true, type: 'string', description: 'Management token (UUID v4)', example: '550e8400-e29b-41d4-a716-446655440000' } #swagger.parameters['body'] = { in: 'body', required: true, schema: { consentType: 'workshop', action: 'revoke', platformId: 1 } } #swagger.responses[200] = { description: 'Consent updated successfully', schema: { success: true, message: 'Workshop consent revoked successfully', data: { consentType: 'workshop', newValue: false } } } #swagger.responses[400] = { description: 'Invalid request parameters' } #swagger.responses[404] = { description: 'Invalid token' } */ try { const { token } = req.params; const { consentType, action, platformId } = req.body; // Validate token format if (!validateToken(token)) { return res.status(404).json({ success: false, error: 'Invalid management token format' }); } // Validate request body if (!consentType || !action) { return res.status(400).json({ success: false, error: 'Missing required fields: consentType and action' }); } if (!['workshop', 'social_media'].includes(consentType)) { return res.status(400).json({ success: false, error: 'Invalid consentType. Must be "workshop" or "social_media"' }); } if (!['revoke', 'restore'].includes(action)) { return res.status(400).json({ success: false, error: 'Invalid action. Must be "revoke" or "restore"' }); } if (consentType === 'social_media' && !platformId) { return res.status(400).json({ success: false, error: 'platformId is required for social_media consent type' }); } // Load group by management token const groupData = await groupRepository.getGroupByManagementToken(token); if (!groupData) { return res.status(404).json({ success: false, error: 'Management token not found or group has been deleted' }); } // Handle workshop consent if (consentType === 'workshop') { const newValue = action === 'revoke' ? 0 : 1; await dbManager.run( 'UPDATE groups SET display_in_workshop = ? WHERE group_id = ?', [newValue, groupData.groupId] ); // Sende Telegram-Benachrichtigung (async, non-blocking) if (telegramService.isAvailable()) { telegramService.sendConsentChangeNotification({ name: groupData.name, year: groupData.year, title: groupData.title, consentType: 'workshop', action: action, newValue: newValue === 1 }).catch(err => { console.error('[Telegram] Consent change notification failed:', err.message); }); } return res.json({ success: true, message: `Workshop consent ${action}d successfully`, data: { consentType: 'workshop', displayInWorkshop: newValue === 1 } }); } // Handle social media consent if (consentType === 'social_media') { const SocialMediaRepository = require('../repositories/SocialMediaRepository'); const socialMediaRepo = new SocialMediaRepository(dbManager); if (action === 'revoke') { // Check if consent exists before revoking const existing = await dbManager.get( 'SELECT id FROM group_social_media_consents WHERE group_id = ? AND platform_id = ?', [groupData.groupId, platformId] ); if (existing) { await socialMediaRepo.revokeConsent(groupData.groupId, platformId); } else { // Can't revoke what doesn't exist - return error return res.status(400).json({ success: false, error: 'Cannot revoke consent that was never granted' }); } } else { // action === 'restore' // Check if consent exists const existing = await dbManager.get( 'SELECT id, revoked FROM group_social_media_consents WHERE group_id = ? AND platform_id = ?', [groupData.groupId, platformId] ); if (existing) { // Restore existing consent await socialMediaRepo.restoreConsent(groupData.groupId, platformId); } else { // Create new consent (user wants to grant consent for a platform they didn't select during upload) await dbManager.run( `INSERT INTO group_social_media_consents (group_id, platform_id, consented, consent_timestamp) VALUES (?, ?, 1, CURRENT_TIMESTAMP)`, [groupData.groupId, platformId] ); } } // Sende Telegram-Benachrichtigung (async, non-blocking) if (telegramService.isAvailable()) { // Hole Platform-Name für Benachrichtigung const platform = await dbManager.get( 'SELECT name FROM social_media_platforms WHERE id = ?', [platformId] ); telegramService.sendConsentChangeNotification({ name: groupData.name, year: groupData.year, title: groupData.title, consentType: 'social_media', action: action, platform: platform ? platform.name : `Platform ${platformId}` }).catch(err => { console.error('[Telegram] Consent change notification failed:', err.message); }); } return res.json({ success: true, message: `Social media consent ${action}d successfully`, data: { consentType: 'social_media', platformId: platformId, revoked: action === 'revoke' } }); } } catch (error) { console.error('Error updating consent:', error); res.status(500).json({ success: false, error: 'Failed to update consent' }); } }); /** * PUT /api/manage/:token/images/descriptions * Batch update image descriptions for a group * * Body: * - descriptions: [{ imageId: number, description: string }, ...] * * @returns {Object} Update result with count of updated images * @throws {400} Invalid request or validation error * @throws {404} Token invalid or not found * @throws {500} Server error */ router.put('/:token/images/descriptions', async (req, res) => { /* #swagger.tags = ['Management Portal'] #swagger.summary = 'Batch update image descriptions' #swagger.description = 'Updates descriptions for multiple images in a group (max 200 chars each)' #swagger.parameters['token'] = { in: 'path', required: true, type: 'string', description: 'Management token (UUID v4)', example: '550e8400-e29b-41d4-a716-446655440000' } #swagger.parameters['body'] = { in: 'body', required: true, schema: { descriptions: [ { imageId: 1, description: 'Sonnenuntergang' }, { imageId: 2, description: 'Gruppenfoto' } ] } } #swagger.responses[200] = { description: 'Descriptions updated', schema: { success: true, message: '2 image descriptions updated successfully', updatedCount: 2 } } #swagger.responses[400] = { description: 'Invalid request or description too long' } #swagger.responses[404] = { description: 'Invalid token' } */ try { const { token } = req.params; const { descriptions } = req.body; // Validate token format if (!validateToken(token)) { return res.status(404).json({ success: false, error: 'Invalid management token format' }); } // Validate descriptions array if (!Array.isArray(descriptions) || descriptions.length === 0) { return res.status(400).json({ success: false, error: 'descriptions must be a non-empty array' }); } // Validate each description for (const desc of descriptions) { if (!desc.imageId || typeof desc.imageId !== 'number') { return res.status(400).json({ success: false, error: 'Each description must contain a valid imageId' }); } if (desc.description && desc.description.length > 200) { return res.status(400).json({ success: false, error: `Description for image ${desc.imageId} exceeds 200 characters` }); } } // Load group by management token const groupData = await groupRepository.getGroupByManagementToken(token); if (!groupData) { return res.status(404).json({ success: false, error: 'Management token not found or group has been deleted' }); } // Update descriptions let updatedCount = 0; for (const desc of descriptions) { const updated = await groupRepository.updateImageDescription( desc.imageId, groupData.groupId, desc.description || null ); if (updated) { updatedCount++; } } await res.auditLog('update_image_descriptions', true, groupData.groupId, `Updated ${updatedCount} image descriptions`); res.json({ success: true, message: `${updatedCount} image description(s) updated successfully`, data: { groupId: groupData.groupId, updatedImages: updatedCount, totalRequested: descriptions.length } }); } catch (error) { console.error('Error updating image descriptions:', error); await res.auditLog('update_image_descriptions', false, null, error.message); res.status(500).json({ success: false, error: 'Failed to update image descriptions' }); } }); /** * PUT /api/manage/:token/metadata * Update group metadata (title, description, name) * IMPORTANT: Sets approved=0 (returns to moderation queue) * * Body: * - title: string (optional) * - description: string (optional) * - name: string (optional) * * @returns {Object} Updated metadata * @throws {400} Invalid request or validation error * @throws {404} Token invalid or not found * @throws {500} Server error */ router.put('/:token/metadata', async (req, res) => { /* #swagger.tags = ['Management Portal'] #swagger.summary = 'Update group metadata' #swagger.description = 'Updates group title, description or name. Sets approved=0 (returns to moderation).' #swagger.parameters['token'] = { in: 'path', required: true, type: 'string', description: 'Management token (UUID v4)', example: '550e8400-e29b-41d4-a716-446655440000' } #swagger.parameters['body'] = { in: 'body', required: true, schema: { title: 'Sommercamp 2025', description: 'Tolle Veranstaltung', name: 'Familie_Mueller' } } #swagger.responses[200] = { description: 'Metadata updated', schema: { success: true, message: 'Metadata updated successfully', data: { groupId: 'abc123', updatedFields: ['title', 'description'], requiresModeration: true } } } #swagger.responses[400] = { description: 'No fields provided' } #swagger.responses[404] = { description: 'Invalid token' } */ try { const { token } = req.params; const { title, description, name } = req.body; // Validate token format if (!validateToken(token)) { return res.status(404).json({ success: false, error: 'Invalid management token format' }); } // At least one field must be provided if (title === undefined && description === undefined && name === undefined) { return res.status(400).json({ success: false, error: 'At least one field (title, description, name) must be provided' }); } // Load group by management token const groupData = await groupRepository.getGroupByManagementToken(token); if (!groupData) { return res.status(404).json({ success: false, error: 'Management token not found or group has been deleted' }); } // Build update query dynamically const updates = []; const values = []; if (title !== undefined) { updates.push('title = ?'); values.push(title); } if (description !== undefined) { updates.push('description = ?'); values.push(description); } if (name !== undefined) { updates.push('name = ?'); values.push(name); } // Always reset approval when metadata changes updates.push('approved = 0'); // Add groupId to values values.push(groupData.groupId); // Execute update const query = `UPDATE groups SET ${updates.join(', ')} WHERE group_id = ?`; await dbManager.run(query, values); // Load updated group data const updatedGroup = await groupRepository.getGroupByManagementToken(token); res.json({ success: true, message: 'Metadata updated successfully. Group returned to moderation queue.', data: { groupId: updatedGroup.groupId, title: updatedGroup.title, description: updatedGroup.description, name: updatedGroup.name, approved: updatedGroup.approved } }); } catch (error) { console.error('Error updating metadata:', error); res.status(500).json({ success: false, error: 'Failed to update metadata' }); } }); /** * POST /api/manage/:token/images * Add new images to existing group * IMPORTANT: Sets approved=0 (returns to moderation queue) * * Form-Data: * - images: file(s) to upload * * @returns {Object} Upload result with image count * @throws {400} Invalid request or no images * @throws {404} Token invalid or not found * @throws {500} Server error */ router.post('/:token/images', async (req, res) => { /* #swagger.tags = ['Management Portal'] #swagger.summary = 'Add new images to group' #swagger.description = 'Uploads additional images to existing group. Sets approved=0 (requires re-moderation). Max 50 images per group.' #swagger.consumes = ['multipart/form-data'] #swagger.parameters['token'] = { in: 'path', required: true, type: 'string', description: 'Management token (UUID v4)', example: '550e8400-e29b-41d4-a716-446655440000' } #swagger.parameters['images'] = { in: 'formData', type: 'file', required: true, description: 'Image files to upload (JPEG, PNG)' } #swagger.responses[200] = { description: 'Images uploaded', schema: { success: true, message: '3 images added successfully', data: { groupId: 'abc123', newImagesCount: 3, totalImagesCount: 15 } } } #swagger.responses[400] = { description: 'No images or limit exceeded (max 50)' } #swagger.responses[404] = { description: 'Invalid token' } */ try { const { token } = req.params; // Validate token format if (!validateToken(token)) { return res.status(404).json({ success: false, error: 'Invalid management token format' }); } // Check if files were uploaded if (!req.files || !req.files.images) { return res.status(400).json({ success: false, error: 'No images uploaded' }); } // Load group by management token const groupData = await groupRepository.getGroupByManagementToken(token); if (!groupData) { return res.status(404).json({ success: false, error: 'Management token not found or group has been deleted' }); } // Check current image count and validate limit (max 50 images per group) const MAX_IMAGES_PER_GROUP = 50; const currentImageCount = groupData.imageCount; // Handle both single file and array of files const files = Array.isArray(req.files.images) ? req.files.images : [req.files.images]; const newImageCount = currentImageCount + files.length; if (newImageCount > MAX_IMAGES_PER_GROUP) { return res.status(400).json({ success: false, error: `Cannot add ${files.length} images. Group has ${currentImageCount} images, maximum is ${MAX_IMAGES_PER_GROUP}.` }); } console.log(`Adding ${files.length} files to group ${groupData.groupId}`); // Process all files const generateId = require("shortid"); const path = require('path'); const { UPLOAD_FS_DIR, PREVIEW_FS_DIR } = require('../constants'); const ImagePreviewService = require('../services/ImagePreviewService'); const processedFiles = []; const uploadDir = path.join(__dirname, '..', UPLOAD_FS_DIR); for (let i = 0; i < files.length; i++) { const file = files[i]; // Generate unique filename const fileEnding = file.name.split(".").pop(); const fileName = generateId() + '.' + fileEnding; // Save file to data/images const uploadPath = path.join(uploadDir, fileName); await new Promise((resolve, reject) => { file.mv(uploadPath, (err) => { if (err) { console.error('Error saving file:', err); reject(err); } else { resolve(); } }); }); // Calculate new upload order (append to existing images) const uploadOrder = currentImageCount + i + 1; // Insert image into database await dbManager.run(` INSERT INTO images (group_id, file_name, original_name, file_path, upload_order, file_size, mime_type) VALUES (?, ?, ?, ?, ?, ?, ?) `, [ groupData.groupId, fileName, file.name, `/upload/${fileName}`, uploadOrder, file.size, file.mimetype ]); processedFiles.push({ fileName, originalName: file.name, size: file.size, uploadOrder }); } // Reset approval status (group needs re-moderation after adding images) await dbManager.run('UPDATE groups SET approved = 0 WHERE group_id = ?', [groupData.groupId]); // Generate previews in background (don't wait) const previewDir = path.join(__dirname, '..', PREVIEW_FS_DIR); ImagePreviewService.generatePreviewsForGroup( processedFiles.map(f => ({ file_name: f.fileName, file_path: `/upload/${f.fileName}` })), uploadDir, previewDir ).then(results => { const successCount = results.filter(r => r.success).length; console.log(`Preview generation completed: ${successCount}/${results.length} successful`); }).catch(error => { console.error('Error generating previews:', error); }); res.json({ success: true, message: `${files.length} image(s) added successfully. Group returned to moderation queue.`, data: { groupId: groupData.groupId, imagesAdded: files.length, totalImages: newImageCount, approved: 0, uploadedFiles: processedFiles.map(f => ({ fileName: f.fileName, originalName: f.originalName, uploadOrder: f.uploadOrder })) } }); } catch (error) { console.error('Error adding images:', error); res.status(500).json({ success: false, error: 'Failed to add images' }); } }); /** * DELETE /api/manage/:token/images/:imageId * Delete a single image from group * Removes files (original + preview) and database entry * IMPORTANT: Sets approved=0 if group was previously approved * * @returns {Object} Deletion result * @throws {400} Cannot delete last image (use group delete instead) * @throws {404} Token/image not found * @throws {500} Server error */ router.delete('/:token/images/:imageId', async (req, res) => { /* #swagger.tags = ['Management Portal'] #swagger.summary = 'Delete single image' #swagger.description = 'Deletes a specific image from group (files + DB entry). Sets approved=0. Cannot delete last image.' #swagger.parameters['token'] = { in: 'path', required: true, type: 'string', description: 'Management token (UUID v4)', example: '550e8400-e29b-41d4-a716-446655440000' } #swagger.parameters['imageId'] = { in: 'path', required: true, type: 'integer', description: 'Image ID', example: 42 } #swagger.responses[200] = { description: 'Image deleted', schema: { success: true, message: 'Image deleted successfully', data: { groupId: 'abc123', imageId: 42, remainingImages: 11 } } } #swagger.responses[400] = { description: 'Cannot delete last image' } #swagger.responses[404] = { description: 'Invalid token or image not found' } */ try { const { token, imageId } = req.params; // Validate token format if (!validateToken(token)) { return res.status(404).json({ success: false, error: 'Invalid management token format' }); } // Load group by management token const groupData = await groupRepository.getGroupByManagementToken(token); if (!groupData) { return res.status(404).json({ success: false, error: 'Management token not found or group has been deleted' }); } // Load image data const image = await dbManager.get( 'SELECT * FROM images WHERE id = ? AND group_id = ?', [imageId, groupData.groupId] ); if (!image) { return res.status(404).json({ success: false, error: 'Image not found in this group' }); } // Prevent deletion of last image (use group delete instead) if (groupData.imageCount === 1) { return res.status(400).json({ success: false, error: 'Cannot delete the last image. Use group deletion instead.' }); } // Delete image file from filesystem const path = require('path'); const fs = require('fs'); const { UPLOAD_FS_DIR, PREVIEW_FS_DIR } = require('../constants'); const originalPath = path.join(__dirname, '..', UPLOAD_FS_DIR, image.file_name); const previewPath = image.preview_path ? path.join(__dirname, '..', PREVIEW_FS_DIR, path.basename(image.preview_path)) : null; // Delete original file try { if (fs.existsSync(originalPath)) { fs.unlinkSync(originalPath); console.log(`Deleted original file: ${originalPath}`); } } catch (error) { console.error(`Error deleting original file: ${originalPath}`, error); } // Delete preview file if exists if (previewPath) { try { if (fs.existsSync(previewPath)) { fs.unlinkSync(previewPath); console.log(`Deleted preview file: ${previewPath}`); } } catch (error) { console.error(`Error deleting preview file: ${previewPath}`, error); } } // Delete image from database await dbManager.run('DELETE FROM images WHERE id = ?', [imageId]); // Reset approval if group was previously approved if (groupData.approved === 1) { await dbManager.run('UPDATE groups SET approved = 0 WHERE group_id = ?', [groupData.groupId]); } res.json({ success: true, message: 'Image deleted successfully' + (groupData.approved === 1 ? '. Group returned to moderation queue.' : '.'), data: { groupId: groupData.groupId, deletedImageId: parseInt(imageId), remainingImages: groupData.imageCount - 1, approved: groupData.approved === 1 ? 0 : groupData.approved } }); } catch (error) { console.error('Error deleting image:', error); res.status(500).json({ success: false, error: 'Failed to delete image' }); } }); /** * DELETE /api/manage/:token * Delete complete group with all images and consents * Removes all files (originals + previews) and database entries * Creates deletion_log entry for audit trail * * @returns {Object} Deletion confirmation * @throws {404} Token invalid or not found * @throws {500} Server error */ router.delete('/:token', async (req, res) => { /* #swagger.tags = ['Management Portal'] #swagger.summary = 'Delete complete group' #swagger.description = 'Deletes entire group with all images, consents and metadata. Creates deletion_log entry. Removes all files (originals + previews).' #swagger.parameters['token'] = { in: 'path', required: true, type: 'string', description: 'Management token (UUID v4)', example: '550e8400-e29b-41d4-a716-446655440000' } #swagger.responses[200] = { description: 'Group deleted', schema: { success: true, message: 'Group and all associated data deleted successfully', data: { groupId: 'abc123', imagesDeleted: 12, deletionTimestamp: '2025-11-15T16:30:00Z' } } } #swagger.responses[404] = { description: 'Invalid token or group already deleted' } */ try { const { token } = req.params; // Validate token format if (!validateToken(token)) { return res.status(404).json({ success: false, error: 'Invalid management token format' }); } // Load group by management token const groupData = await groupRepository.getGroupByManagementToken(token); if (!groupData) { return res.status(404).json({ success: false, error: 'Management token not found or group has been deleted' }); } const groupId = groupData.groupId; const imageCount = groupData.imageCount; // Delete all image files (originals + previews) const fs = require('fs'); const path = require('path'); const { UPLOAD_FS_DIR, PREVIEW_FS_DIR } = require('../constants'); for (const image of groupData.images) { // Delete original file try { const originalPath = path.join(__dirname, '..', UPLOAD_FS_DIR, image.fileName); if (fs.existsSync(originalPath)) { fs.unlinkSync(originalPath); console.log(`Deleted original: ${originalPath}`); } } catch (error) { console.error(`Error deleting original ${image.fileName}:`, error); } // Delete preview file if exists if (image.previewPath) { try { const previewPath = path.join(__dirname, '..', PREVIEW_FS_DIR, path.basename(image.previewPath)); if (fs.existsSync(previewPath)) { fs.unlinkSync(previewPath); console.log(`Deleted preview: ${previewPath}`); } } catch (error) { console.error(`Error deleting preview ${image.previewPath}:`, error); } } } // Delete group from database (CASCADE will delete images and consents) await dbManager.run('DELETE FROM groups WHERE group_id = ?', [groupId]); // Create deletion_log entry await deletionLogRepository.createDeletionEntry({ groupId: groupId, year: groupData.year, imageCount: imageCount, uploadDate: groupData.uploadDate, deletionReason: 'user_self_service_deletion', totalFileSize: groupData.images.reduce((sum, img) => sum + (img.fileSize || 0), 0) }); console.log(`✓ Group ${groupId} deleted via management token (${imageCount} images)`); // Sende Telegram-Benachrichtigung (async, non-blocking) if (telegramService.isAvailable()) { telegramService.sendGroupDeletedNotification({ name: groupData.name, year: groupData.year, title: groupData.title, imageCount: imageCount }).catch(err => { console.error('[Telegram] Group deletion notification failed:', err.message); }); } res.json({ success: true, message: 'Group and all associated data deleted successfully', data: { groupId: groupId, imagesDeleted: imageCount, deletionTimestamp: new Date().toISOString() } }); } catch (error) { console.error('Error deleting group:', error); res.status(500).json({ success: false, error: 'Failed to delete group' }); } }); router.put('/:token/reorder', async (req, res) => { /* #swagger.tags = ['Management Portal'] #swagger.summary = 'Reorder images in group' #swagger.description = 'Reorder images within the managed group (token-based access)' #swagger.parameters['token'] = { in: 'path', required: true, type: 'string', description: 'Management token (UUID v4)', example: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' } #swagger.parameters['body'] = { in: 'body', required: true, schema: { imageIds: [1, 3, 2, 4] } } #swagger.responses[200] = { description: 'Images reordered successfully', schema: { success: true, updatedCount: 4 } } #swagger.responses[400] = { description: 'Invalid token format or imageIds' } #swagger.responses[404] = { description: 'Token not found or group deleted' } */ try { const { token } = req.params; const { imageIds } = req.body; // Validate token format if (!validateToken(token)) { recordFailedTokenValidation(req); return res.status(400).json({ success: false, error: 'Invalid management token format' }); } // Validate imageIds if (!imageIds || !Array.isArray(imageIds) || imageIds.length === 0) { return res.status(400).json({ success: false, error: 'imageIds array is required and cannot be empty' }); } // Validate that all imageIds are numbers const invalidIds = imageIds.filter(id => !Number.isInteger(id) || id <= 0); if (invalidIds.length > 0) { return res.status(400).json({ success: false, error: `Invalid image IDs: ${invalidIds.join(', ')}. Image IDs must be positive integers` }); } // Load group by token to get groupId const groupData = await groupRepository.getGroupByManagementToken(token); if (!groupData) { recordFailedTokenValidation(req); await res.auditLog('reorder_images', false, null, 'Token not found or group deleted'); return res.status(404).json({ success: false, error: 'Management token not found or group has been deleted' }); } // Execute reorder using GroupRepository const result = await groupRepository.updateImageOrder(groupData.groupId, imageIds); await res.auditLog('reorder_images', true, groupData.groupId, `Reordered ${result.updatedImages} images`); res.status(200).json({ success: true, message: 'Image order updated successfully', data: result }); } catch (error) { console.error(`[MANAGEMENT] Error reordering images for token ${req.params.token}:`, error.message); await res.auditLog('reorder_images', false, null, error.message); // Handle specific errors if (error.message.includes('not found')) { return res.status(404).json({ success: false, error: error.message }); } if (error.message.includes('Invalid image IDs') || error.message.includes('Missing image IDs')) { return res.status(400).json({ success: false, error: error.message }); } res.status(500).json({ success: false, error: 'Failed to reorder images' }); } }); module.exports = router;