From e6a11f2eed5af87604481590bd81cde45b89f5b8 Mon Sep 17 00:00:00 2001 From: Kroonk Date: Wed, 20 May 2026 18:44:47 +0200 Subject: [PATCH] Improve folder access admin dashboard --- .github/workflows/docker-build.yml | 8 +- _includes/footer.html | 2 +- admin.html | 104 ++++--- assets/css/custom.min.css | 2 +- assets/js/admin.js | 469 +++++++++++++++++------------ assets/js/main.js | 42 ++- assets/sass/custom.scss | 34 +++ backend/server.js | 172 ++++++++--- 8 files changed, 556 insertions(+), 277 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 3cca3fd..c71f05b 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -6,6 +6,7 @@ on: env: REGISTRY: git.mischlabs.de + REGISTRY_USER: MrDiderot REGISTRY_IMAGE: git.mischlabs.de/mrdiderot/photography jobs: @@ -44,13 +45,12 @@ jobs: - name: Log in to Gitea Package Registry shell: sh run: | - if [ -z "${{ secrets.REGISTRY_USER }}" ] || [ -z "${{ secrets.REGISTRY_TOKEN }}" ]; then - echo "Missing registry credentials." - echo "Create repository secrets REGISTRY_USER and REGISTRY_TOKEN." + if [ -z "${{ secrets.REGISTRY_TOKEN }}" ]; then + echo "Missing REGISTRY_TOKEN secret." echo "REGISTRY_TOKEN must be a Gitea access token with package write permission." exit 1 fi - echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "${{ env.REGISTRY }}" -u "${{ secrets.REGISTRY_USER }}" --password-stdin + echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "${{ env.REGISTRY }}" -u "${{ env.REGISTRY_USER }}" --password-stdin - name: Build and push Docker image shell: sh diff --git a/_includes/footer.html b/_includes/footer.html index 69011ff..9b4855a 100644 --- a/_includes/footer.html +++ b/_includes/footer.html @@ -5,4 +5,4 @@ - \ No newline at end of file + diff --git a/admin.html b/admin.html index 0a2fb65..340bdaa 100644 --- a/admin.html +++ b/admin.html @@ -6,18 +6,29 @@ @@ -41,22 +68,22 @@ -
+

Nutzer erstellen

-
-
-
+ +
+
-
-
+
+
-
-
+
+ - - +

Nutzer waehlen, Ordner anhaken, fertig.

+
- - - -
OrdnerStatus
+
+
+

Ordnerinhalt

+

Waehle in der Ordneruebersicht "Ansehen".

+
+
+

Eigenes Passwort aendern

- -
-
+ +
+
-
-
+
+
- +
@@ -120,6 +154,6 @@
- + diff --git a/assets/css/custom.min.css b/assets/css/custom.min.css index e4d3435..cd75cdc 100644 --- a/assets/css/custom.min.css +++ b/assets/css/custom.min.css @@ -1 +1 @@ -.text-fg{color:#a0a0a1}#selection-toolbar{position:fixed;top:0;left:0;width:100%;background:rgba(31,34,36,.95);padding:.75em 1.5em;display:flex;align-items:center;gap:1em;z-index:10003;transform:translateY(-100%);transition:transform .3s ease;box-sizing:border-box}#selection-toolbar.visible{transform:translateY(0)}#selection-toolbar #sel-count{color:#fff;font-weight:400;margin-right:auto;white-space:nowrap}#selection-toolbar button{white-space:nowrap}body.selection-mode #main .thumb{cursor:pointer;position:relative}body.selection-mode #main .thumb::before{content:"";position:absolute;bottom:.6em;right:.6em;width:1.6em;height:1.6em;border:2px solid rgba(255,255,255,.8);border-radius:4px;background:rgba(0,0,0,.5);z-index:2;transition:all .2s ease;pointer-events:none}body.selection-mode #main .thumb .image{opacity:.85;transition:opacity .2s ease}body.selection-mode #main .thumb:active .image{opacity:.6}body.selection-mode #main .thumb.selected::before{background:#34a58e;border-color:#34a58e}body.selection-mode #main .thumb.selected .image{opacity:1;outline:3px solid #34a58e;outline-offset:-3px}@media screen and (max-width: 736px){#selection-toolbar{top:4em;padding:.6em 1em;gap:.6em}#selection-toolbar #sel-count{font-size:.9em}#selection-toolbar button{font-size:.85em;padding:.5em 1em}body.selection-mode #main .thumb::before{bottom:.3em;right:.3em;width:2em;height:2em;border-width:2.5px}body.selection-mode #main .thumb.selected .image{outline-width:4px}}#footer .presentify{margin:0 0 1.4em 0;display:grid;grid-template-columns:auto 1fr;column-gap:1rem;align-items:center}#footer .presentify .presentify-logo{display:flex;align-items:center;justify-content:center}#footer .presentify .presentify-logo a{display:flex;align-items:center;justify-content:center;border:0}#footer .presentify .presentify-logo img{width:72px;height:72px;display:block}#footer .presentify .presentify-text{margin:0} \ No newline at end of file +.text-fg{color:#a0a0a1}#folder-picker-wrap{display:inline-flex;align-items:center;padding:0 .5em}#folder-picker-wrap #folder-picker{width:auto;min-width:13rem;height:2.35em;margin:0;padding:0 2.25em 0 .8em;border-radius:4px;background-color:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.18);color:#fff;font-size:.78em;text-transform:none;letter-spacing:0}@media screen and (max-width:736px){#folder-picker-wrap{max-width:48vw;padding:0 .35em}#folder-picker-wrap #folder-picker{min-width:0;width:100%;font-size:.74rem}}#selection-toolbar{position:fixed;top:0;left:0;width:100%;background:rgba(31,34,36,.95);padding:.75em 1.5em;display:flex;align-items:center;gap:1em;z-index:10003;transform:translateY(-100%);transition:transform .3s ease;box-sizing:border-box}#selection-toolbar.visible{transform:translateY(0)}#selection-toolbar #sel-count{color:#fff;font-weight:400;margin-right:auto;white-space:nowrap}#selection-toolbar button{white-space:nowrap}body.selection-mode #main .thumb{cursor:pointer;position:relative}body.selection-mode #main .thumb::before{content:"";position:absolute;bottom:.6em;right:.6em;width:1.6em;height:1.6em;border:2px solid rgba(255,255,255,.8);border-radius:4px;background:rgba(0,0,0,.5);z-index:2;transition:all .2s ease;pointer-events:none}body.selection-mode #main .thumb .image{opacity:.85;transition:opacity .2s ease}body.selection-mode #main .thumb:active .image{opacity:.6}body.selection-mode #main .thumb.selected::before{background:#34a58e;border-color:#34a58e}body.selection-mode #main .thumb.selected .image{opacity:1;outline:3px solid #34a58e;outline-offset:-3px}@media screen and (max-width: 736px){#selection-toolbar{top:4em;padding:.6em 1em;gap:.6em}#selection-toolbar #sel-count{font-size:.9em}#selection-toolbar button{font-size:.85em;padding:.5em 1em}body.selection-mode #main .thumb::before{bottom:.3em;right:.3em;width:2em;height:2em;border-width:2.5px}body.selection-mode #main .thumb.selected .image{outline-width:4px}}#footer .presentify{margin:0 0 1.4em 0;display:grid;grid-template-columns:auto 1fr;column-gap:1rem;align-items:center}#footer .presentify .presentify-logo{display:flex;align-items:center;justify-content:center}#footer .presentify .presentify-logo a{display:flex;align-items:center;justify-content:center;border:0}#footer .presentify .presentify-logo img{width:72px;height:72px;display:block}#footer .presentify .presentify-text{margin:0} diff --git a/assets/js/admin.js b/assets/js/admin.js index a5c80ab..9ce99f0 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -1,83 +1,265 @@ $(document).ready(function() { - // Auth check + var users = []; + var folders = []; + var assignedFolders = []; + var selectedUserId = ""; + $.get('/api/auth/me') .done(function(user) { if(user.role !== 'admin') { alert('Kein Zugriff!'); window.location.href = '/'; - } else { - loadUsers(); - loadFolders(); - $('body').removeClass('is-preload'); + return; } + refreshDashboard(); + $('body').removeClass('is-preload'); }) .fail(function() { window.location.href = '/'; }); - // Logout $('#logout-btn').click(function(e) { e.preventDefault(); $.post('/api/auth/logout').done(function() { window.location.href = '/'; }); }); - // ==================== NUTZER ==================== - - function loadUsers() { - $.get('/api/admin/users').done(function(users) { - $('#users-tbody').empty(); - $('#user-select').html(''); - - if (!users || users.length === 0) { - $('#users-tbody').html('Keine Nutzer gefunden'); - return; + function refreshDashboard() { + $.when(loadUsers(), loadFolders()).done(function() { + renderUsers(); + renderUserSelect(); + renderFolders(); + if (selectedUserId) { + loadAssignments(selectedUserId); } - - users.forEach(function(u) { - var role = u.role || 'unbekannt'; - var badge = $('').addClass('badge badge-' + role).text(role); - var created = u.created_at ? new Date(u.created_at).toLocaleDateString('de-DE') : '-'; - var info = created; - - if (role === 'client' && u.expires_at) { - var expDate = new Date(u.expires_at).toLocaleDateString('de-DE'); - if (u.expired) { - badge = $('').addClass('badge badge-expired').text('abgelaufen'); - info = 'Abgelaufen: ' + expDate; - } else { - info = 'Bis: ' + expDate; - } - } - - var row = $(''); - var userCell = $('').text(u.username + ' ').append(badge); - var actions = $(''); - $('') - .data('user-id', u.id) - .data('username', u.username) - .appendTo(actions); - $('') - .data('user-id', u.id) - .data('username', u.username) - .appendTo(actions); - - row.append($('').text(u.id)); - row.append(userCell); - row.append($('').text(role)); - row.append($('').text(info)); - row.append(actions); - $('#users-tbody').append(row); - - // Dropdown fuer Ordner-Zuweisung (nur non-admin) - if (role !== 'admin') { - $('#user-select').append($('').val(u.id).text(u.username + ' (' + role + ')')); - } - }); - }).fail(function(xhr) { - var msg = 'HTTP ' + xhr.status + ': ' + xhr.statusText; - $('#users-tbody').html('Fehler beim Laden: ' + msg + ''); }); } - // Nutzer erstellen + function loadUsers() { + return $.get('/api/admin/users').done(function(data) { + users = data || []; + }).fail(function(xhr) { + $('#users-tbody').html( + 'Fehler beim Laden: HTTP ' + + xhr.status + '' + ); + }); + } + + function loadFolders() { + return $.get('/api/admin/folders').done(function(data) { + folders = data || []; + }).fail(function() { + $('#folders-overview').html('

Ordner konnten nicht geladen werden.

'); + }); + } + + function getRoleBadge(role, expired) { + if (expired) return $('').addClass('badge badge-expired').text('abgelaufen'); + return $('').addClass('badge badge-' + (role || 'client')).text(role || 'client'); + } + + function renderUsers() { + var $tbody = $('#users-tbody').empty(); + if (!users.length) { + $tbody.html('Keine Nutzer gefunden'); + return; + } + + users.forEach(function(u) { + var role = u.role || 'client'; + var created = u.created_at ? new Date(u.created_at).toLocaleDateString('de-DE') : '-'; + var status = created; + if (role === 'client' && u.expires_at) { + status = (u.expired ? 'Abgelaufen: ' : 'Bis: ') + + new Date(u.expires_at).toLocaleDateString('de-DE'); + } + + var $row = $(''); + var $userCell = $('').text(u.username + ' ').append(getRoleBadge(role, u.expired)); + var $foldersCell = $(''); + if (u.folders && u.folders.length) { + u.folders.forEach(function(folder) { + $('').addClass('badge folder-badge').text(folder).appendTo($foldersCell); + }); + } else { + $foldersCell.append($('').addClass('muted').text('Keine privaten Ordner')); + } + + var $actions = $(''); + $('') + .data('user-id', u.id) + .appendTo($actions); + $('') + .data('user-id', u.id) + .data('username', u.username) + .appendTo($actions); + $('') + .data('user-id', u.id) + .data('username', u.username) + .appendTo($actions); + + $row.append($('').text(u.id)); + $row.append($userCell); + $row.append($('').text(role)); + $row.append($('').text(status)); + $row.append($foldersCell); + $row.append($actions); + $tbody.append($row); + }); + } + + function renderUserSelect() { + var $select = $('#user-select').html(''); + users.forEach(function(u) { + if ((u.role || 'client') === 'admin') return; + $('') + .val(u.id) + .text(u.username + ' (' + (u.role || 'client') + ')') + .appendTo($select); + }); + if (selectedUserId) $select.val(selectedUserId); + } + + function renderFolders() { + var $wrap = $('#folders-overview').empty(); + if (!folders.length) { + $wrap.html('

Keine Unterordner im Bilderpfad gefunden.

'); + return; + } + + folders.forEach(function(folder) { + var $card = $('
').addClass('folder-card'); + $('

').text(folder.name).appendTo($card); + + var $meta = $('
').addClass('folder-meta').appendTo($card); + $('').addClass('folder-pill').text((folder.image_count || 0) + ' Bilder').appendTo($meta); + $('').addClass('folder-pill').text('privat').appendTo($meta); + $('').addClass('folder-pill').text((folder.assigned_users || []).length + ' Freigaben').appendTo($meta); + + var $assigned = $('
').appendTo($card); + if (folder.assigned_users && folder.assigned_users.length) { + folder.assigned_users.forEach(function(user) { + $('').addClass('badge folder-badge').text(user.username).appendTo($assigned); + }); + } else { + $assigned.append($('').addClass('muted').text('Noch niemandem freigegeben')); + } + + var $actions = $('
').addClass('folder-actions').appendTo($card); + $('') + .data('folder', folder.name) + .appendTo($actions); + $('') + .data('folder', folder.name) + .appendTo($actions); + + $wrap.append($card); + }); + } + + function renderAccessList() { + var $list = $('#folder-access-list').empty(); + if (!folders.length) { + $list.html('

Keine Unterordner gefunden.

'); + return; + } + + folders.forEach(function(folder) { + var checked = assignedFolders.indexOf(folder.name) !== -1; + var $label = $('').addClass('access-item'); + $('') + .val(folder.name) + .prop('checked', checked) + .appendTo($label); + $('').text(folder.name + ' (' + (folder.image_count || 0) + ')').appendTo($label); + $list.append($label); + }); + } + + function loadAssignments(userId) { + selectedUserId = String(userId || ""); + if (!selectedUserId) { + $('#folder-table-wrap').hide(); + assignedFolders = []; + return; + } + + $('#user-select').val(selectedUserId); + $.get('/api/admin/assign?user_id=' + encodeURIComponent(selectedUserId)).done(function(data) { + assignedFolders = data || []; + renderAccessList(); + $('#folder-table-wrap').show(); + }).fail(function() { + assignedFolders = []; + renderAccessList(); + $('#folder-table-wrap').show(); + }); + } + + function saveAssignments() { + if (!selectedUserId) return; + assignedFolders = []; + $('#folder-access-list input:checked').each(function() { + assignedFolders.push($(this).val()); + }); + $.ajax({ + url: '/api/admin/assign', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify({ user_id: selectedUserId, folders: assignedFolders }) + }).done(function() { + $('#assign-status').text('Gespeichert').show().delay(1200).fadeOut(); + refreshDashboard(); + }).fail(function(xhr) { + alert((xhr.responseJSON && xhr.responseJSON.error) || 'Speichern fehlgeschlagen'); + }); + } + + $('#users-tbody').on('click', '.user-access', function() { + loadAssignments($(this).data('user-id')); + document.getElementById('user-select').scrollIntoView({ behavior: 'smooth', block: 'center' }); + }); + + $('#user-select').change(function() { + loadAssignments($(this).val()); + }); + + $('#folder-access-list').on('change', 'input[type="checkbox"]', saveAssignments); + + $('#folders-overview').on('click', '.folder-assign-btn', function() { + var folder = $(this).data('folder'); + if (!selectedUserId) { + alert('Bitte zuerst einen Nutzer in der Ordner-Zuweisung waehlen.'); + return; + } + var $checkbox = $('#folder-access-list input').filter(function() { + return $(this).val() === folder; + }); + if ($checkbox.length) { + $checkbox.prop('checked', !$checkbox.prop('checked')).trigger('change'); + } + }); + + $('#folders-overview').on('click', '.folder-preview-btn', function() { + var folder = $(this).data('folder'); + $('#folder-preview-title').text('Ordnerinhalt: ' + folder); + $('#folder-preview-note').text('Lade Bilder...'); + $('#folder-preview').empty(); + $.get('/api/admin/folders/' + encodeURIComponent(folder) + '/images').done(function(images) { + $('#folder-preview-note').text((images || []).length + ' Bilder im Ordner'); + var $preview = $('#folder-preview').empty(); + if (!images || !images.length) { + $preview.html('

Dieser Ordner ist leer.

'); + return; + } + images.forEach(function(img) { + var $link = $('').attr({ href: img.full_url, target: '_blank', rel: 'noopener' }); + $('').attr({ src: img.thumb_url, alt: img.name, title: img.name }).appendTo($link); + $preview.append($link); + }); + }).fail(function(xhr) { + $('#folder-preview-note').text((xhr.responseJSON && xhr.responseJSON.error) || 'Ordner konnte nicht geladen werden.'); + }); + }); + $('#create-user-form').submit(function(e) { e.preventDefault(); var username = $('#new-username').val().trim(); @@ -92,34 +274,27 @@ $(document).ready(function() { type: 'POST', contentType: 'application/json', data: JSON.stringify({ username: username, password: password, role: role }) - }).done(function(data) { - alert('Nutzer "' + username + '" erfolgreich erstellt!'); - $('#new-username').val(''); - $('#new-password').val(''); - loadUsers(); + }).done(function() { + $('#new-username, #new-password').val(''); + refreshDashboard(); }).fail(function(xhr) { - var msg = 'Unbekannter Fehler'; - if (xhr.responseJSON && xhr.responseJSON.error) msg = xhr.responseJSON.error; - else if (xhr.status) msg = 'HTTP ' + xhr.status + ': ' + xhr.statusText; + var msg = (xhr.responseJSON && xhr.responseJSON.error) || 'Unbekannter Fehler'; alert('Fehler beim Erstellen: ' + msg); }); }); - // Nutzer loeschen - function deleteUser(id, name) { - if (confirm('Nutzer "' + name + '" wirklich loeschen?')) { - $.ajax({ url: '/api/admin/users/' + id, type: 'DELETE' }) - .done(loadUsers) - .fail(function(err) { alert(err.responseJSON ? err.responseJSON.error : 'Fehler'); }); - } - } - $('#users-tbody').on('click', '.user-delete', function() { - deleteUser($(this).data('user-id'), $(this).data('username')); + var id = $(this).data('user-id'); + var name = $(this).data('username'); + if (!confirm('Nutzer "' + name + '" wirklich loeschen?')) return; + $.ajax({ url: '/api/admin/users/' + id, type: 'DELETE' }) + .done(refreshDashboard) + .fail(function(err) { alert(err.responseJSON ? err.responseJSON.error : 'Fehler'); }); }); - // Passwort aendern (Admin setzt fuer anderen User) - function changePassword(id, name) { + $('#users-tbody').on('click', '.user-password', function() { + var id = $(this).data('user-id'); + var name = $(this).data('username'); var newPw = prompt('Neues Passwort fuer "' + name + '":'); if (!newPw) return; $.ajax({ @@ -128,142 +303,38 @@ $(document).ready(function() { contentType: 'application/json', data: JSON.stringify({ new_password: newPw }) }).done(function() { - alert('Passwort geaendert!'); + alert('Passwort geaendert.'); }).fail(function(err) { alert(err.responseJSON ? err.responseJSON.error : 'Fehler'); }); - } - - $('#users-tbody').on('click', '.user-password', function() { - changePassword($(this).data('user-id'), $(this).data('username')); }); - // ==================== ORDNER-ZUWEISUNG ==================== - - var allFolders = []; - var assignedFolders = []; - - function loadFolders() { - $.get('/api/admin/folders').done(function(folders) { - allFolders = folders || []; - renderFolderTable(); - }); - } - - function renderFolderTable() { - var $tbody = $('#folder-tbody'); - $tbody.empty(); - if (allFolders.length === 0) { - $tbody.html('Keine Unterordner gefunden. Erstelle Ordner in Nextcloud.'); - return; - } - allFolders.forEach(function(f) { - var isAssigned = assignedFolders.indexOf(f) !== -1; - var cls = isAssigned ? 'folder-row assigned' : 'folder-row'; - var statusCls = isAssigned ? 'folder-status yes' : 'folder-status no'; - var statusText = isAssigned ? 'Zugewiesen' : '—'; - $tbody.append( - '' + - ' ' + f + '' + - '' + statusText + '' + - '' - ); - }); - } - - // Bei User-Auswahl: zugewiesene Ordner laden und Tabelle anzeigen - $('#user-select').change(function() { - var uid = $(this).val(); - if (!uid) { - $('#folder-table-wrap').hide(); - assignedFolders = []; - return; - } - // Ordner neu laden falls noch leer, dann Zuweisung anzeigen - var showTable = function() { - $.get('/api/admin/assign?user_id=' + uid).done(function(folders) { - assignedFolders = folders || []; - renderFolderTable(); - $('#folder-table-wrap').show(); - }).fail(function() { - assignedFolders = []; - renderFolderTable(); - $('#folder-table-wrap').show(); - }); - }; - if (allFolders.length === 0) { - $.get('/api/admin/folders').done(function(folders) { - allFolders = folders || []; - showTable(); - }).fail(function() { - allFolders = []; - showTable(); - }); - } else { - showTable(); - } - }); - - // Klick auf Ordner-Zeile: Toggle + sofort speichern - $(document).on('click', '.folder-row', function() { - var uid = $('#user-select').val(); - if (!uid) return; - var folder = $(this).data('folder'); - var idx = assignedFolders.indexOf(folder); - if (idx !== -1) { - assignedFolders.splice(idx, 1); - } else { - assignedFolders.push(folder); - } - renderFolderTable(); - $.ajax({ - url: '/api/admin/assign', - type: 'POST', - contentType: 'application/json', - data: JSON.stringify({ user_id: uid, folders: assignedFolders }) - }).done(function() { - $('#assign-status').text('Gespeichert!').show().delay(1500).fadeOut(); - }).fail(function() { - alert('Fehler beim Speichern'); - }); - }); - - // ==================== WEBSITE SYNC ==================== - $('#sync-site').click(function() { - if (!confirm('Neueste Version laden und Website neu starten?')) return; - - var $button = $(this); - var $status = $('#sync-status'); - $button.prop('disabled', true); - $status.text('Sync laeuft...'); - + $('#sync-status').text('Sync laeuft...'); + $('#sync-site').prop('disabled', true); $.post('/api/admin/sync') - .done(function(res) { - $status.text(res.message || 'Sync gestartet. Website startet neu...'); - window.setTimeout(function() { - window.location.reload(); - }, 12000); + .done(function(resp) { + $('#sync-status').text(resp.message || 'Sync gestartet.'); }) - .fail(function(err) { - var msg = err.responseJSON ? err.responseJSON.error : 'Sync fehlgeschlagen'; - $status.text(msg); - $button.prop('disabled', false); + .fail(function(xhr) { + var msg = (xhr.responseJSON && (xhr.responseJSON.error || xhr.responseJSON.details)) || 'Sync fehlgeschlagen'; + $('#sync-status').text(msg); + }) + .always(function() { + $('#sync-site').prop('disabled', false); }); }); - // ==================== EIGENES PASSWORT ==================== - $('#admin-pw-form').submit(function(e) { e.preventDefault(); $.post('/api/auth/change-password', { old_password: $('#admin-pw-old').val(), new_password: $('#admin-pw-new').val() }).done(function() { - alert('Passwort geaendert!'); + alert('Passwort geaendert.'); $('#admin-pw-old, #admin-pw-new').val(''); - }).fail(function(err) { - alert(err.responseJSON ? err.responseJSON.error : 'Fehler'); + }).fail(function(xhr) { + alert(xhr.responseJSON ? xhr.responseJSON.error : 'Fehler'); }); }); }); diff --git a/assets/js/main.js b/assets/js/main.js index a6bc30f..90040ce 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -153,10 +153,17 @@ exifDatas = {}; var imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp']; + var currentFolder = null; - function loadGallery() { + function loadGallery(folderName) { + currentFolder = folderName || null; + $main.empty(); + var galleryUrl = '/images/fulls/'; + if (currentFolder) { + galleryUrl += '?folder=' + encodeURIComponent(currentFolder); + } $.ajax({ - url: '/images/fulls/', + url: galleryUrl, dataType: 'json', cache: false, success: function(files) { @@ -296,6 +303,10 @@ // ==================== SELECTION MODE ==================== function initSelectionMode(allImages) { + $('#selection-toolbar').remove(); + $('#select-mode-toggle').closest('li').remove(); + $('.folder-download-btn').closest('li').remove(); + var selectedImages = []; var selectionActive = false; @@ -481,6 +492,32 @@ }); } + function loadAvailableFolders() { + $.get('/api/folders/available').done(function(folders) { + renderFolderPicker(folders || []); + }); + } + + function renderFolderPicker(folders) { + $('#folder-picker-wrap').remove(); + if (!folders.length) return; + + var $item = $('
  • '); + var $select = $(''); + $select.append($('').val('').text('Oeffentliche Galerie')); + folders.forEach(function(folder) { + var label = folder.name + ' (' + (folder.image_count || 0) + ')'; + $select.append($('').val(folder.name).text(label)); + }); + $item.append($select); + $('#header nav > ul').prepend($item); + + $select.on('change', function(e) { + e.preventDefault(); + loadGallery($(this).val() || null); + }); + } + // Galerie laden loadGallery(); @@ -509,6 +546,7 @@ $('#login-username, #login-password, #login-submit').hide(); $('#logout-btn').show().css('display', 'inline-block'); $('#pw-change-section').show(); + loadAvailableFolders(); if(u.role === 'admin') { $('#admin-link').html('Admin-Dashboard'); } diff --git a/assets/sass/custom.scss b/assets/sass/custom.scss index 9836f19..91c2661 100644 --- a/assets/sass/custom.scss +++ b/assets/sass/custom.scss @@ -6,6 +6,40 @@ color: _palette(fg); } +#folder-picker-wrap { + display: inline-flex; + align-items: center; + padding: 0 0.5em; + + #folder-picker { + width: auto; + min-width: 13rem; + height: 2.35em; + margin: 0; + padding: 0 2.25em 0 0.8em; + border-radius: 4px; + background-color: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.18); + color: _palette(fg-bold); + font-size: 0.78em; + text-transform: none; + letter-spacing: 0; + } +} + +@media screen and (max-width: 736px) { + #folder-picker-wrap { + max-width: 48vw; + padding: 0 0.35em; + + #folder-picker { + min-width: 0; + width: 100%; + font-size: 0.74rem; + } + } +} + /* ==================== Selection Mode ==================== */ #selection-toolbar { diff --git a/backend/server.js b/backend/server.js index 376ac43..ed51de4 100644 --- a/backend/server.js +++ b/backend/server.js @@ -90,6 +90,18 @@ function listSubfolders(dir) { } } +function getFolderInfo(folder) { + const fullPath = path.join(FULLS_DIR, folder); + const thumbPath = path.join(THUMBS_DIR, folder); + const images = listImagesInDir(fullPath); + return { + name: folder, + image_count: images.length, + thumb_count: listImagesInDir(thumbPath).length, + sample: images[0] || null + }; +} + function isSafePath(baseDir, requestedPath) { const resolvedBase = path.resolve(baseDir); const resolvedPath = path.resolve(resolvedBase, requestedPath); @@ -206,29 +218,52 @@ app.get('/images/fulls/', (req, res) => { const rootImages = listImagesInDir(FULLS_DIR).map(name => ({ name, type: 'file', folder: null })); + const requestedFolder = req.query.folder; + + if (requestedFolder) { + if (!isValidName(requestedFolder)) { + return res.status(400).json({ error: 'Ungueltiger Ordnername' }); + } + if (!req.user) { + return res.status(401).json({ error: 'Nicht eingeloggt' }); + } + + const sendFolderImages = () => { + const folderPath = path.join(FULLS_DIR, requestedFolder); + if (!fs.existsSync(folderPath) || !fs.statSync(folderPath).isDirectory()) { + return res.status(404).json({ error: 'Ordner nicht gefunden' }); + } + return res.json(listImagesInDir(folderPath).map(name => ({ + name, type: 'file', folder: requestedFolder + }))); + }; + + if (req.user.role === 'admin') { + return sendFolderImages(); + } + + return db.get('SELECT 1 FROM user_folders WHERE user_id = ? AND folder_name = ?', + [req.user.id, requestedFolder], (err, row) => { + if (err || !row) return res.status(403).json({ error: 'Kein Zugriff auf diesen Ordner' }); + return sendFolderImages(); + }); + } // Nicht eingeloggt: nur Root-Bilder if (!req.user) return res.json(rootImages); + // Eingeloggt: Startseite bleibt die normale oeffentliche Galerie. + return res.json(rootImages); +}); + +app.get('/api/folders/available', requireAuth, (req, res) => { const folders = listSubfolders(FULLS_DIR); - - // Admin: Nur Root-Bilder (damit die Startseite sauber bleibt) if (req.user.role === 'admin') { - return res.json(rootImages); + return res.json(folders.map(getFolderInfo)); } - - // User/Client: NUR zugewiesene Ordner (keine Root-Bilder untermischen) db.all('SELECT folder_name FROM user_folders WHERE user_id = ?', [req.user.id], (err, rows) => { const assignedFolders = err ? [] : rows.map(r => r.folder_name); - const result = []; - assignedFolders.forEach(folder => { - if (folders.includes(folder)) { - listImagesInDir(path.join(FULLS_DIR, folder)).forEach(name => { - result.push({ name, type: 'file', folder }); - }); - } - }); - return res.json(result); + return res.json(assignedFolders.filter(folder => folders.includes(folder)).map(getFolderInfo)); }); }); @@ -244,8 +279,17 @@ app.get('/api/admin/users', requireAdmin, (req, res) => { res.json(rows2.map(u => ({ id: u.id, username: u.username, role: u.role || 'client', created_at: u.created_at || null }))); }); } - // Ablaufdatum berechnen fuer Clients - const result = rows.map(u => { + db.all('SELECT user_id, folder_name FROM user_folders ORDER BY folder_name', (folderErr, folderRows) => { + const foldersByUser = {}; + if (!folderErr && folderRows) { + folderRows.forEach(row => { + if (!foldersByUser[row.user_id]) foldersByUser[row.user_id] = []; + foldersByUser[row.user_id].push(row.folder_name); + }); + } + + // Ablaufdatum berechnen fuer Clients + const result = rows.map(u => { const out = { ...u }; if (u.role === 'client' && u.created_at) { const expires = new Date(u.created_at); @@ -253,9 +297,11 @@ app.get('/api/admin/users', requireAdmin, (req, res) => { out.expires_at = expires.toISOString(); out.expired = new Date() > expires; } + out.folders = foldersByUser[u.id] || []; return out; + }); + res.json(result); }); - res.json(result); }); }); @@ -298,7 +344,49 @@ app.put('/api/admin/users/:id/password', requireAdmin, (req, res) => { // Ordner-Verwaltung app.get('/api/admin/folders', requireAdmin, (req, res) => { - res.json(listSubfolders(FULLS_DIR)); + const folders = listSubfolders(FULLS_DIR).map(getFolderInfo); + db.all( + `SELECT uf.folder_name, u.id, u.username, u.role + FROM user_folders uf + JOIN users u ON u.id = uf.user_id + ORDER BY uf.folder_name, u.username`, + (err, rows) => { + const assignedByFolder = {}; + if (!err && rows) { + rows.forEach(row => { + if (!assignedByFolder[row.folder_name]) assignedByFolder[row.folder_name] = []; + assignedByFolder[row.folder_name].push({ + id: row.id, + username: row.username, + role: row.role || 'client' + }); + }); + } + + res.json(folders.map(folder => ({ + ...folder, + assigned_users: assignedByFolder[folder.name] || [], + private: true + }))); + } + ); +}); + +app.get('/api/admin/folders/:folderName/images', requireAdmin, (req, res) => { + const folderName = req.params.folderName; + if (!isValidName(folderName)) { + return res.status(400).json({ error: 'Ungueltiger Ordnername' }); + } + const folderPath = path.join(FULLS_DIR, folderName); + if (!fs.existsSync(folderPath) || !fs.statSync(folderPath).isDirectory()) { + return res.status(404).json({ error: 'Ordner nicht gefunden' }); + } + res.json(listImagesInDir(folderPath).map(name => ({ + name, + folder: folderName, + full_url: '/images/fulls/' + encodeURIComponent(folderName) + '/' + encodeURIComponent(name), + thumb_url: '/images/thumbs/' + encodeURIComponent(folderName) + '/' + encodeURIComponent(name) + }))); }); app.get('/api/admin/assign', requireAdmin, (req, res) => { @@ -317,7 +405,10 @@ app.get('/api/admin/assign', requireAdmin, (req, res) => { app.post('/api/admin/assign', requireAdmin, (req, res) => { const { user_id, folders } = req.body; if (!user_id) return res.status(400).json({ error: 'user_id erforderlich' }); - const folderList = Array.isArray(folders) ? folders : []; + const existingFolders = listSubfolders(FULLS_DIR); + const folderList = Array.isArray(folders) + ? folders.filter(name => isValidName(name) && existingFolders.includes(name)) + : []; db.serialize(() => { db.run('DELETE FROM user_folders WHERE user_id = ?', [user_id]); const stmt = db.prepare('INSERT INTO user_folders (user_id, folder_name) VALUES (?, ?)'); @@ -408,6 +499,10 @@ app.post('/api/download/selected', (req, res) => { } else { // Ordner-Bild: nur wenn User den Ordner zugewiesen hat if (!req.user) return checkAccess(false); + if (req.user.role === 'admin') { + const filePath = path.join(FULLS_DIR, folder, name); + return checkAccess(fs.existsSync(filePath)); + } db.get('SELECT 1 FROM user_folders WHERE user_id = ? AND folder_name = ?', [req.user.id, folder], (err, row) => { if (err || !row) return checkAccess(false); @@ -453,30 +548,37 @@ app.get('/api/download/folder/:folderName', (req, res) => { if (!req.user) { return res.status(401).json({ error: 'Nicht eingeloggt' }); } + if (req.user.role === 'admin') { + return sendFolderDownload(folderName, res); + } db.get('SELECT 1 FROM user_folders WHERE user_id = ? AND folder_name = ?', [req.user.id, folderName], (err, row) => { if (err || !row) { return res.status(403).json({ error: 'Kein Zugriff auf diesen Ordner' }); } - const folderPath = path.join(FULLS_DIR, folderName); - const images = listImagesInDir(folderPath); - if (images.length === 0) { - return res.status(404).json({ error: 'Keine Bilder im Ordner' }); - } - res.setHeader('Content-Type', 'application/zip'); - res.setHeader('Content-Disposition', 'attachment; filename="' + encodeURIComponent(folderName) + '.zip"'); - const archive = archiver('zip', { zlib: { level: 5 } }); - archive.on('error', () => { - if (!res.headersSent) res.status(500).json({ error: 'ZIP-Fehler' }); - }); - archive.pipe(res); - images.forEach(name => { - archive.file(path.join(folderPath, name), { name }); - }); - archive.finalize(); + return sendFolderDownload(folderName, res); }); }); +function sendFolderDownload(folderName, res) { + const folderPath = path.join(FULLS_DIR, folderName); + const images = listImagesInDir(folderPath); + if (images.length === 0) { + return res.status(404).json({ error: 'Keine Bilder im Ordner' }); + } + res.setHeader('Content-Type', 'application/zip'); + res.setHeader('Content-Disposition', 'attachment; filename="' + encodeURIComponent(folderName) + '.zip"'); + const archive = archiver('zip', { zlib: { level: 5 } }); + archive.on('error', () => { + if (!res.headersSent) res.status(500).json({ error: 'ZIP-Fehler' }); + }); + archive.pipe(res); + images.forEach(name => { + archive.file(path.join(folderPath, name), { name }); + }); + archive.finalize(); +} + // ==================== STATIC FILES ==================== app.get(/^\/images\/(fulls|thumbs)\/(.+)$/, serveProtectedImage);