Project-Image-Uploader/backend/src/routes/batchUpload.js
matthias.lotz c18c258135 feat(phase2): Implement Management Portal API (Tasks 2-7)
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
2025-11-10 20:00:54 +01:00

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;