Backend Management API implementation for self-service user portal: ✅ Task 2: Token Generation (already implemented in Phase 1) - UUID v4 generated at upload - Stored in groups.management_token - Returned in upload response ✅ Task 3: Token Validation API - GET /api/manage/:token - Validates token and loads complete group data - Returns group with images, consents, metadata - 404 for invalid/missing tokens ✅ Task 4: Consent Revocation API - PUT /api/manage/:token/consents - Revoke/restore workshop consent - Revoke/restore social media platform consents - Sets revoked=1, revoked_timestamp - Full error handling and validation ✅ Task 5: Metadata Edit API - PUT /api/manage/:token/metadata - Update title, description, name - Supports partial updates - Automatically sets approved=0 (returns to moderation) ✅ Task 6: Add Images API - POST /api/manage/:token/images - Upload new images to existing group - Calculates correct upload_order - Sets approved=0 on changes - Max 50 images per group validation - Preview generation support ✅ Task 7: Delete Image API - DELETE /api/manage/:token/images/:imageId - Deletes original and preview files - Removes DB entry - Sets approved=0 if group was approved - Prevents deletion of last image ⏳ Task 8: Delete Group API (in progress) - DELETE /api/manage/:token route created - Integration with existing GroupRepository.deleteGroup - Needs testing Technical Changes: - Created backend/src/routes/management.js - Added getGroupByManagementToken() to GroupRepository - Registered /api/manage routes in index.js - Installed uuid package for token generation - All routes use token validation helper - Docker-only development workflow Tested Features: - Token validation with real uploads - Workshop consent revoke/restore - Social media consent management - Metadata updates (full and partial) - Image upload with multipart/form-data - Image deletion with file cleanup - Error handling and edge cases
175 lines
6.9 KiB
JavaScript
175 lines
6.9 KiB
JavaScript
const generateId = require("shortid");
|
|
const express = require('express');
|
|
const { Router } = require('express');
|
|
const path = require('path');
|
|
const { endpoints } = require('../constants');
|
|
const UploadGroup = require('../models/uploadGroup');
|
|
const groupRepository = require('../repositories/GroupRepository');
|
|
const dbManager = require('../database/DatabaseManager');
|
|
const ImagePreviewService = require('../services/ImagePreviewService');
|
|
|
|
const router = Router();
|
|
|
|
// Batch-Upload für mehrere Bilder
|
|
router.post(endpoints.UPLOAD_BATCH, async (req, res) => {
|
|
try {
|
|
// Überprüfe ob Dateien hochgeladen wurden
|
|
if (!req.files || !req.files.images) {
|
|
return res.status(400).json({
|
|
error: 'No images uploaded',
|
|
message: 'Keine Bilder wurden hochgeladen'
|
|
});
|
|
}
|
|
|
|
// Metadaten aus dem Request body
|
|
let metadata = {};
|
|
let descriptions = [];
|
|
let consents = {};
|
|
try {
|
|
metadata = req.body.metadata ? JSON.parse(req.body.metadata) : {};
|
|
descriptions = req.body.descriptions ? JSON.parse(req.body.descriptions) : [];
|
|
consents = req.body.consents ? JSON.parse(req.body.consents) : {};
|
|
} catch (e) {
|
|
console.error('Error parsing metadata/descriptions/consents:', e);
|
|
metadata = { description: req.body.description || "" };
|
|
descriptions = [];
|
|
consents = {};
|
|
}
|
|
|
|
// Validiere Workshop Consent (Pflichtfeld)
|
|
if (!consents.workshopConsent) {
|
|
return res.status(400).json({
|
|
error: 'Workshop consent required',
|
|
message: 'Die Zustimmung zur Anzeige in der Werkstatt ist erforderlich'
|
|
});
|
|
}
|
|
|
|
// Erstelle neue Upload-Gruppe mit erweiterten Metadaten
|
|
const group = new UploadGroup(metadata);
|
|
|
|
// Handle sowohl einzelne Datei als auch Array von Dateien
|
|
const files = Array.isArray(req.files.images) ? req.files.images : [req.files.images];
|
|
|
|
console.log(`Processing ${files.length} files for batch upload`);
|
|
|
|
// Verarbeite alle Dateien
|
|
const processedFiles = [];
|
|
for (let i = 0; i < files.length; i++) {
|
|
const file = files[i];
|
|
|
|
// Generiere eindeutigen Dateinamen
|
|
const fileEnding = file.name.split(".").pop();
|
|
const fileName = generateId() + '.' + fileEnding;
|
|
|
|
// Speichere Datei unter data/images
|
|
const path = require('path');
|
|
const { UPLOAD_FS_DIR } = require('../constants');
|
|
const uploadPath = path.join(__dirname, '..', UPLOAD_FS_DIR, fileName);
|
|
file.mv(uploadPath, (err) => {
|
|
if (err) {
|
|
console.error('Error saving file:', err);
|
|
}
|
|
});
|
|
|
|
// Füge Bild zur Gruppe hinzu
|
|
group.addImage(fileName, file.name, i + 1);
|
|
processedFiles.push({
|
|
fileName,
|
|
originalName: file.name,
|
|
size: file.size
|
|
});
|
|
}
|
|
|
|
// Generate previews for all uploaded images asynchronously
|
|
const previewDir = path.join(__dirname, '..', require('../constants').PREVIEW_FS_DIR);
|
|
const uploadDir = path.join(__dirname, '..', require('../constants').UPLOAD_FS_DIR);
|
|
|
|
// Generate previews in background (don't wait)
|
|
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`);
|
|
|
|
// Update preview_path in database for successful previews
|
|
results.forEach(async (result) => {
|
|
if (result.success) {
|
|
try {
|
|
await dbManager.run(`
|
|
UPDATE images
|
|
SET preview_path = ?
|
|
WHERE group_id = ? AND file_name = ?
|
|
`, [result.previewPath, group.groupId, result.fileName]);
|
|
} catch (err) {
|
|
console.error(`Failed to update preview_path for ${result.fileName}:`, err);
|
|
}
|
|
}
|
|
});
|
|
}).catch(err => {
|
|
console.error('Preview generation failed:', err);
|
|
});
|
|
|
|
// Speichere Gruppe mit Consents in SQLite
|
|
const createResult = await groupRepository.createGroupWithConsent({
|
|
groupId: group.groupId,
|
|
year: group.year,
|
|
title: group.title,
|
|
description: group.description,
|
|
name: group.name,
|
|
uploadDate: group.uploadDate,
|
|
images: processedFiles.map((file, index) => {
|
|
// Finde passende Beschreibung für dieses Bild (match by fileName or originalName)
|
|
const descObj = descriptions.find(d =>
|
|
d.fileName === file.originalName || d.fileName === file.fileName
|
|
);
|
|
const imageDescription = descObj ? descObj.description : null;
|
|
|
|
// Validierung: Max 200 Zeichen
|
|
if (imageDescription && imageDescription.length > 200) {
|
|
console.warn(`Image description for ${file.originalName} exceeds 200 characters, truncating`);
|
|
}
|
|
|
|
return {
|
|
fileName: file.fileName,
|
|
originalName: file.originalName,
|
|
filePath: `/upload/${file.fileName}`,
|
|
uploadOrder: index + 1,
|
|
fileSize: file.size,
|
|
mimeType: files[index].mimetype,
|
|
imageDescription: imageDescription ? imageDescription.slice(0, 200) : null
|
|
};
|
|
})
|
|
},
|
|
consents.workshopConsent,
|
|
consents.socialMediaConsents || []
|
|
);
|
|
|
|
console.log(`Successfully saved group ${group.groupId} with ${files.length} images to database`);
|
|
|
|
// Erfolgreiche Antwort mit Management-Token
|
|
res.json({
|
|
groupId: group.groupId,
|
|
managementToken: createResult.managementToken,
|
|
message: 'Batch upload successful',
|
|
imageCount: files.length,
|
|
year: group.year,
|
|
title: group.title,
|
|
description: group.description,
|
|
name: group.name,
|
|
uploadDate: group.uploadDate,
|
|
files: processedFiles
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Batch upload error:', error);
|
|
res.status(500).json({
|
|
error: 'Internal server error',
|
|
message: 'Ein Fehler ist beim Upload aufgetreten',
|
|
details: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
module.exports = router; |