diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js index 30bef64..b14d496 100644 --- a/backend/src/routes/admin.js +++ b/backend/src/routes/admin.js @@ -170,4 +170,60 @@ router.put('/users/:id/hidden', authenticate, requireAdmin, (req, res) => { res.json({ hidden: newHidden }); }); +// ── WebDAV / Synology NAS Einstellungen ─────────────────────────────────── +router.get('/webdav', authenticate, requireAdmin, (req, res) => { + const get = k => db.prepare('SELECT value FROM admin_settings WHERE key=?').get(k)?.value || ''; + res.json({ + url: get('webdav_url'), + user: get('webdav_user'), + password: get('webdav_password'), + basePath: get('webdav_base_path') || '/dickendock', + enabled: get('webdav_enabled') === '1', + }); +}); + +router.put('/webdav', authenticate, requireAdmin, (req, res) => { + const { url, user, password, basePath, enabled } = req.body; + const set = (k, v) => db.prepare('INSERT OR REPLACE INTO admin_settings (key,value) VALUES (?,?)').run(k, v || ''); + set('webdav_url', url); + set('webdav_user', user); + set('webdav_password', password); + set('webdav_base_path', basePath || '/dickendock'); + set('webdav_enabled', enabled ? '1' : '0'); + res.json({ ok: true }); +}); + +router.post('/webdav/test', authenticate, requireAdmin, async (req, res) => { + // Temporär die gesendeten Werte zum Testen nutzen (noch nicht gespeichert) + const { url, user, password, basePath } = req.body; + try { + const https = require('https'); + const http = require('http'); + const base = new URL(url); + const testPath = (base.pathname.replace(/\/$/, '') + (basePath || '/dickendock')).replace(/\/+/g, '/'); + const mod = base.protocol === 'https:' ? https : http; + const auth = 'Basic ' + Buffer.from(`${user}:${password}`).toString('base64'); + + await new Promise((resolve, reject) => { + const req2 = mod.request({ + hostname: base.hostname, + port: base.port || (base.protocol === 'https:' ? 443 : 80), + path: testPath, + method: 'PROPFIND', + headers: { 'Authorization': auth, 'Depth': '0', 'Content-Type': 'application/xml' }, + rejectUnauthorized: false, + timeout: 8000, + }, r => { r.resume(); r.statusCode < 400 || r.statusCode === 404 ? resolve(r.statusCode) : reject(new Error(`HTTP ${r.statusCode}`)); }); + req2.on('error', reject); + req2.on('timeout', () => { req2.destroy(); reject(new Error('Timeout')); }); + req2.write(''); + req2.end(); + }); + + res.json({ ok: true }); + } catch(e) { + res.status(502).json({ error: e.message }); + } +}); + module.exports = router; diff --git a/backend/src/tools/dateien/routes.js b/backend/src/tools/dateien/routes.js index c9d7204..b455c69 100644 --- a/backend/src/tools/dateien/routes.js +++ b/backend/src/tools/dateien/routes.js @@ -8,13 +8,17 @@ const { authenticate, requireAdmin } = require('../../middleware/auth'); const router = express.Router(); const UPLOAD_DIR = process.env.UPLOAD_DIR || '/data/uploads'; if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true }); +const webdav = require('./webdav'); const getSetting = key => db.prepare('SELECT value FROM admin_settings WHERE key=?').get(key)?.value; -const storage = multer.diskStorage({ +const diskStorage = multer.diskStorage({ destination: UPLOAD_DIR, filename: (req, file, cb) => cb(null, `${Date.now()}-${Math.random().toString(36).slice(2)}${path.extname(file.originalname)}`), }); +const memStorage = multer.memoryStorage(); + +const storage = diskStorage; // Standard: Disk const getUpload = () => multer({ storage, limits: { fileSize: parseInt(getSetting('file_max_size_mb') || '50') * 1024 * 1024 }, @@ -194,7 +198,19 @@ router.post('/', authenticate, (req, res) => { const folderId = req.body.folder_id ? parseInt(req.body.folder_id) : null; const r = db.prepare('INSERT INTO files (user_id,filename,originalname,mimetype,size,folder_id) VALUES (?,?,?,?,?,?)') .run(uid, req.file.filename, req.file.originalname, req.file.mimetype||'application/octet-stream', req.file.size, folderId); - res.json(db.prepare('SELECT f.*,u.username as owner FROM files f JOIN users u ON u.id=f.user_id WHERE f.id=?').get(r.lastInsertRowid)); + const newFile = db.prepare('SELECT f.*,u.username as owner FROM files f JOIN users u ON u.id=f.user_id WHERE f.id=?').get(r.lastInsertRowid); + + // WebDAV: Datei asynchron spiegeln (Fehler ignorieren, lokale Kopie bleibt) + const cfg = webdav.getConfig(); + if (cfg.enabled) { + const user = db.prepare('SELECT username FROM users WHERE id=?').get(uid); + const davPath = webdav.userPath(cfg, user.username) + '/' + req.file.originalname; + fs.readFile(path.join(UPLOAD_DIR, req.file.filename), (err, buf) => { + if (!err) webdav.uploadFile(buf, davPath, req.file.mimetype).catch(e => console.error('[webdav upload]', e.message)); + }); + } + + res.json(newFile); }); }); diff --git a/backend/src/tools/dateien/webdav.js b/backend/src/tools/dateien/webdav.js new file mode 100644 index 0000000..49d9537 --- /dev/null +++ b/backend/src/tools/dateien/webdav.js @@ -0,0 +1,165 @@ +/** + * WebDAV Storage Backend für Synology NAS + * Abstrahiert alle Datei-Operationen – lokal oder WebDAV + */ +const https = require('https'); +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const db = require('../../db'); + +function getSetting(key) { + return db.prepare('SELECT value FROM admin_settings WHERE key=?').get(key)?.value || ''; +} + +function getConfig() { + return { + url: getSetting('webdav_url'), // z.B. http://192.168.1.100:5005 + user: getSetting('webdav_user'), + password: getSetting('webdav_password'), + basePath: getSetting('webdav_base_path') || '/dickendock', // z.B. /dickendock + enabled: getSetting('webdav_enabled') === '1', + }; +} + +function authHeader(cfg) { + return 'Basic ' + Buffer.from(`${cfg.user}:${cfg.password}`).toString('base64'); +} + +// WebDAV Request ausführen +function davRequest(method, davPath, opts = {}) { + return new Promise((resolve, reject) => { + const cfg = getConfig(); + const base = new URL(cfg.url); + const fullPath = base.pathname.replace(/\/$/, '') + davPath; + const mod = base.protocol === 'https:' ? https : http; + + const headers = { + 'Authorization': authHeader(cfg), + 'Accept': '*/*', + ...opts.headers, + }; + + const reqOpts = { + hostname: base.hostname, + port: base.port || (base.protocol === 'https:' ? 443 : 80), + path: fullPath, + method, + headers, + rejectUnauthorized: false, // Synology Self-Signed Certs + }; + + if (opts.body) { + headers['Content-Length'] = Buffer.byteLength(opts.body); + } + + const req = mod.request(reqOpts, res => { + let data = ''; + res.setEncoding('binary'); + res.on('data', d => data += d); + res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: data })); + }); + + req.on('error', reject); + if (opts.body) req.write(opts.body); + req.end(); + }); +} + +// ── Öffentliche API ──────────────────────────────────────────────────────── + +/** + * Ordner auf WebDAV anlegen (inkl. alle Parent-Ordner) + */ +async function mkdirp(davPath) { + const parts = davPath.split('/').filter(Boolean); + let current = ''; + for (const part of parts) { + current += '/' + part; + const r = await davRequest('MKCOL', current); + if (r.status !== 201 && r.status !== 405) { // 405 = schon vorhanden + throw new Error(`MKCOL ${current} → ${r.status}`); + } + } +} + +/** + * User-Basispfad auf WebDAV: /dickendock/{username} + */ +function userPath(cfg, username) { + return cfg.basePath.replace(/\/$/, '') + '/' + username.replace(/[^a-zA-Z0-9_-]/g, '_'); +} + +/** + * Datei auf WebDAV hochladen + * @param {Buffer} buffer - Dateiinhalt + * @param {string} davPath - Zielpfad auf WebDAV + * @param {string} mimeType + */ +async function uploadFile(buffer, davPath, mimeType = 'application/octet-stream') { + await mkdirp(davPath.split('/').slice(0, -1).join('/')); + const r = await new Promise((resolve, reject) => { + const cfg = getConfig(); + const base = new URL(cfg.url); + const fullPath = base.pathname.replace(/\/$/, '') + davPath; + const mod = base.protocol === 'https:' ? https : http; + + const headers = { + 'Authorization': authHeader(cfg), + 'Content-Type': mimeType, + 'Content-Length': buffer.length, + }; + + const req = mod.request({ + hostname: base.hostname, + port: base.port || (base.protocol === 'https:' ? 443 : 80), + path: fullPath, + method: 'PUT', + headers, + rejectUnauthorized: false, + }, res => { res.resume(); resolve({ status: res.statusCode }); }); + + req.on('error', reject); + req.write(buffer); + req.end(); + }); + + if (r.status !== 201 && r.status !== 204) { + throw new Error(`PUT ${davPath} → ${r.status}`); + } +} + +/** + * Datei von WebDAV downloaden (gibt Buffer zurück) + */ +async function downloadFile(davPath) { + const r = await davRequest('GET', davPath); + if (r.status !== 200) throw new Error(`GET ${davPath} → ${r.status}`); + return Buffer.from(r.body, 'binary'); +} + +/** + * Datei/Ordner auf WebDAV löschen + */ +async function deleteFile(davPath) { + const r = await davRequest('DELETE', davPath); + if (r.status !== 204 && r.status !== 200 && r.status !== 404) { + throw new Error(`DELETE ${davPath} → ${r.status}`); + } +} + +/** + * Verbindung testen + */ +async function testConnection() { + const cfg = getConfig(); + if (!cfg.url || !cfg.user || !cfg.password) throw new Error('WebDAV nicht konfiguriert'); + const r = await davRequest('PROPFIND', cfg.basePath || '/', { + headers: { 'Depth': '0', 'Content-Type': 'application/xml' }, + body: '', + }); + if (r.status !== 207 && r.status !== 200) throw new Error(`Verbindung fehlgeschlagen: HTTP ${r.status}`); + return true; +} + +module.exports = { getConfig, userPath, mkdirp, uploadFile, downloadFile, deleteFile, testConnection }; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index bea2eea..724f43d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2696,6 +2696,104 @@ function UploadShareAdminPanel({ toast }) { } + +// ── WebDAV / Synology NAS Einstellungen ─────────────────────────────────── +function WebDavSettings({ toast }) { + const [cfg, setCfg] = useState({ url:'', user:'', password:'', basePath:'/dickendock', enabled:false }); + const [testing, setTesting] = useState(false); + const [saving, setSaving] = useState(false); + const [showPw, setShowPw] = useState(false); + + useEffect(() => { + api('/admin/webdav').then(d => setCfg(d)).catch(() => {}); + }, []); + + async function save() { + setSaving(true); + try { + await api('/admin/webdav', { method:'PUT', body: cfg }); + toast('WebDAV Einstellungen gespeichert ✓'); + } catch(e) { toast('Fehler: ' + e.message); } + finally { setSaving(false); } + } + + async function test() { + setTesting(true); + try { + await api('/admin/webdav/test', { body: cfg }); + toast('✓ Verbindung erfolgreich!'); + } catch(e) { toast('✗ ' + e.message); } + finally { setTesting(false); } + } + + const field = (label, key, type='text', placeholder='') => ( +
+
{label}
+
+ setCfg(p => ({...p, [key]: e.target.value}))} + placeholder={placeholder} + style={{...S.inp, width:'100%', boxSizing:'border-box', paddingRight: type==='password' ? 36 : 12}} + /> + {type === 'password' && ( + + )} +
+
+ ); + + return ( +
+ {/* Enable Toggle */} +
setCfg(p => ({...p, enabled: !p.enabled}))}> +
+
+
+ + {cfg.enabled ? 'WebDAV aktiv – Dateien werden auf NAS gespiegelt' : 'WebDAV deaktiviert'} + +
+ + {field('WEBDAV URL', 'url', 'text', 'http://192.168.1.100:5005 oder https://nas.local:5006')} + {field('BENUTZERNAME', 'user', 'text', 'admin')} + {field('PASSWORT', 'password', 'password', '')} + {field('BASISPFAD AUF DER SYNOLOGY', 'basePath', 'text', '/dickendock')} + +
+ Dateien werden unter Basispfad/Username/Dateiname gespeichert.
+ Beispiel: /dickendock/flo/dokumente/rechnung.pdf
+ Synology WebDAV aktivieren: Systemsteuerung → Dateidienste → WebDAV +
+ +
+ + +
+
+ ); +} + function AdminPanel({ toast, mobile, user, nav }) { const [section, setSection] = useState('profil'); useEffect(() => { if (nav?.section) setSection(nav.section); }, [nav?.ts]); @@ -2848,6 +2946,9 @@ function AdminPanel({ toast, mobile, user, nav }) { + + +
)}