mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
d845057f84
- Fix backup restore: try/finally ensures DB always reopens after closeDb - Fix EBUSY on uploads during restore (in-place overwrite instead of rmSync) - Add DB proxy null guard for clearer errors during restore window - Add red warning modal before backup restore (DE/EN, dark mode support) - JWT secret: empty docker-compose default so auto-generation kicks in - OIDC: pass token via URL fragment instead of query param (no server logs) - Block SVG uploads on photos, files and covers (stored XSS prevention) - Add helmet for security headers (HSTS, X-Frame, nosniff, etc.) - Explicit express.json body size limit (100kb) - Fix XSS in Leaflet map markers (escape image_url in HTML) - Remove verbose WebSocket debug logging from client
168 lines
4.9 KiB
JavaScript
168 lines
4.9 KiB
JavaScript
const express = require('express');
|
|
const multer = require('multer');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const { v4: uuidv4 } = require('uuid');
|
|
const { db, canAccessTrip } = require('../db/database');
|
|
const { authenticate, demoUploadBlock } = require('../middleware/auth');
|
|
|
|
const router = express.Router({ mergeParams: true });
|
|
|
|
const photosDir = path.join(__dirname, '../../uploads/photos');
|
|
|
|
const storage = multer.diskStorage({
|
|
destination: (req, file, cb) => {
|
|
if (!fs.existsSync(photosDir)) fs.mkdirSync(photosDir, { recursive: true });
|
|
cb(null, photosDir);
|
|
},
|
|
filename: (req, file, cb) => {
|
|
const ext = path.extname(file.originalname);
|
|
cb(null, `${uuidv4()}${ext}`);
|
|
},
|
|
});
|
|
|
|
const upload = multer({
|
|
storage,
|
|
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
|
|
fileFilter: (req, file, cb) => {
|
|
const ext = path.extname(file.originalname).toLowerCase();
|
|
const allowedExts = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
|
|
if (file.mimetype.startsWith('image/') && !file.mimetype.includes('svg') && allowedExts.includes(ext)) {
|
|
cb(null, true);
|
|
} else {
|
|
cb(new Error('Only jpg, png, gif, webp images allowed'));
|
|
}
|
|
},
|
|
});
|
|
|
|
function formatPhoto(photo) {
|
|
return {
|
|
...photo,
|
|
url: `/uploads/photos/${photo.filename}`,
|
|
};
|
|
}
|
|
|
|
// GET /api/trips/:tripId/photos
|
|
router.get('/', authenticate, (req, res) => {
|
|
const { tripId } = req.params;
|
|
const { day_id, place_id } = req.query;
|
|
|
|
const trip = canAccessTrip(tripId, req.user.id);
|
|
if (!trip) return res.status(404).json({ error: 'Reise nicht gefunden' });
|
|
|
|
let query = 'SELECT * FROM photos WHERE trip_id = ?';
|
|
const params = [tripId];
|
|
|
|
if (day_id) {
|
|
query += ' AND day_id = ?';
|
|
params.push(day_id);
|
|
}
|
|
|
|
if (place_id) {
|
|
query += ' AND place_id = ?';
|
|
params.push(place_id);
|
|
}
|
|
|
|
query += ' ORDER BY created_at DESC';
|
|
|
|
const photos = db.prepare(query).all(...params);
|
|
res.json({ photos: photos.map(formatPhoto) });
|
|
});
|
|
|
|
// POST /api/trips/:tripId/photos
|
|
router.post('/', authenticate, demoUploadBlock, upload.array('photos', 20), (req, res) => {
|
|
const { tripId } = req.params;
|
|
const { day_id, place_id, caption } = req.body;
|
|
|
|
const trip = canAccessTrip(tripId, req.user.id);
|
|
if (!trip) {
|
|
// Delete uploaded files on auth failure
|
|
if (req.files) req.files.forEach(f => fs.unlinkSync(f.path));
|
|
return res.status(404).json({ error: 'Reise nicht gefunden' });
|
|
}
|
|
|
|
if (!req.files || req.files.length === 0) {
|
|
return res.status(400).json({ error: 'Keine Dateien hochgeladen' });
|
|
}
|
|
|
|
const insertPhoto = db.prepare(`
|
|
INSERT INTO photos (trip_id, day_id, place_id, filename, original_name, file_size, mime_type, caption)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`);
|
|
|
|
const photos = [];
|
|
db.exec('BEGIN');
|
|
try {
|
|
for (const file of req.files) {
|
|
const result = insertPhoto.run(
|
|
tripId,
|
|
day_id || null,
|
|
place_id || null,
|
|
file.filename,
|
|
file.originalname,
|
|
file.size,
|
|
file.mimetype,
|
|
caption || null
|
|
);
|
|
const photo = db.prepare('SELECT * FROM photos WHERE id = ?').get(result.lastInsertRowid);
|
|
photos.push(formatPhoto(photo));
|
|
}
|
|
db.exec('COMMIT');
|
|
} catch (e) {
|
|
db.exec('ROLLBACK');
|
|
throw e;
|
|
}
|
|
|
|
res.status(201).json({ photos });
|
|
});
|
|
|
|
// PUT /api/trips/:tripId/photos/:id
|
|
router.put('/:id', authenticate, (req, res) => {
|
|
const { tripId, id } = req.params;
|
|
const { caption, day_id, place_id } = req.body;
|
|
|
|
const trip = canAccessTrip(tripId, req.user.id);
|
|
if (!trip) return res.status(404).json({ error: 'Reise nicht gefunden' });
|
|
|
|
const photo = db.prepare('SELECT * FROM photos WHERE id = ? AND trip_id = ?').get(id, tripId);
|
|
if (!photo) return res.status(404).json({ error: 'Foto nicht gefunden' });
|
|
|
|
db.prepare(`
|
|
UPDATE photos SET
|
|
caption = COALESCE(?, caption),
|
|
day_id = ?,
|
|
place_id = ?
|
|
WHERE id = ?
|
|
`).run(
|
|
caption !== undefined ? caption : photo.caption,
|
|
day_id !== undefined ? (day_id || null) : photo.day_id,
|
|
place_id !== undefined ? (place_id || null) : photo.place_id,
|
|
id
|
|
);
|
|
|
|
const updated = db.prepare('SELECT * FROM photos WHERE id = ?').get(id);
|
|
res.json({ photo: formatPhoto(updated) });
|
|
});
|
|
|
|
// DELETE /api/trips/:tripId/photos/:id
|
|
router.delete('/:id', authenticate, (req, res) => {
|
|
const { tripId, id } = req.params;
|
|
|
|
const trip = canAccessTrip(tripId, req.user.id);
|
|
if (!trip) return res.status(404).json({ error: 'Reise nicht gefunden' });
|
|
|
|
const photo = db.prepare('SELECT * FROM photos WHERE id = ? AND trip_id = ?').get(id, tripId);
|
|
if (!photo) return res.status(404).json({ error: 'Foto nicht gefunden' });
|
|
|
|
// Delete file
|
|
const filePath = path.join(photosDir, photo.filename);
|
|
if (fs.existsSync(filePath)) {
|
|
try { fs.unlinkSync(filePath); } catch (e) { console.error('Error deleting photo file:', e); }
|
|
}
|
|
|
|
db.prepare('DELETE FROM photos WHERE id = ?').run(id);
|
|
res.json({ success: true });
|
|
});
|
|
|
|
module.exports = router;
|