Zugriff verwalten
-
Wähle Sachbearbeiter aus, die auf die Kundendateien per WebDAV zugreifen dürfen.
+
Wer darf per WebDAV auf die Kundendateien zugreifen?
-
-
-
+
+
+
@@ -166,7 +394,7 @@
const api = {
async get(path) {
const r = await fetch(`/admin/api${path}`);
- if (r.status === 401) { throw Object.assign(new Error('unauth'), { status: 401 }); }
+ if (r.status === 401) throw Object.assign(new Error('unauth'), { status: 401 });
return r.json();
},
async send(method, path, body) {
@@ -175,19 +403,25 @@ const api = {
body: body !== undefined ? JSON.stringify(body) : undefined,
});
const data = await r.json().catch(() => ({}));
- if (!r.ok) throw Object.assign(new Error(data.error || r.statusText), { status: r.status, data });
+ if (!r.ok) throw Object.assign(new Error(data.error || r.statusText), { status: r.status });
+ return data;
+ },
+ async upload(path, file, field = 'file') {
+ const fd = new FormData();
+ fd.append(field, file);
+ const r = await fetch(`/admin/api${path}`, { method: 'POST', body: fd });
+ const data = await r.json().catch(() => ({}));
+ if (!r.ok) throw Object.assign(new Error(data.error || r.statusText), { status: r.status });
return data;
},
};
let me = null;
-
function show(view) {
for (const id of ['view-setup','view-login','view-app']) {
document.getElementById(id).style.display = id === view ? '' : 'none';
}
}
-
function fmtSize(n) {
if (!n) return '0 B';
if (n < 1024) return n + ' B';
@@ -196,53 +430,72 @@ function fmtSize(n) {
return (n/1024/1024/1024).toFixed(2) + ' GB';
}
function parseDate(s) { if (!s) return null; const t = Date.parse(String(s).replace(' ','T')); return isNaN(t)?null:t; }
+function esc(s) { return String(s || '').replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c])); }
+
+let naturalW = 0, naturalH = 0;
+
+function applySizeStyle(el, w, h) {
+ el.style.width = w > 0 ? w + 'px' : '';
+ el.style.height = h > 0 ? h + 'px' : '';
+ if (!w && !h) { el.style.maxHeight = '40px'; el.style.maxWidth = '240px'; }
+ else { el.style.maxHeight = ''; el.style.maxWidth = ''; }
+}
+
+async function applyLogo(width, height) {
+ const img = document.getElementById('navLogo');
+ const src = '/logo?t=' + Date.now();
+ const r = await fetch(src, { method: 'HEAD' });
+ if (r.ok) { img.src = src; img.style.display = ''; applySizeStyle(img, width, height); }
+ else img.style.display = 'none';
+}
+
+function measureNatural() {
+ return new Promise((resolve) => {
+ const probe = new Image();
+ probe.onload = () => { naturalW = probe.naturalWidth; naturalH = probe.naturalHeight; resolve(); };
+ probe.onerror = () => { naturalW = naturalH = 0; resolve(); };
+ probe.src = '/logo?t=' + Date.now();
+ });
+}
async function bootstrap() {
- const status = await api.get('/status');
+ const [status, branding] = await Promise.all([api.get('/status'), api.get('/branding')]);
+ await measureNatural();
+ applyLogo(branding.logo_width_px, branding.logo_height_px);
if (status.setup_required) return show('view-setup');
if (!status.authenticated) return show('view-login');
me = status.user;
- document.getElementById('whoami').textContent = `${me.username} (${me.role})`;
+ document.getElementById('whoami').textContent = `${me.username} · ${me.role}`;
if (me.role !== 'admin') {
document.getElementById('tabUsersBtn').style.display = 'none';
document.getElementById('tabSettingsBtn').style.display = 'none';
- document.getElementById('createCustomerForm').style.display = 'none';
+ document.getElementById('createCustomerCard').style.display = 'none';
} else {
- document.getElementById('createCustomerForm').style.display = '';
+ document.getElementById('createCustomerCard').style.display = '';
}
document.getElementById('webdavUrl').textContent = `webdav://${location.hostname}:1900/`;
show('view-app');
loadCustomers();
}
-// --- Setup ---
+// Setup
document.getElementById('setupForm').addEventListener('submit', async (e) => {
e.preventDefault();
- const fd = new FormData(e.target);
- try {
- await api.send('POST', '/setup', Object.fromEntries(fd.entries()));
- location.reload();
- } catch (ex) { document.getElementById('setupErr').textContent = ex.message; }
+ try { await api.send('POST', '/setup', Object.fromEntries(new FormData(e.target))); location.reload(); }
+ catch (ex) { document.getElementById('setupErr').textContent = ex.message; }
});
-
-// --- Login / Logout ---
+// Login
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
- const fd = new FormData(e.target);
- try {
- await api.send('POST', '/login', Object.fromEntries(fd.entries()));
- location.reload();
- } catch (ex) { document.getElementById('loginErr').textContent = 'Login fehlgeschlagen'; }
-});
-document.getElementById('logoutBtn').addEventListener('click', async () => {
- await api.send('POST', '/logout');
- location.reload();
+ try { await api.send('POST', '/login', Object.fromEntries(new FormData(e.target))); location.reload(); }
+ catch { document.getElementById('loginErr').textContent = 'Login fehlgeschlagen'; }
});
+document.getElementById('logoutBtn').onclick = async () => { await api.send('POST', '/logout'); location.reload(); };
-// --- Tabs ---
-document.querySelectorAll('.tabs button').forEach(btn => {
- btn.addEventListener('click', () => {
- document.querySelectorAll('.tabs button').forEach(b => b.classList.remove('active'));
+// Tabs
+document.querySelectorAll('.tab').forEach(btn => {
+ btn.onclick = () => {
+ document.querySelectorAll('.tab').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const tab = btn.dataset.tab;
document.getElementById('tab-customers').style.display = tab === 'customers' ? '' : 'none';
@@ -250,84 +503,76 @@ document.querySelectorAll('.tabs button').forEach(btn => {
document.getElementById('tab-settings').style.display = tab === 'settings' ? '' : 'none';
if (tab === 'users') loadUsers();
if (tab === 'settings') loadSettings();
- });
+ };
});
-// --- Customers ---
+// Customers
async function loadCustomers() {
const data = await api.get('/customers');
const rows = document.getElementById('customerRows');
+ const adminOnly = me.role === 'admin';
rows.innerHTML = '';
+ if (!data.length) {
+ rows.innerHTML = `
| Noch keine Kunden. |
`;
+ return;
+ }
for (const c of data) {
+ const link = c.upload_url || '';
const tr = document.createElement('tr');
- const link = c.upload_url || '–';
- const adminOnly = me.role === 'admin';
tr.innerHTML = `
-
${c.name} |
-
${c.slug} |
-
${adminOnly ? `${link} ` : 'nur Admin'} |
-
${c.has_password ? '🔒' : '–'} |
-
${c.expires_at ? new Date(c.expires_at).toLocaleString() : '–'} |
-
${c.upload_count} (${fmtSize(c.total_size)}) |
-
- ${adminOnly ? `
-
-
-
-
- ` : `${c.my_access}`}
- |
- `;
+
${esc(c.name)} |
+
${esc(c.slug)} |
+
${adminOnly
+ ? ` ${esc(link)} `
+ : '–'} |
+
${c.has_password ? '🔒 PW' : '–'} |
+
${c.expires_at ? new Date(c.expires_at).toLocaleString() : '–'} |
+
${c.upload_count} · ${fmtSize(c.total_size)} |
+
${adminOnly ? `
+
+
+
+
+ ` : `${c.my_access}`} | `;
rows.appendChild(tr);
}
}
-
document.getElementById('customerRows').addEventListener('click', async (e) => {
- const t = e.target;
+ const t = e.target.closest('button'); if (!t) return;
if (t.dataset.copy) {
navigator.clipboard.writeText(t.dataset.copy);
- const orig = t.textContent; t.textContent = 'Kopiert!';
- setTimeout(() => t.textContent = orig, 1200);
+ const orig = t.textContent; t.textContent = '✓'; setTimeout(() => t.textContent = orig, 1200);
}
- if (t.dataset.del) {
- if (!confirm('Kunde wirklich löschen? (Hochgeladene Dateien bleiben auf Disk.)')) return;
- await api.send('DELETE', `/customers/${t.dataset.del}`);
- loadCustomers();
+ if (t.dataset.del && confirm('Kunde wirklich löschen? (Dateien bleiben auf Disk.)')) {
+ await api.send('DELETE', `/customers/${t.dataset.del}`); loadCustomers();
}
- if (t.dataset.regen) {
- if (!confirm('Neuen Link erzeugen? Alter Link wird ungültig.')) return;
- await api.send('POST', `/customers/${t.dataset.regen}/regenerate-token`);
- loadCustomers();
+ if (t.dataset.regen && confirm('Neuen Link erzeugen? Alter Link wird ungültig.')) {
+ await api.send('POST', `/customers/${t.dataset.regen}/regenerate-token`); loadCustomers();
}
if (t.dataset.edit) {
- const pw = prompt('Neues Passwort (leer = unverändert, "-" = entfernen):', '');
- if (pw === null) return;
- const exp = prompt('Neuer Ablauf (YYYY-MM-DD HH:MM, leer = unverändert, "-" = entfernen):', '');
- if (exp === null) return;
+ const pw = prompt('Neues Passwort (leer = unverändert, "-" = entfernen):', ''); if (pw === null) return;
+ const exp = prompt('Neuer Ablauf (YYYY-MM-DD HH:MM, leer = unverändert, "-" = entfernen):', ''); if (exp === null) return;
const body = {};
- if (pw === '-') body.clear_password = true;
- else if (pw) body.password = pw;
- if (exp === '-') body.expires_at = null;
- else if (exp) body.expires_at = parseDate(exp);
- await api.send('PATCH', `/customers/${t.dataset.edit}`, body);
- loadCustomers();
+ if (pw === '-') body.clear_password = true; else if (pw) body.password = pw;
+ if (exp === '-') body.expires_at = null; else if (exp) body.expires_at = parseDate(exp);
+ await api.send('PATCH', `/customers/${t.dataset.edit}`, body); loadCustomers();
}
if (t.dataset.access) openAccessModal(t.dataset.access, t.dataset.name);
});
-
document.getElementById('createCustomerForm').addEventListener('submit', async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
- const body = {
- name: fd.get('name'),
- password: fd.get('password') || undefined,
- expires_at: parseDate(fd.get('expires_at')) || undefined,
- };
- try { await api.send('POST', '/customers', body); e.target.reset(); loadCustomers(); }
- catch (ex) { alert('Fehler: ' + ex.message); }
+ try {
+ await api.send('POST', '/customers', {
+ name: fd.get('name'),
+ password: fd.get('password') || undefined,
+ expires_at: parseDate(fd.get('expires_at')) || undefined,
+ });
+ e.target.reset(); loadCustomers();
+ } catch (ex) { alert('Fehler: ' + ex.message); }
});
-// --- Users ---
+// Users
async function loadUsers() {
const data = await api.get('/users');
const rows = document.getElementById('userRows');
@@ -335,51 +580,143 @@ async function loadUsers() {
for (const u of data) {
const tr = document.createElement('tr');
tr.innerHTML = `
-
${u.username}${u.id === me.id ? ' (du)' : ''} |
-
${u.role} |
+
${esc(u.username)}${u.id === me.id ? ' (du)' : ''} |
+
${u.role} |
${new Date(u.created_at).toLocaleDateString()} |
-
-
-
-
- |
- `;
+
+
+
+
+ | `;
rows.appendChild(tr);
}
}
-
document.getElementById('userRows').addEventListener('click', async (e) => {
- const t = e.target;
- if (t.dataset.pw) {
- const pw = prompt('Neues Passwort (min. 6 Zeichen):', '');
- if (!pw) return;
- try { await api.send('PATCH', `/users/${t.dataset.pw}`, { password: pw }); loadUsers(); }
- catch (ex) { alert('Fehler: ' + ex.message); }
- }
- if (t.dataset.role) {
- const newRole = t.dataset.current === 'admin' ? 'staff' : 'admin';
- if (!confirm(`Rolle auf "${newRole}" ändern?`)) return;
- try { await api.send('PATCH', `/users/${t.dataset.role}`, { role: newRole }); loadUsers(); }
- catch (ex) { alert('Fehler: ' + ex.message); }
- }
- if (t.dataset.deluser) {
- if (!confirm(`Benutzer "${t.dataset.name}" löschen?`)) return;
- try { await api.send('DELETE', `/users/${t.dataset.deluser}`); loadUsers(); }
- catch (ex) { alert('Fehler: ' + ex.message); }
- }
+ const t = e.target.closest('button'); if (!t) return;
+ try {
+ if (t.dataset.pw) {
+ const pw = prompt('Neues Passwort (min. 6 Zeichen):', ''); if (!pw) return;
+ await api.send('PATCH', `/users/${t.dataset.pw}`, { password: pw }); loadUsers();
+ }
+ if (t.dataset.role) {
+ const newRole = t.dataset.current === 'admin' ? 'staff' : 'admin';
+ if (!confirm(`Rolle auf "${newRole}" ändern?`)) return;
+ await api.send('PATCH', `/users/${t.dataset.role}`, { role: newRole }); loadUsers();
+ }
+ if (t.dataset.deluser && confirm(`Benutzer "${t.dataset.name}" löschen?`)) {
+ await api.send('DELETE', `/users/${t.dataset.deluser}`); loadUsers();
+ }
+ } catch (ex) { alert('Fehler: ' + ex.message); }
+});
+document.getElementById('createUserForm').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ try { await api.send('POST', '/users', Object.fromEntries(new FormData(e.target))); e.target.reset(); loadUsers(); }
+ catch (ex) { alert('Fehler: ' + ex.message); }
});
-document.getElementById('createUserForm').addEventListener('submit', async (e) => {
+// Settings
+async function loadSettings() {
+ await measureNatural();
+ const s = await api.get('/settings');
+ const form = document.getElementById('settingsForm');
+ form.public_base_url.value = s.public_base_url || '';
+ form.janitor_interval_minutes.value = s.janitor_interval_minutes || 30;
+ setSlider('logoWidth', s.logo_width_px || 0);
+ setSlider('logoHeight', s.logo_height_px || 0);
+ updateScaleFromWH();
+ updateLogoPreview(s.logo_filename);
+}
+function setSlider(id, val) {
+ document.getElementById(id).value = val;
+ document.getElementById(id + 'Val').textContent = val;
+}
+function getSlider(id) { return parseInt(document.getElementById(id).value, 10) || 0; }
+
+function updateScaleFromWH() {
+ if (!naturalW || !naturalH) return;
+ const w = getSlider('logoWidth');
+ const h = getSlider('logoHeight');
+ let pct = 100;
+ if (w > 0) pct = Math.round((w / naturalW) * 100);
+ else if (h > 0) pct = Math.round((h / naturalH) * 100);
+ setSlider('logoScale', Math.max(25, Math.min(300, pct)));
+}
+function updateLogoPreview(filename) {
+ const box = document.getElementById('logoPreviewBox');
+ if (filename) {
+ const w = getSlider('logoWidth'), h = getSlider('logoHeight');
+ box.innerHTML = `
})
`;
+ applySizeStyle(box.querySelector('img'), w || 0, h || 0);
+ if (!w && !h) { const im = box.querySelector('img'); im.style.maxHeight = '80px'; im.style.maxWidth = '240px'; }
+ } else {
+ box.innerHTML = `
kein Logo
`;
+ }
+ applyLogo(getSlider('logoWidth'), getSlider('logoHeight'));
+}
+document.getElementById('settingsForm').addEventListener('submit', async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
try {
- await api.send('POST', '/users', Object.fromEntries(fd.entries()));
- e.target.reset();
- loadUsers();
+ await api.send('PUT', '/settings', {
+ public_base_url: fd.get('public_base_url') || '',
+ janitor_interval_minutes: parseInt(fd.get('janitor_interval_minutes') || '30', 10),
+ logo_width_px: getSlider('logoWidth'),
+ logo_height_px: getSlider('logoHeight'),
+ });
+ const msg = document.getElementById('settingsMsg');
+ msg.textContent = '✓ Gespeichert';
+ setTimeout(() => msg.textContent = '', 2000);
+ applyLogo(getSlider('logoWidth'), getSlider('logoHeight'));
} catch (ex) { alert('Fehler: ' + ex.message); }
});
+document.getElementById('runJanitor').addEventListener('click', async () => {
+ const msg = document.getElementById('settingsMsg'); msg.textContent = '… läuft';
+ try { const r = await api.send('POST', '/janitor/run'); msg.textContent = `✓ +${r.added} / -${r.removed}`; }
+ catch (ex) { msg.textContent = 'Fehler: ' + ex.message; }
+});
+document.getElementById('logoWidth').addEventListener('input', (e) => {
+ document.getElementById('logoWidthVal').textContent = e.target.value;
+ updateScaleFromWH();
+ updateLogoPreview(document.querySelector('#logoPreviewBox img') ? 'yes' : '');
+});
+document.getElementById('logoHeight').addEventListener('input', (e) => {
+ document.getElementById('logoHeightVal').textContent = e.target.value;
+ updateScaleFromWH();
+ updateLogoPreview(document.querySelector('#logoPreviewBox img') ? 'yes' : '');
+});
+document.getElementById('logoScale').addEventListener('input', (e) => {
+ const pct = parseInt(e.target.value, 10);
+ document.getElementById('logoScaleVal').textContent = pct;
+ if (naturalW && naturalH) {
+ setSlider('logoWidth', Math.round(naturalW * pct / 100));
+ setSlider('logoHeight', Math.round(naturalH * pct / 100));
+ updateLogoPreview(document.querySelector('#logoPreviewBox img') ? 'yes' : '');
+ }
+});
+document.getElementById('logoReset').addEventListener('click', () => {
+ setSlider('logoWidth', 0);
+ setSlider('logoHeight', 0);
+ setSlider('logoScale', 100);
+ updateLogoPreview(document.querySelector('#logoPreviewBox img') ? 'yes' : '');
+});
+document.getElementById('logoUploadBtn').onclick = () => document.getElementById('logoFile').click();
+document.getElementById('logoFile').onchange = async (e) => {
+ const f = e.target.files[0]; if (!f) return;
+ try {
+ await api.upload('/logo', f, 'logo');
+ await measureNatural();
+ const s = await api.get('/settings');
+ updateLogoPreview(s.logo_filename);
+ } catch (ex) { alert('Fehler: ' + ex.message); }
+};
+document.getElementById('logoDeleteBtn').onclick = async () => {
+ if (!confirm('Logo entfernen?')) return;
+ await api.send('DELETE', '/logo');
+ naturalW = naturalH = 0;
+ updateLogoPreview('');
+};
-// --- Access Modal ---
+// Access modal
let accessCustomerId = null;
async function openAccessModal(id, name) {
accessCustomerId = id;
@@ -391,12 +728,11 @@ async function openAccessModal(id, name) {
} else {
list.innerHTML = staff.map(s => `
- `).join('');
+
`).join('');
}
document.getElementById('accessModal').classList.add('open');
}
@@ -406,8 +742,7 @@ document.getElementById('accessSave').onclick = async () => {
document.querySelectorAll('#accessList .access-row').forEach(row => {
const picked = row.querySelector('input[type=radio]:checked');
if (!picked || !picked.value) return;
- const uid = picked.name.replace('acc-', '');
- entries.push({ user_id: parseInt(uid, 10), access: picked.value });
+ entries.push({ user_id: parseInt(picked.name.replace('acc-',''), 10), access: picked.value });
});
try {
await api.send('PUT', `/customers/${accessCustomerId}/access`, { access: entries });
@@ -415,34 +750,6 @@ document.getElementById('accessSave').onclick = async () => {
} catch (ex) { alert('Fehler: ' + ex.message); }
};
-// --- Settings ---
-async function loadSettings() {
- const s = await api.get('/settings');
- const form = document.getElementById('settingsForm');
- form.public_base_url.value = s.public_base_url || '';
- form.janitor_interval_minutes.value = s.janitor_interval_minutes || 30;
-}
-document.getElementById('settingsForm').addEventListener('submit', async (e) => {
- e.preventDefault();
- const fd = new FormData(e.target);
- try {
- await api.send('PUT', '/settings', {
- public_base_url: fd.get('public_base_url') || '',
- janitor_interval_minutes: parseInt(fd.get('janitor_interval_minutes') || '30', 10),
- });
- document.getElementById('janitorMsg').textContent = 'Gespeichert.';
- setTimeout(() => document.getElementById('janitorMsg').textContent = '', 2000);
- } catch (ex) { alert('Fehler: ' + ex.message); }
-});
-document.getElementById('runJanitor').addEventListener('click', async () => {
- const msg = document.getElementById('janitorMsg');
- msg.textContent = 'läuft …';
- try {
- const r = await api.send('POST', '/janitor/run');
- msg.textContent = `fertig: +${r.added} hinzugefügt, -${r.removed} entfernt`;
- } catch (ex) { msg.textContent = 'Fehler: ' + ex.message; }
-});
-
bootstrap().catch(e => {
document.getElementById('loginErr').textContent = 'Fehler: ' + e.message;
show('view-login');
diff --git a/public/upload.html b/public/upload.html
index a1f6a18..1c7b285 100644
--- a/public/upload.html
+++ b/public/upload.html
@@ -5,53 +5,164 @@