Dateien nach "backend/src/routes" hochladen

This commit is contained in:
2026-05-27 09:58:27 +02:00
parent 89f1affc88
commit 552572040c

View File

@@ -1,24 +1,20 @@
const express = require('express'); const express = require('express');
const https = require('https'); const https = require('https');
const http = require('http'); const http = require('http');
const db = require('../db');
const { authenticate } = require('../middleware/auth'); const { authenticate } = require('../middleware/auth');
const router = express.Router(); const router = express.Router();
// Fetch mit Redirect-Unterstützung (bis 5 Hops) // Fetch mit Redirect-Unterstützung
function fetchWithRedirects(url, hops = 0) { function fetchWithRedirects(url, hops = 0) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (hops > 5) return reject(new Error('Zu viele Redirects')); if (hops > 5) return reject(new Error('Zu viele Redirects'));
let parsed; let parsed;
try { parsed = new URL(url); } catch { return reject(new Error('Ungültige URL')); } try { parsed = new URL(url); } catch { return reject(new Error('Ungültige URL')); }
const lib = parsed.protocol === 'https:' ? https : http; const lib = parsed.protocol === 'https:' ? https : http;
const req = lib.get(url, { const req = lib.get(url, {
headers: { headers: { 'User-Agent': 'Mozilla/5.0 DickenDock/1.0', 'Accept': 'text/calendar, text/plain, */*' }
'User-Agent': 'Mozilla/5.0 DickenDock/1.0',
'Accept': 'text/calendar, text/plain, */*',
}
}, res => { }, res => {
// Redirect folgen
if ([301,302,303,307,308].includes(res.statusCode) && res.headers.location) { if ([301,302,303,307,308].includes(res.statusCode) && res.headers.location) {
const next = res.headers.location.startsWith('http') const next = res.headers.location.startsWith('http')
? res.headers.location ? res.headers.location
@@ -26,59 +22,48 @@ function fetchWithRedirects(url, hops = 0) {
res.destroy(); res.destroy();
return fetchWithRedirects(next, hops + 1).then(resolve).catch(reject); return fetchWithRedirects(next, hops + 1).then(resolve).catch(reject);
} }
if (res.statusCode !== 200) if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode}`));
return reject(new Error(`HTTP ${res.statusCode}`));
let data = ''; let data = '';
res.setEncoding('utf8'); res.setEncoding('utf8');
res.on('data', d => { res.on('data', d => { data += d; if (data.length > 3*1024*1024) { res.destroy(); reject(new Error('Feed zu groß')); } });
data += d;
if (data.length > 3 * 1024 * 1024) { res.destroy(); reject(new Error('Feed zu groß (max 3MB)')); }
});
res.on('end', () => resolve(data)); res.on('end', () => resolve(data));
}); });
req.on('error', reject); req.on('error', reject);
req.setTimeout(10000, () => { req.destroy(); reject(new Error('Timeout nach 10s')); }); req.setTimeout(10000, () => { req.destroy(); reject(new Error('Timeout')); });
}); });
} }
// GET /fetch?url=...
router.get('/fetch', authenticate, async (req, res) => { router.get('/fetch', authenticate, async (req, res) => {
const { url } = req.query; const { url } = req.query;
if (!url) return res.status(400).json({ error: 'URL fehlt' }); if (!url) return res.status(400).json({ error: 'URL fehlt' });
try { try { new URL(url); } catch { return res.status(400).json({ error: 'Ungültige URL' }); }
new URL(url); // validate
} catch { return res.status(400).json({ error: 'Ungültige URL' }); }
try { try {
const data = await fetchWithRedirects(url); const data = await fetchWithRedirects(url);
// Prüfen ob es wirklich iCal ist
if (!data.includes('BEGIN:VCALENDAR')) if (!data.includes('BEGIN:VCALENDAR'))
return res.status(502).json({ error: 'Kein gültiger iCal-Feed (BEGIN:VCALENDAR fehlt)' }); return res.status(502).json({ error: 'Kein gültiger iCal-Feed' });
res.type('text/calendar; charset=utf-8').send(data); res.type('text/calendar; charset=utf-8').send(data);
} catch(e) { } catch(e) { res.status(502).json({ error: e.message }); }
res.status(502).json({ error: e.message });
}
}); });
module.exports = router; // GET /feeds
router.get('/feeds', authenticate, (req, res) => {
// ── iCal Feed CRUD (gespeichert in DB statt localStorage) ────────────────────
const db = require('../db');
const { authenticate: auth } = require('../middleware/auth');
router.get('/feeds', auth, (req, res) => {
res.json(db.prepare('SELECT * FROM calendar_feeds WHERE user_id=? ORDER BY id').all(req.user.id)); res.json(db.prepare('SELECT * FROM calendar_feeds WHERE user_id=? ORDER BY id').all(req.user.id));
}); });
router.post('/feeds', auth, (req, res) => { // POST /feeds
router.post('/feeds', authenticate, (req, res) => {
const { name, url, color='#4ecdc4' } = req.body; const { name, url, color='#4ecdc4' } = req.body;
if (!name?.trim() || !url?.trim()) return res.status(400).json({ error: 'Name und URL erforderlich' }); if (!name?.trim() || !url?.trim()) return res.status(400).json({ error: 'Name und URL erforderlich' });
const r = db.prepare('INSERT INTO calendar_feeds (user_id,name,url,color) VALUES (?,?,?,?)').run(req.user.id, name.trim(), url.trim(), color); const r = db.prepare('INSERT INTO calendar_feeds (user_id,name,url,color) VALUES (?,?,?,?)').run(req.user.id, name.trim(), url.trim(), color);
res.json(db.prepare('SELECT * FROM calendar_feeds WHERE id=?').get(r.lastInsertRowid)); res.json(db.prepare('SELECT * FROM calendar_feeds WHERE id=?').get(r.lastInsertRowid));
}); });
router.delete('/feeds/:id', auth, (req, res) => { // DELETE /feeds/:id
router.delete('/feeds/:id', authenticate, (req, res) => {
const r = db.prepare('DELETE FROM calendar_feeds WHERE id=? AND user_id=?').run(req.params.id, req.user.id); const r = db.prepare('DELETE FROM calendar_feeds WHERE id=? AND user_id=?').run(req.params.id, req.user.id);
if (!r.changes) return res.status(404).json({ error: 'Nicht gefunden' }); if (!r.changes) return res.status(404).json({ error: 'Nicht gefunden' });
res.json({ success: true }); res.json({ success: true });
}); });
module.exports = router;