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 }) {
+
+
+
)}