Improve folder access admin dashboard
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 20s
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 20s
This commit is contained in:
8
.github/workflows/docker-build.yml
vendored
8
.github/workflows/docker-build.yml
vendored
@@ -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
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
<script src="assets/js/breakpoints.min.js" defer></script>
|
||||
<script src="assets/js/util.min.js" defer></script>
|
||||
<script src="assets/js/exif.min.js" defer></script>
|
||||
<script src="{{ site.baseurl }}/assets/js/main.min.js?v={{ site.time | date: '%s' }}" defer></script>
|
||||
<script src="{{ site.baseurl }}/assets/js/main.js?v={{ site.time | date: '%s' }}" defer></script>
|
||||
|
||||
104
admin.html
104
admin.html
@@ -6,18 +6,29 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
||||
<link rel="stylesheet" href="/assets/css/main.min.css" />
|
||||
<style>
|
||||
.admin-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.25em; align-items: start; }
|
||||
.admin-section { background: rgba(0,0,0,0.5); padding: 1.5em; border-radius: 4px; min-width: 0; }
|
||||
.admin-section h2 { margin-top: 0; }
|
||||
body { background: #111418; }
|
||||
.dashboard-shell { display:block; padding: 1.25em; max-width: 1320px; margin: 0 auto; width: 100%; }
|
||||
.admin-grid { display: grid; grid-template-columns: 1.1fr 0.9fr; gap: 1em; align-items: start; }
|
||||
.admin-section { background: rgba(20,23,27,0.92); border: 1px solid rgba(255,255,255,0.08); padding: 1em; border-radius: 8px; min-width: 0; box-shadow: 0 12px 30px rgba(0,0,0,0.18); }
|
||||
.admin-section h2 { margin: 0 0 .75em 0; font-size: .95em; letter-spacing: .08em; text-transform: uppercase; color: #fff; }
|
||||
.admin-section p { margin: 0 0 .75em 0; color: rgba(255,255,255,.62); font-size: .9em; }
|
||||
.admin-section.wide { grid-column: 1 / -1; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { text-align: left; padding: 10px; border-bottom: 1px solid rgba(255,255,255,0.2); }
|
||||
.badge { display: inline-block; padding: 2px 8px; border-radius: 3px; font-size: 0.8em; }
|
||||
.toolbar-row, .compact-form { display:flex; gap:.55em; flex-wrap:wrap; align-items:flex-end; }
|
||||
.compact-form .field-inline { min-width: 150px; flex: 1 1 150px; }
|
||||
.compact-form label { display:block; margin: 0 0 .25em 0; font-size:.72em; text-transform:uppercase; letter-spacing:.08em; color:rgba(255,255,255,.55); }
|
||||
.compact-form input, .compact-form select { height: 2.45em; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: .9em; }
|
||||
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid rgba(255,255,255,0.09); vertical-align: middle; }
|
||||
th { color: rgba(255,255,255,.58); font-size: .72em; text-transform: uppercase; letter-spacing: .08em; font-weight: 400; }
|
||||
.badge { display: inline-block; padding: 2px 7px; border-radius: 999px; font-size: 0.76em; line-height: 1.6; }
|
||||
.badge-admin { background: #e44d26; }
|
||||
.badge-user { background: #2196F3; }
|
||||
.badge-client { background: #4CAF50; }
|
||||
.badge-expired { background: #666; }
|
||||
.btn-small { padding: 4px 10px; font-size: 0.85em; margin: 2px; cursor: pointer; }
|
||||
.folder-badge { background: rgba(52,165,142,.16); color:#8ee6d2; margin: 2px 3px 2px 0; border: 1px solid rgba(52,165,142,.28); }
|
||||
.muted { color: rgba(255,255,255,.48); }
|
||||
.btn-small { padding: 3px 9px; font-size: 0.78em; margin: 2px; cursor: pointer; height: auto; line-height: 1.6; border-radius: 5px; }
|
||||
.btn-danger { border-color: rgba(228,77,38,.55) !important; color: #ffad9c !important; }
|
||||
.folder-row { cursor: pointer; transition: background 0.2s; }
|
||||
.folder-row:hover { background: rgba(255,255,255,0.1); }
|
||||
.folder-row.assigned { background: rgba(76,175,80,0.25); }
|
||||
@@ -26,9 +37,25 @@
|
||||
.folder-status.yes { color: #4CAF50; }
|
||||
.folder-status.no { color: #888; }
|
||||
#folder-table-wrap { display: none; margin-top: 1em; }
|
||||
.folder-grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap:.75em; }
|
||||
.folder-card { border: 1px solid rgba(255,255,255,.09); border-radius: 8px; padding:.85em; background: rgba(255,255,255,.035); }
|
||||
.folder-card h3 { margin:0 0 .35em 0; font-size:1em; color:#fff; }
|
||||
.folder-meta { display:flex; gap:.45em; flex-wrap:wrap; margin:.5em 0; }
|
||||
.folder-pill { border:1px solid rgba(255,255,255,.12); border-radius:999px; padding:.15em .55em; font-size:.78em; color:rgba(255,255,255,.68); }
|
||||
.folder-actions { margin-top:.65em; display:flex; gap:.35em; flex-wrap:wrap; }
|
||||
.access-list { display:grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap:.35em .7em; margin-top:.75em; }
|
||||
.access-item { display:flex; align-items:center; gap:.45em; padding:.45em .55em; border:1px solid rgba(255,255,255,.08); border-radius:6px; background:rgba(255,255,255,.025); cursor:pointer; }
|
||||
.access-item input { width:auto; margin:0; }
|
||||
.preview-grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(92px, 1fr)); gap:.55em; margin-top:.75em; max-height: 360px; overflow:auto; padding-right:.25em; }
|
||||
.preview-grid a { border:0; display:block; border-radius:6px; overflow:hidden; background:#0b0d10; aspect-ratio: 1 / 1; }
|
||||
.preview-grid img { width:100%; height:100%; object-fit:cover; display:block; }
|
||||
#folder-preview-title { margin-top:0; }
|
||||
#assign-status, #sync-status { font-size:.85em; color:#8ee6d2; }
|
||||
@media screen and (max-width: 900px) {
|
||||
.admin-grid { grid-template-columns: 1fr; }
|
||||
.admin-section { padding: 1.25em; }
|
||||
.admin-section { padding: .9em; }
|
||||
.dashboard-shell { padding: .75em; }
|
||||
table { font-size:.82em; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -41,22 +68,22 @@
|
||||
</ul></nav>
|
||||
</header>
|
||||
|
||||
<div id="main" style="display:block; padding: 2em; max-width: 1180px; margin: 0 auto; width: 100%;">
|
||||
<div id="main" class="dashboard-shell">
|
||||
<div class="admin-grid">
|
||||
|
||||
<div class="admin-section">
|
||||
<h2>Nutzer erstellen</h2>
|
||||
<form id="create-user-form" style="display:flex; gap:10px; flex-wrap:wrap; align-items:flex-end;">
|
||||
<div>
|
||||
<label>Username</label><br/>
|
||||
<form id="create-user-form" class="compact-form">
|
||||
<div class="field-inline">
|
||||
<label>Username</label>
|
||||
<input type="text" id="new-username" placeholder="Nutzername" required />
|
||||
</div>
|
||||
<div>
|
||||
<label>Passwort</label><br/>
|
||||
<div class="field-inline">
|
||||
<label>Passwort</label>
|
||||
<input type="password" id="new-password" placeholder="Passwort" required />
|
||||
</div>
|
||||
<div>
|
||||
<label>Rolle</label><br/>
|
||||
<div class="field-inline">
|
||||
<label>Rolle</label>
|
||||
<select id="new-role">
|
||||
<option value="client">Client (30 Tage)</option>
|
||||
<option value="user">User (permanent)</option>
|
||||
@@ -71,47 +98,54 @@
|
||||
|
||||
<div class="admin-section">
|
||||
<h2>Website aktualisieren</h2>
|
||||
<p>Laedt das neueste Docker-Image und startet den Website-Container neu.</p>
|
||||
<button id="sync-site">Sync starten</button>
|
||||
<p>Laedt das neueste Registry-Image und startet den Container neu.</p>
|
||||
<button id="sync-site" class="btn-small">Sync starten</button>
|
||||
<span id="sync-status" style="margin-left:10px;"></span>
|
||||
</div>
|
||||
|
||||
<div class="admin-section wide">
|
||||
<h2>Nutzerverwaltung</h2>
|
||||
<h2>Nutzer, Rollen und Ordnerzugriffe</h2>
|
||||
<table>
|
||||
<thead><tr><th>ID</th><th>Username</th><th>Rolle</th><th>Erstellt</th><th>Aktionen</th></tr></thead>
|
||||
<thead><tr><th>ID</th><th>Username</th><th>Rolle</th><th>Status</th><th>Ordnerzugriff</th><th>Aktionen</th></tr></thead>
|
||||
<tbody id="users-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="admin-section wide">
|
||||
<h2>Ordner im Bilderpfad</h2>
|
||||
<p>Unterordner sind privat. Nur explizit freigegebene Nutzer sehen sie nach dem Login.</p>
|
||||
<div id="folders-overview" class="folder-grid"></div>
|
||||
</div>
|
||||
|
||||
<div class="admin-section">
|
||||
<h2>Ordner-Zuweisung</h2>
|
||||
<p>Waehle einen Nutzer und klicke auf Ordner um sie zuzuweisen oder zu entfernen.</p>
|
||||
<select id="user-select">
|
||||
<option value="">Nutzer waehlen...</option>
|
||||
</select>
|
||||
<p>Nutzer waehlen, Ordner anhaken, fertig.</p>
|
||||
<select id="user-select"><option value="">Nutzer waehlen...</option></select>
|
||||
<div id="folder-table-wrap">
|
||||
<table id="folder-table">
|
||||
<thead><tr><th><i class="fa fa-folder"></i> Ordner</th><th style="width:120px; text-align:center;">Status</th></tr></thead>
|
||||
<tbody id="folder-tbody"></tbody>
|
||||
</table>
|
||||
<div id="folder-access-list" class="access-list"></div>
|
||||
<span id="assign-status" style="display:inline-block; margin-top:10px;"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-section">
|
||||
<h2 id="folder-preview-title">Ordnerinhalt</h2>
|
||||
<p id="folder-preview-note">Waehle in der Ordneruebersicht "Ansehen".</p>
|
||||
<div id="folder-preview" class="preview-grid"></div>
|
||||
</div>
|
||||
|
||||
<div class="admin-section">
|
||||
<h2>Eigenes Passwort aendern</h2>
|
||||
<form id="admin-pw-form" style="display:flex; gap:10px; flex-wrap:wrap; align-items:flex-end;">
|
||||
<div>
|
||||
<label>Altes Passwort</label><br/>
|
||||
<form id="admin-pw-form" class="compact-form">
|
||||
<div class="field-inline">
|
||||
<label>Altes Passwort</label>
|
||||
<input type="password" id="admin-pw-old" required />
|
||||
</div>
|
||||
<div>
|
||||
<label>Neues Passwort</label><br/>
|
||||
<div class="field-inline">
|
||||
<label>Neues Passwort</label>
|
||||
<input type="password" id="admin-pw-new" required />
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit">Aendern</button>
|
||||
<button type="submit" class="btn-small">Aendern</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -120,6 +154,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="/assets/js/jquery.min.js"></script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
<script src="/assets/js/admin.js?v=2"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
2
assets/css/custom.min.css
vendored
2
assets/css/custom.min.css
vendored
@@ -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}
|
||||
.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}
|
||||
|
||||
@@ -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 refreshDashboard() {
|
||||
$.when(loadUsers(), loadFolders()).done(function() {
|
||||
renderUsers();
|
||||
renderUserSelect();
|
||||
renderFolders();
|
||||
if (selectedUserId) {
|
||||
loadAssignments(selectedUserId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadUsers() {
|
||||
$.get('/api/admin/users').done(function(users) {
|
||||
$('#users-tbody').empty();
|
||||
$('#user-select').html('<option value="">Nutzer waehlen...</option>');
|
||||
return $.get('/api/admin/users').done(function(data) {
|
||||
users = data || [];
|
||||
}).fail(function(xhr) {
|
||||
$('#users-tbody').html(
|
||||
'<tr><td colspan="6" style="color:#e44d26;">Fehler beim Laden: HTTP ' +
|
||||
xhr.status + '</td></tr>'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (!users || users.length === 0) {
|
||||
$('#users-tbody').html('<tr><td colspan="5" style="text-align:center;">Keine Nutzer gefunden</td></tr>');
|
||||
function loadFolders() {
|
||||
return $.get('/api/admin/folders').done(function(data) {
|
||||
folders = data || [];
|
||||
}).fail(function() {
|
||||
$('#folders-overview').html('<p style="color:#e44d26;">Ordner konnten nicht geladen werden.</p>');
|
||||
});
|
||||
}
|
||||
|
||||
function getRoleBadge(role, expired) {
|
||||
if (expired) return $('<span></span>').addClass('badge badge-expired').text('abgelaufen');
|
||||
return $('<span></span>').addClass('badge badge-' + (role || 'client')).text(role || 'client');
|
||||
}
|
||||
|
||||
function renderUsers() {
|
||||
var $tbody = $('#users-tbody').empty();
|
||||
if (!users.length) {
|
||||
$tbody.html('<tr><td colspan="6" style="text-align:center;">Keine Nutzer gefunden</td></tr>');
|
||||
return;
|
||||
}
|
||||
|
||||
users.forEach(function(u) {
|
||||
var role = u.role || 'unbekannt';
|
||||
var badge = $('<span></span>').addClass('badge badge-' + role).text(role);
|
||||
var role = u.role || 'client';
|
||||
var created = u.created_at ? new Date(u.created_at).toLocaleDateString('de-DE') : '-';
|
||||
var info = created;
|
||||
|
||||
var status = created;
|
||||
if (role === 'client' && u.expires_at) {
|
||||
var expDate = new Date(u.expires_at).toLocaleDateString('de-DE');
|
||||
if (u.expired) {
|
||||
badge = $('<span></span>').addClass('badge badge-expired').text('abgelaufen');
|
||||
info = 'Abgelaufen: ' + expDate;
|
||||
} else {
|
||||
info = 'Bis: ' + expDate;
|
||||
}
|
||||
status = (u.expired ? 'Abgelaufen: ' : 'Bis: ') +
|
||||
new Date(u.expires_at).toLocaleDateString('de-DE');
|
||||
}
|
||||
|
||||
var row = $('<tr></tr>');
|
||||
var userCell = $('<td></td>').text(u.username + ' ').append(badge);
|
||||
var actions = $('<td></td>');
|
||||
var $row = $('<tr></tr>');
|
||||
var $userCell = $('<td></td>').text(u.username + ' ').append(getRoleBadge(role, u.expired));
|
||||
var $foldersCell = $('<td></td>');
|
||||
if (u.folders && u.folders.length) {
|
||||
u.folders.forEach(function(folder) {
|
||||
$('<span></span>').addClass('badge folder-badge').text(folder).appendTo($foldersCell);
|
||||
});
|
||||
} else {
|
||||
$foldersCell.append($('<span></span>').addClass('muted').text('Keine privaten Ordner'));
|
||||
}
|
||||
|
||||
var $actions = $('<td></td>');
|
||||
$('<button type="button" class="btn-small user-access">Zugriff</button>')
|
||||
.data('user-id', u.id)
|
||||
.appendTo($actions);
|
||||
$('<button type="button" class="btn-small user-password">PW</button>')
|
||||
.data('user-id', u.id)
|
||||
.data('username', u.username)
|
||||
.appendTo(actions);
|
||||
$('<button type="button" class="btn-small user-delete">X</button>')
|
||||
.appendTo($actions);
|
||||
$('<button type="button" class="btn-small btn-danger user-delete">Loeschen</button>')
|
||||
.data('user-id', u.id)
|
||||
.data('username', u.username)
|
||||
.appendTo(actions);
|
||||
.appendTo($actions);
|
||||
|
||||
row.append($('<td></td>').text(u.id));
|
||||
row.append(userCell);
|
||||
row.append($('<td></td>').text(role));
|
||||
row.append($('<td></td>').text(info));
|
||||
row.append(actions);
|
||||
$('#users-tbody').append(row);
|
||||
|
||||
// Dropdown fuer Ordner-Zuweisung (nur non-admin)
|
||||
if (role !== 'admin') {
|
||||
$('#user-select').append($('<option></option>').val(u.id).text(u.username + ' (' + role + ')'));
|
||||
$row.append($('<td></td>').text(u.id));
|
||||
$row.append($userCell);
|
||||
$row.append($('<td></td>').text(role));
|
||||
$row.append($('<td></td>').text(status));
|
||||
$row.append($foldersCell);
|
||||
$row.append($actions);
|
||||
$tbody.append($row);
|
||||
});
|
||||
}
|
||||
|
||||
function renderUserSelect() {
|
||||
var $select = $('#user-select').html('<option value="">Nutzer waehlen...</option>');
|
||||
users.forEach(function(u) {
|
||||
if ((u.role || 'client') === 'admin') return;
|
||||
$('<option></option>')
|
||||
.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('<p class="muted">Keine Unterordner im Bilderpfad gefunden.</p>');
|
||||
return;
|
||||
}
|
||||
|
||||
folders.forEach(function(folder) {
|
||||
var $card = $('<div></div>').addClass('folder-card');
|
||||
$('<h3></h3>').text(folder.name).appendTo($card);
|
||||
|
||||
var $meta = $('<div></div>').addClass('folder-meta').appendTo($card);
|
||||
$('<span></span>').addClass('folder-pill').text((folder.image_count || 0) + ' Bilder').appendTo($meta);
|
||||
$('<span></span>').addClass('folder-pill').text('privat').appendTo($meta);
|
||||
$('<span></span>').addClass('folder-pill').text((folder.assigned_users || []).length + ' Freigaben').appendTo($meta);
|
||||
|
||||
var $assigned = $('<div></div>').appendTo($card);
|
||||
if (folder.assigned_users && folder.assigned_users.length) {
|
||||
folder.assigned_users.forEach(function(user) {
|
||||
$('<span></span>').addClass('badge folder-badge').text(user.username).appendTo($assigned);
|
||||
});
|
||||
} else {
|
||||
$assigned.append($('<span></span>').addClass('muted').text('Noch niemandem freigegeben'));
|
||||
}
|
||||
|
||||
var $actions = $('<div></div>').addClass('folder-actions').appendTo($card);
|
||||
$('<button type="button" class="btn-small folder-preview-btn">Ansehen</button>')
|
||||
.data('folder', folder.name)
|
||||
.appendTo($actions);
|
||||
$('<button type="button" class="btn-small folder-assign-btn">Freigaben</button>')
|
||||
.data('folder', folder.name)
|
||||
.appendTo($actions);
|
||||
|
||||
$wrap.append($card);
|
||||
});
|
||||
}
|
||||
|
||||
function renderAccessList() {
|
||||
var $list = $('#folder-access-list').empty();
|
||||
if (!folders.length) {
|
||||
$list.html('<p class="muted">Keine Unterordner gefunden.</p>');
|
||||
return;
|
||||
}
|
||||
|
||||
folders.forEach(function(folder) {
|
||||
var checked = assignedFolders.indexOf(folder.name) !== -1;
|
||||
var $label = $('<label></label>').addClass('access-item');
|
||||
$('<input type="checkbox" />')
|
||||
.val(folder.name)
|
||||
.prop('checked', checked)
|
||||
.appendTo($label);
|
||||
$('<span></span>').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('<p class="muted">Dieser Ordner ist leer.</p>');
|
||||
return;
|
||||
}
|
||||
images.forEach(function(img) {
|
||||
var $link = $('<a></a>').attr({ href: img.full_url, target: '_blank', rel: 'noopener' });
|
||||
$('<img />').attr({ src: img.thumb_url, alt: img.name, title: img.name }).appendTo($link);
|
||||
$preview.append($link);
|
||||
});
|
||||
}).fail(function(xhr) {
|
||||
var msg = 'HTTP ' + xhr.status + ': ' + xhr.statusText;
|
||||
$('#users-tbody').html('<tr><td colspan="5" style="color:#e44d26;">Fehler beim Laden: ' + msg + '</td></tr>');
|
||||
$('#folder-preview-note').text((xhr.responseJSON && xhr.responseJSON.error) || 'Ordner konnte nicht geladen werden.');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Nutzer erstellen
|
||||
$('#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('<tr><td colspan="2" style="text-align:center;">Keine Unterordner gefunden. Erstelle Ordner in Nextcloud.</td></tr>');
|
||||
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(
|
||||
'<tr class="' + cls + '" data-folder="' + f + '">' +
|
||||
'<td><i class="fa fa-folder"></i> ' + f + '</td>' +
|
||||
'<td style="text-align:center;"><span class="' + statusCls + '">' + statusText + '</span></td>' +
|
||||
'</tr>'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 = $('<li id="folder-picker-wrap"></li>');
|
||||
var $select = $('<select id="folder-picker" title="Galerie-Ordner waehlen"></select>');
|
||||
$select.append($('<option></option>').val('').text('Oeffentliche Galerie'));
|
||||
folders.forEach(function(folder) {
|
||||
var label = folder.name + ' (' + (folder.image_count || 0) + ')';
|
||||
$select.append($('<option></option>').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('<a href="/admin.html" class="button">Admin-Dashboard</a>');
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
const folders = listSubfolders(FULLS_DIR);
|
||||
|
||||
// Admin: Nur Root-Bilder (damit die Startseite sauber bleibt)
|
||||
if (req.user.role === 'admin') {
|
||||
// Eingeloggt: Startseite bleibt die normale oeffentliche Galerie.
|
||||
return res.json(rootImages);
|
||||
}
|
||||
});
|
||||
|
||||
// User/Client: NUR zugewiesene Ordner (keine Root-Bilder untermischen)
|
||||
app.get('/api/folders/available', requireAuth, (req, res) => {
|
||||
const folders = listSubfolders(FULLS_DIR);
|
||||
if (req.user.role === 'admin') {
|
||||
return res.json(folders.map(getFolderInfo));
|
||||
}
|
||||
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,6 +279,15 @@ 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 })));
|
||||
});
|
||||
}
|
||||
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 };
|
||||
@@ -253,11 +297,13 @@ 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/admin/users', requireAdmin, (req, res) => {
|
||||
const { username, password, role } = req.body;
|
||||
@@ -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,11 +548,19 @@ 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' });
|
||||
}
|
||||
return sendFolderDownload(folderName, res);
|
||||
});
|
||||
});
|
||||
|
||||
function sendFolderDownload(folderName, res) {
|
||||
const folderPath = path.join(FULLS_DIR, folderName);
|
||||
const images = listImagesInDir(folderPath);
|
||||
if (images.length === 0) {
|
||||
@@ -474,8 +577,7 @@ app.get('/api/download/folder/:folderName', (req, res) => {
|
||||
archive.file(path.join(folderPath, name), { name });
|
||||
});
|
||||
archive.finalize();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== STATIC FILES ====================
|
||||
|
||||
|
||||
Reference in New Issue
Block a user