const express = require('express'); const router = express.Router(); const DeletionLogRepository = require('../repositories/DeletionLogRepository'); const ManagementAuditLogRepository = require('../repositories/ManagementAuditLogRepository'); const GroupRepository = require('../repositories/GroupRepository'); const GroupCleanupService = require('../services/GroupCleanupService'); const AdminAuthService = require('../services/AdminAuthService'); const { getStatistics: getRateLimiterStats } = require('../middlewares/rateLimiter'); const { requireAdminAuth } = require('../middlewares/auth'); const { requireCsrf } = require('../middlewares/csrf'); // GroupCleanupService ist bereits eine Instanz, keine Klasse const cleanupService = GroupCleanupService; // Apply admin authentication to ALL routes in this router router.use(requireAdminAuth); router.use(requireCsrf); router.post('/users', async (req, res) => { /* #swagger.tags = ['Admin - Users'] #swagger.summary = 'Create a new admin user' #swagger.description = 'Adds an additional admin (or auditor) via API' #swagger.requestBody = { required: true, content: { 'application/json': { schema: { type: 'object', required: ['username', 'password'], properties: { username: { type: 'string', example: 'admin2' }, password: { type: 'string', example: 'SehrSicher123!' }, role: { type: 'string', example: 'admin' }, requirePasswordChange: { type: 'boolean', example: true } } } } } } #swagger.responses[201] = { description: 'Admin user created', schema: { success: true, user: { id: 5, username: 'admin2', role: 'admin', requiresPasswordChange: false } } } */ try { const { username, password, role, requirePasswordChange } = req.body || {}; const user = await AdminAuthService.createAdminUser({ username, password, role, requiresPasswordChange: Boolean(requirePasswordChange) }); res.status(201).json({ success: true, user }); } catch (error) { console.error('[Admin API] create user failed:', error.message); if (['USERNAME_REQUIRED', 'PASSWORD_TOO_WEAK'].includes(error.message)) { return res.status(400).json({ error: error.message }); } if (error.message === 'USERNAME_IN_USE') { return res.status(409).json({ error: 'USERNAME_IN_USE' }); } res.status(500).json({ error: 'CREATE_ADMIN_FAILED' }); } }); router.get('/deletion-log', async (req, res) => { /* #swagger.tags = ['Admin - Deletion Log'] #swagger.summary = 'Get recent deletion log entries' #swagger.description = 'Returns recent deletion log entries with optional limit' #swagger.parameters['limit'] = { in: 'query', type: 'integer', description: 'Number of entries to return (1-1000)', example: 10 } #swagger.responses[200] = { description: 'Deletion log entries', schema: { success: true, deletions: [], total: 2, limit: 10 } } #swagger.responses[400] = { description: 'Invalid limit parameter' } */ try { const limit = parseInt(req.query.limit) || 10; if (limit < 1 || limit > 1000) { return res.status(400).json({ error: 'Invalid limit', message: 'Limit must be between 1 and 1000' }); } const deletions = await DeletionLogRepository.getRecentDeletions(limit); const total = deletions.length; res.json({ success: true, deletions: deletions, total: total, limit: limit }); } catch (error) { console.error('Error fetching deletion log:', error); res.status(500).json({ error: 'Internal server error', message: error.message }); } }); router.get('/deletion-log/all', async (req, res) => { /* #swagger.tags = ['Admin - Deletion Log'] #swagger.summary = 'Get all deletion log entries' #swagger.description = 'Returns complete deletion log without pagination' #swagger.responses[200] = { description: 'All deletion log entries', schema: { success: true, deletions: [], total: 50 } } */ try { const deletions = await DeletionLogRepository.getAllDeletions(); res.json({ success: true, deletions: deletions, total: deletions.length }); } catch (error) { console.error('Error fetching all deletion logs:', error); res.status(500).json({ error: 'Internal server error', message: error.message }); } }); router.get('/deletion-log/stats', async (req, res) => { /* #swagger.tags = ['Admin - Deletion Log'] #swagger.summary = 'Get deletion statistics' #swagger.description = 'Returns aggregated statistics about deleted images' #swagger.responses[200] = { description: 'Deletion statistics', schema: { success: true, totalDeleted: 12, totalImages: 348, totalSize: '19.38 MB', totalSizeBytes: 20324352, lastCleanup: '2025-11-15T10:30:00Z' } } */ try { const stats = await DeletionLogRepository.getDeletionStatistics(); // Format file size const formatBytes = (bytes) => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; res.json({ success: true, totalDeleted: stats.totalDeleted, totalImages: stats.totalImages, totalSize: formatBytes(stats.totalSize), totalSizeBytes: stats.totalSize, lastCleanup: stats.lastCleanup }); } catch (error) { console.error('Error fetching deletion statistics:', error); res.status(500).json({ error: 'Internal server error', message: error.message }); } }); router.post('/cleanup/trigger', async (req, res) => { /* #swagger.tags = ['Admin - Cleanup'] #swagger.summary = 'Manually trigger cleanup of unapproved groups' #swagger.description = 'Deletes groups that have not been approved within retention period' #swagger.responses[200] = { description: 'Cleanup completed', schema: { success: true, deletedGroups: 3, message: '3 alte unbestätigte Gruppen gelöscht' } } */ try { console.log('[Admin API] Manual cleanup triggered'); const result = await cleanupService.performScheduledCleanup(); res.json({ success: true, result: result, message: `Cleanup completed: ${result.deletedGroups} groups deleted` }); } catch (error) { console.error('[Admin API] Error triggering cleanup:', error); res.status(500).json({ error: 'Internal server error', message: error.message }); } }); router.get('/cleanup/preview', async (req, res) => { /* #swagger.tags = ['Admin - Cleanup'] #swagger.summary = 'Preview groups that would be deleted' #swagger.description = 'Dry-run showing which unapproved groups are eligible for deletion' #swagger.responses[200] = { description: 'Preview of groups to delete', schema: { success: true, groupsToDelete: 2, groups: [{ id: 'abc123', groupName: 'Familie_Mueller', uploadDate: '2025-10-01T12:00:00Z', daysUntilDeletion: -5, imageCount: 8 }], message: '2 groups would be deleted' } } */ try { const groups = await cleanupService.findGroupsForDeletion(); // Berechne Tage bis zur Löschung für jede Gruppe const groupsWithDays = groups.map(group => ({ ...group, daysUntilDeletion: cleanupService.getDaysUntilDeletion(group.uploadDate) })); res.json({ success: true, groupsToDelete: groupsWithDays.length, groups: groupsWithDays, message: groupsWithDays.length === 0 ? 'No groups would be deleted' : `${groupsWithDays.length} groups would be deleted` }); } catch (error) { console.error('[Admin API] Error previewing cleanup:', error); res.status(500).json({ error: 'Internal server error', message: error.message }); } }); router.get('/rate-limiter/stats', async (req, res) => { /* #swagger.tags = ['Admin - Monitoring'] #swagger.summary = 'Get rate limiter statistics' #swagger.description = 'Returns statistics about rate limiting (blocked requests, active limits)' #swagger.responses[200] = { description: 'Rate limiter statistics', schema: { success: true, totalRequests: 1523, blockedRequests: 12, activeClients: 45 } } */ try { const stats = getRateLimiterStats(); res.json({ success: true, ...stats }); } catch (error) { console.error('[Admin API] Error fetching rate-limiter stats:', error); res.status(500).json({ error: 'Internal server error', message: error.message }); } }); router.get('/management-audit', async (req, res) => { /* #swagger.tags = ['Admin - Monitoring'] #swagger.summary = 'Get management audit log entries' #swagger.description = 'Returns recent management portal activity logs' #swagger.parameters['limit'] = { in: 'query', type: 'integer', description: 'Number of entries to return (1-1000)', example: 100 } #swagger.responses[200] = { description: 'Audit log entries', schema: { success: true, logs: [], total: 15, limit: 100 } } #swagger.responses[400] = { description: 'Invalid limit parameter' } */ try { const limit = parseInt(req.query.limit) || 100; if (limit < 1 || limit > 1000) { return res.status(400).json({ error: 'Invalid limit', message: 'Limit must be between 1 and 1000' }); } const logs = await ManagementAuditLogRepository.getRecentLogs(limit); res.json({ success: true, logs: logs, total: logs.length, limit: limit }); } catch (error) { console.error('[Admin API] Error fetching management audit log:', error); res.status(500).json({ error: 'Internal server error', message: error.message }); } }); router.get('/management-audit/stats', async (req, res) => { /* #swagger.tags = ['Admin - Monitoring'] #swagger.summary = 'Get management audit log statistics' #swagger.description = 'Returns aggregated statistics about management portal activity' #swagger.responses[200] = { description: 'Audit log statistics', schema: { success: true, totalActions: 523, actionsByType: { 'update': 312, 'delete': 45, 'approve': 166 }, lastAction: '2025-11-15T14:30:00Z' } } */ try { const stats = await ManagementAuditLogRepository.getStatistics(); res.json({ success: true, ...stats }); } catch (error) { console.error('[Admin API] Error fetching audit log stats:', error); res.status(500).json({ error: 'Internal server error', message: error.message }); } }); router.get('/management-audit/group/:groupId', async (req, res) => { /* #swagger.tags = ['Admin - Monitoring'] #swagger.summary = 'Get audit log for specific group' #swagger.description = 'Returns all management actions performed on a specific group' #swagger.parameters['groupId'] = { in: 'path', required: true, type: 'string', description: 'Group ID', example: 'abc123def456' } #swagger.responses[200] = { description: 'Audit log for group', schema: { success: true, groupId: 'abc123def456', logs: [], total: 8 } } */ try { const { groupId } = req.params; const logs = await ManagementAuditLogRepository.getLogsByGroupId(groupId); res.json({ success: true, groupId: groupId, logs: logs, total: logs.length }); } catch (error) { console.error('[Admin API] Error fetching audit log for group:', error); res.status(500).json({ error: 'Internal server error', message: error.message }); } }); // ============================================================================ // GRUPPEN-MODERATION (verschoben von groups.js) // ============================================================================ router.get('/groups', async (req, res) => { /* #swagger.tags = ['Admin - Groups Moderation'] #swagger.summary = 'Get all groups for moderation' #swagger.description = 'Returns all groups including unapproved ones with moderation info and consent data' #swagger.parameters['workshopOnly'] = { in: 'query', type: 'boolean', description: 'Filter by workshop consent', example: false } #swagger.parameters['platform'] = { in: 'query', type: 'string', description: 'Filter by social media platform', example: 'instagram' } #swagger.responses[200] = { description: 'All groups with moderation info', schema: { success: true, groups: [{ groupId: 'abc123', groupName: 'Familie_Mueller', isApproved: false, uploadDate: '2025-11-01T10:00:00Z', imageCount: 12, socialMediaConsents: [] }] } } */ try { const { workshopOnly, platform, consents } = req.query; // Hole alle Gruppen mit vollständigen Infos (inkl. Bilder) let allGroups = await GroupRepository.getAllGroupsWithModerationInfo(); // Füge Consent-Daten für jede Gruppe hinzu const groupsWithConsents = await Promise.all( allGroups.map(async (group) => { const consentData = await GroupRepository.getSocialMediaConsentsForGroup(group.groupId); return { ...group, socialMediaConsents: consentData }; }) ); // Jetzt filtern wir basierend auf den Query-Parametern let filteredGroups = groupsWithConsents; // Neuer Multi-Checkbox Filter if (consents) { const selectedConsents = consents.split(','); // z.B. ['workshop', 'facebook', 'instagram'] filteredGroups = groupsWithConsents.filter(group => { // Gruppe muss mindestens einen der ausgewählten Consents haben return selectedConsents.some(consentType => { if (consentType === 'workshop') { return group.display_in_workshop === 1 || group.display_in_workshop === true; } else { // Social Media Platform (facebook, instagram, tiktok) return group.socialMediaConsents && group.socialMediaConsents.some(consent => consent.platform_name === consentType && (consent.consented === 1 || consent.consented === true) && (consent.revoked !== 1 && consent.revoked !== true) ); } }); }); } else if (workshopOnly === 'true') { // Filter: Nur Gruppen MIT Werkstatt-Consent aber OHNE zugestimmte Social Media Consents filteredGroups = groupsWithConsents.filter(group => { // Muss Werkstatt-Consent haben if (!group.display_in_workshop) return false; // Darf KEINE zugestimmten Social Media Consents haben const hasConsentedSocialMedia = group.socialMediaConsents && group.socialMediaConsents.some(consent => consent.consented === 1 || consent.consented === true); return !hasConsentedSocialMedia; }); } else if (platform) { // Filter: Gruppen mit bestimmter Social Media Platform (unabhängig vom Werkstatt-Consent) filteredGroups = groupsWithConsents.filter(group => group.socialMediaConsents && group.socialMediaConsents.some(consent => consent.platform_name === platform && (consent.consented === 1 || consent.consented === true) ) ); } // else: Kein Filter - zeige ALLE Gruppen (nicht filtern) res.json({ groups: filteredGroups, totalCount: filteredGroups.length, pendingCount: filteredGroups.filter(g => !g.approved).length, approvedCount: filteredGroups.filter(g => g.approved).length }); } catch (error) { console.error('Error fetching moderation groups:', error); res.status(500).json({ error: 'Internal server error', message: 'Fehler beim Laden der Moderations-Gruppen', details: error.message }); } }); router.get('/groups/:groupId', async (req, res) => { /* #swagger.tags = ['Admin - Groups Moderation'] #swagger.summary = 'Get single group for moderation' #swagger.description = 'Returns detailed info for a specific group including unapproved ones' #swagger.parameters['groupId'] = { in: 'path', required: true, type: 'string', description: 'Group ID', example: 'abc123def456' } #swagger.responses[200] = { description: 'Group details with images', schema: { groupId: 'abc123', groupName: 'Familie_Mueller', isApproved: true, images: [] } } #swagger.responses[404] = { description: 'Group not found' } */ try { const { groupId } = req.params; const group = await GroupRepository.getGroupForModeration(groupId); if (!group) { return res.status(404).json({ error: 'Group not found', message: `Gruppe mit ID ${groupId} wurde nicht gefunden` }); } res.json(group); } catch (error) { console.error('Error fetching group for moderation:', error); res.status(500).json({ error: 'Internal server error', message: 'Fehler beim Laden der Gruppe für Moderation', details: error.message }); } }); router.patch('/groups/:groupId/approve', async (req, res) => { /* #swagger.tags = ['Admin - Groups Moderation'] #swagger.summary = 'Approve a group' #swagger.description = 'Marks a group as approved, making it publicly visible' #swagger.parameters['groupId'] = { in: 'path', required: true, type: 'string', description: 'Group ID', example: 'abc123def456' } #swagger.parameters['body'] = { in: 'body', required: false, schema: { approved: true } } #swagger.responses[200] = { description: 'Group approved successfully', schema: { success: true, message: 'Gruppe erfolgreich freigegeben' } } #swagger.responses[404] = { description: 'Group not found' } */ try { const { groupId } = req.params; const { approved } = req.body; // Validierung if (typeof approved !== 'boolean') { return res.status(400).json({ error: 'Invalid request', message: 'approved muss ein boolean Wert sein' }); } const updated = await GroupRepository.updateGroupApproval(groupId, approved); if (!updated) { return res.status(404).json({ error: 'Group not found', message: `Gruppe mit ID ${groupId} wurde nicht gefunden` }); } res.json({ success: true, message: approved ? 'Gruppe freigegeben' : 'Gruppe gesperrt', groupId: groupId, approved: approved }); } catch (error) { console.error('Error updating group approval:', error); res.status(500).json({ error: 'Internal server error', message: 'Fehler beim Aktualisieren der Freigabe' }); } }); router.patch('/groups/:groupId', async (req, res) => { /* #swagger.tags = ['Admin - Groups Moderation'] #swagger.summary = 'Update group metadata' #swagger.description = 'Updates group metadata fields (year, title, description, name)' #swagger.parameters['groupId'] = { in: 'path', required: true, type: 'string', description: 'Group ID', example: 'abc123def456' } #swagger.parameters['body'] = { in: 'body', required: true, schema: { year: 2025, title: 'Sommercamp', description: 'Tolle Veranstaltung', name: 'Familie_Mueller' } } #swagger.responses[200] = { description: 'Group updated successfully', schema: { success: true, message: 'Gruppe aktualisiert', updatedFields: ['year', 'title'] } } #swagger.responses[404] = { description: 'Group not found' } */ try { const { groupId } = req.params; // Erlaubte Felder zum Aktualisieren const allowed = ['year', 'title', 'description', 'name']; const updates = {}; for (const field of allowed) { if (req.body[field] !== undefined) { updates[field] = req.body[field]; } } if (Object.keys(updates).length === 0) { return res.status(400).json({ error: 'Invalid request', message: 'Keine gültigen Felder zum Aktualisieren angegeben' }); } const updated = await GroupRepository.updateGroup(groupId, updates); if (!updated) { return res.status(404).json({ error: 'Group not found', message: `Gruppe mit ID ${groupId} wurde nicht gefunden` }); } res.json({ success: true, message: 'Gruppe erfolgreich aktualisiert', groupId: groupId, updates: updates }); } catch (error) { console.error('Error updating group:', error); res.status(500).json({ error: 'Internal server error', message: 'Fehler beim Aktualisieren der Gruppe', details: error.message }); } }); router.delete('/groups/:groupId/images/:imageId', async (req, res) => { /* #swagger.tags = ['Admin - Groups Moderation'] #swagger.summary = 'Delete a single image' #swagger.description = 'Deletes a specific image from a group' #swagger.parameters['groupId'] = { in: 'path', required: true, type: 'string', description: 'Group ID', example: 'abc123def456' } #swagger.parameters['imageId'] = { in: 'path', required: true, type: 'integer', description: 'Image ID', example: 42 } #swagger.responses[200] = { description: 'Image deleted successfully', schema: { success: true, message: 'Bild erfolgreich gelöscht', groupId: 'abc123def456', imageId: 42 } } #swagger.responses[404] = { description: 'Image not found' } */ try { const { groupId, imageId } = req.params; const deleted = await GroupRepository.deleteImage(groupId, parseInt(imageId)); if (!deleted) { return res.status(404).json({ error: 'Image not found', message: `Bild mit ID ${imageId} in Gruppe ${groupId} wurde nicht gefunden` }); } res.json({ success: true, message: 'Bild erfolgreich gelöscht', groupId: groupId, imageId: parseInt(imageId) }); } catch (error) { console.error('Error deleting image:', error); res.status(500).json({ error: 'Internal server error', message: 'Fehler beim Löschen des Bildes' }); } }); router.patch('/groups/:groupId/images/batch-description', async (req, res) => { /* #swagger.tags = ['Admin - Groups Moderation'] #swagger.summary = 'Batch update image descriptions' #swagger.description = 'Updates descriptions for multiple images in a group at once' #swagger.parameters['groupId'] = { in: 'path', required: true, type: 'string', description: 'Group ID', example: 'abc123def456' } #swagger.parameters['body'] = { in: 'body', required: true, schema: { descriptions: [ { imageId: 1, description: 'Sonnenuntergang am Strand' }, { imageId: 2, description: 'Gruppenfoto beim Lagerfeuer' } ] } } #swagger.responses[200] = { description: 'Descriptions updated', schema: { success: true, updatedCount: 2, message: '2 Bildbeschreibungen aktualisiert' } } #swagger.responses[400] = { description: 'Invalid request format' } */ try { const { groupId } = req.params; const { descriptions } = req.body; // Validierung if (!Array.isArray(descriptions) || descriptions.length === 0) { return res.status(400).json({ error: 'Invalid request', message: 'descriptions muss ein nicht-leeres Array sein' }); } // Validiere jede Beschreibung for (const desc of descriptions) { if (!desc.imageId || typeof desc.imageId !== 'number') { return res.status(400).json({ error: 'Invalid request', message: 'Jede Beschreibung muss eine gültige imageId enthalten' }); } if (desc.description && desc.description.length > 200) { return res.status(400).json({ error: 'Invalid request', message: `Bildbeschreibung für Bild ${desc.imageId} darf maximal 200 Zeichen lang sein` }); } } const result = await GroupRepository.updateBatchImageDescriptions(groupId, descriptions); res.json({ success: true, message: `${result.updatedImages} Bildbeschreibungen erfolgreich aktualisiert`, groupId: groupId, updatedImages: result.updatedImages }); } catch (error) { console.error('Error batch updating image descriptions:', error); res.status(500).json({ error: 'Internal server error', message: 'Fehler beim Aktualisieren der Bildbeschreibungen', details: error.message }); } }); router.patch('/groups/:groupId/images/:imageId', async (req, res) => { /* #swagger.tags = ['Admin - Groups Moderation'] #swagger.summary = 'Update single image description' #swagger.description = 'Updates description for a specific image (max 200 characters)' #swagger.parameters['groupId'] = { in: 'path', required: true, type: 'string', description: 'Group ID', example: 'abc123def456' } #swagger.parameters['imageId'] = { in: 'path', required: true, type: 'integer', description: 'Image ID', example: 42 } #swagger.parameters['body'] = { in: 'body', required: true, schema: { image_description: 'Sonnenuntergang am Strand' } } #swagger.responses[200] = { description: 'Description updated', schema: { success: true, message: 'Bildbeschreibung erfolgreich aktualisiert', groupId: 'abc123def456', imageId: 42, imageDescription: 'Sonnenuntergang am Strand' } } #swagger.responses[400] = { description: 'Description too long (max 200 chars)' } #swagger.responses[404] = { description: 'Image not found' } */ try { const { groupId, imageId } = req.params; const { image_description } = req.body; // Validierung: Max 200 Zeichen if (image_description && image_description.length > 200) { return res.status(400).json({ error: 'Invalid request', message: 'Bildbeschreibung darf maximal 200 Zeichen lang sein' }); } const updated = await GroupRepository.updateImageDescription( parseInt(imageId), groupId, image_description ); if (!updated) { return res.status(404).json({ error: 'Image not found', message: `Bild mit ID ${imageId} in Gruppe ${groupId} wurde nicht gefunden` }); } res.json({ success: true, message: 'Bildbeschreibung erfolgreich aktualisiert', groupId: groupId, imageId: parseInt(imageId), imageDescription: image_description }); } catch (error) { console.error('Error updating image description:', error); res.status(500).json({ error: 'Internal server error', message: 'Fehler beim Aktualisieren der Bildbeschreibung', details: error.message }); } }); router.delete('/groups/:groupId', async (req, res) => { /* #swagger.tags = ['Admin - Groups Moderation'] #swagger.summary = 'Delete a group' #swagger.description = 'Deletes a complete group including all images and metadata' #swagger.parameters['groupId'] = { in: 'path', required: true, type: 'string', description: 'Group ID', example: 'abc123def456' } #swagger.responses[200] = { description: 'Group deleted successfully', schema: { success: true, message: 'Gruppe erfolgreich gelöscht', groupId: 'abc123def456' } } #swagger.responses[404] = { description: 'Group not found' } */ try { const { groupId } = req.params; // Get group data before deletion for logging const groupData = await GroupRepository.getGroupById(groupId); if (!groupData) { return res.status(404).json({ error: 'Group not found', message: `Gruppe mit ID ${groupId} wurde nicht gefunden` }); } const imageCount = groupData.images ? groupData.images.length : 0; const totalFileSize = groupData.images ? groupData.images.reduce((sum, img) => sum + (img.fileSize || 0), 0) : 0; // Create deletion_log entry BEFORE deleting await DeletionLogRepository.createDeletionEntry({ groupId: groupId, year: groupData.year, imageCount: imageCount, uploadDate: groupData.uploadDate, deletionReason: 'admin_moderation_deletion', totalFileSize: totalFileSize }); // Delete the group await GroupRepository.deleteGroup(groupId); res.json({ success: true, message: 'Gruppe erfolgreich gelöscht', groupId: groupId }); } catch (error) { console.error('Error deleting group:', error); res.status(500).json({ error: 'Internal server error', message: 'Fehler beim Löschen der Gruppe' }); } }); module.exports = router;