- replace bearer auth with session+CSRF flow and add admin user directory - update frontend moderation flow, force password change gate, and new CLI - refresh changelog/docs/feature plan + ensure swagger dev experience
1051 lines
35 KiB
JavaScript
1051 lines
35 KiB
JavaScript
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;
|