Files
TREK/server/src/routes/photos.js
T
Maurice d845057f84 Security hardening, backup restore fix & restore warning modal
- 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
2026-03-21 15:09:41 +01:00

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;