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
This commit is contained in:
parent
901ecc7633
commit
c18c258135
|
|
@ -23,7 +23,8 @@
|
|||
"node-cron": "^4.2.1",
|
||||
"sharp": "^0.34.4",
|
||||
"shortid": "^2.2.16",
|
||||
"sqlite3": "^5.1.7"
|
||||
"sqlite3": "^5.1.7",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^6.0.0",
|
||||
|
|
|
|||
|
|
@ -520,17 +520,19 @@ class GroupRepository {
|
|||
async createGroupWithConsent(groupData, workshopConsent, socialMediaConsents = []) {
|
||||
const SocialMediaRepository = require('./SocialMediaRepository');
|
||||
const socialMediaRepo = new SocialMediaRepository(dbManager);
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
return await dbManager.transaction(async (db) => {
|
||||
const consentTimestamp = new Date().toISOString();
|
||||
const managementToken = uuidv4(); // Generate UUID v4 token
|
||||
|
||||
// Füge Gruppe mit Consent-Feldern hinzu
|
||||
// Füge Gruppe mit Consent-Feldern und Management-Token hinzu
|
||||
await db.run(`
|
||||
INSERT INTO groups (
|
||||
group_id, year, title, description, name, upload_date, approved,
|
||||
display_in_workshop, consent_timestamp
|
||||
display_in_workshop, consent_timestamp, management_token
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, [
|
||||
groupData.groupId,
|
||||
groupData.year,
|
||||
|
|
@ -540,7 +542,8 @@ class GroupRepository {
|
|||
groupData.uploadDate,
|
||||
groupData.approved || false,
|
||||
workshopConsent ? 1 : 0,
|
||||
consentTimestamp
|
||||
consentTimestamp,
|
||||
managementToken
|
||||
]);
|
||||
|
||||
// Füge Bilder hinzu
|
||||
|
|
@ -575,7 +578,10 @@ class GroupRepository {
|
|||
);
|
||||
}
|
||||
|
||||
return groupData.groupId;
|
||||
return {
|
||||
groupId: groupData.groupId,
|
||||
managementToken: managementToken
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -787,6 +793,63 @@ class GroupRepository {
|
|||
const socialMediaRepo = new SocialMediaRepository(dbManager);
|
||||
return await socialMediaRepo.getConsentsForGroup(groupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hole Gruppe mit allen Daten (Bilder + Consents) per Management Token
|
||||
* Für Self-Service Management Portal
|
||||
* @param {string} managementToken - UUID v4 Management Token
|
||||
* @returns {Promise<Object|null>} Gruppe mit Bildern, Workshop-Consent und Social Media Consents
|
||||
*/
|
||||
async getGroupByManagementToken(managementToken) {
|
||||
// Hole Gruppe
|
||||
const group = await dbManager.get(`
|
||||
SELECT * FROM groups WHERE management_token = ?
|
||||
`, [managementToken]);
|
||||
|
||||
if (!group) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Hole Bilder
|
||||
const images = await dbManager.all(`
|
||||
SELECT * FROM images
|
||||
WHERE group_id = ?
|
||||
ORDER BY upload_order ASC
|
||||
`, [group.group_id]);
|
||||
|
||||
// Hole Social Media Consents
|
||||
const SocialMediaRepository = require('./SocialMediaRepository');
|
||||
const socialMediaRepo = new SocialMediaRepository(dbManager);
|
||||
const socialMediaConsents = await socialMediaRepo.getConsentsForGroup(group.group_id);
|
||||
|
||||
return {
|
||||
groupId: group.group_id,
|
||||
year: group.year,
|
||||
title: group.title,
|
||||
description: group.description,
|
||||
name: group.name,
|
||||
uploadDate: group.upload_date,
|
||||
approved: group.approved,
|
||||
// Workshop consent
|
||||
displayInWorkshop: group.display_in_workshop,
|
||||
consentTimestamp: group.consent_timestamp,
|
||||
// Images
|
||||
images: images.map(img => ({
|
||||
id: img.id,
|
||||
fileName: img.file_name,
|
||||
originalName: img.original_name,
|
||||
filePath: img.file_path,
|
||||
previewPath: img.preview_path,
|
||||
uploadOrder: img.upload_order,
|
||||
fileSize: img.file_size,
|
||||
mimeType: img.mime_type,
|
||||
imageDescription: img.image_description
|
||||
})),
|
||||
imageCount: images.length,
|
||||
// Social Media Consents
|
||||
socialMediaConsents: socialMediaConsents || []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new GroupRepository();
|
||||
|
|
@ -112,7 +112,7 @@ router.post(endpoints.UPLOAD_BATCH, async (req, res) => {
|
|||
});
|
||||
|
||||
// Speichere Gruppe mit Consents in SQLite
|
||||
await groupRepository.createGroupWithConsent({
|
||||
const createResult = await groupRepository.createGroupWithConsent({
|
||||
groupId: group.groupId,
|
||||
year: group.year,
|
||||
title: group.title,
|
||||
|
|
@ -148,9 +148,10 @@ router.post(endpoints.UPLOAD_BATCH, async (req, res) => {
|
|||
|
||||
console.log(`Successfully saved group ${group.groupId} with ${files.length} images to database`);
|
||||
|
||||
// Erfolgreiche Antwort
|
||||
// Erfolgreiche Antwort mit Management-Token
|
||||
res.json({
|
||||
groupId: group.groupId,
|
||||
managementToken: createResult.managementToken,
|
||||
message: 'Batch upload successful',
|
||||
imageCount: files.length,
|
||||
year: group.year,
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ const migrationRouter = require('./migration');
|
|||
const reorderRouter = require('./reorder');
|
||||
const adminRouter = require('./admin');
|
||||
const consentRouter = require('./consent');
|
||||
const managementRouter = require('./management');
|
||||
|
||||
const renderRoutes = (app) => {
|
||||
[uploadRouter, downloadRouter, batchUploadRouter, groupsRouter, migrationRouter, consentRouter].forEach(router => app.use('/', router));
|
||||
app.use('/groups', reorderRouter);
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use('/api/manage', managementRouter);
|
||||
};
|
||||
|
||||
module.exports = { renderRoutes };
|
||||
643
backend/src/routes/management.js
Normal file
643
backend/src/routes/management.js
Normal file
|
|
@ -0,0 +1,643 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const groupRepository = require('../repositories/GroupRepository');
|
||||
const deletionLogRepository = require('../repositories/DeletionLogRepository');
|
||||
const dbManager = require('../database/DatabaseManager');
|
||||
|
||||
// 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) => {
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
// Return complete group data
|
||||
res.json({
|
||||
success: true,
|
||||
data: groupData
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error validating management token:', error);
|
||||
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) => {
|
||||
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]
|
||||
);
|
||||
|
||||
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') {
|
||||
await socialMediaRepo.revokeConsent(groupData.groupId, platformId);
|
||||
} else {
|
||||
await socialMediaRepo.restoreConsent(groupData.groupId, platformId);
|
||||
}
|
||||
|
||||
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/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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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.logDeletion({
|
||||
groupId: groupId,
|
||||
deletedBy: 'user_self_service',
|
||||
reason: 'User-initiated deletion via management token',
|
||||
deletionDate: new Date().toISOString(),
|
||||
imageCount: imageCount
|
||||
});
|
||||
|
||||
console.log(`✓ Group ${groupId} deleted via management token (${imageCount} images)`);
|
||||
|
||||
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'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Loading…
Reference in New Issue
Block a user