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:
|
env:
|
||||||
REGISTRY: git.mischlabs.de
|
REGISTRY: git.mischlabs.de
|
||||||
|
REGISTRY_USER: MrDiderot
|
||||||
REGISTRY_IMAGE: git.mischlabs.de/mrdiderot/photography
|
REGISTRY_IMAGE: git.mischlabs.de/mrdiderot/photography
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -44,13 +45,12 @@ jobs:
|
|||||||
- name: Log in to Gitea Package Registry
|
- name: Log in to Gitea Package Registry
|
||||||
shell: sh
|
shell: sh
|
||||||
run: |
|
run: |
|
||||||
if [ -z "${{ secrets.REGISTRY_USER }}" ] || [ -z "${{ secrets.REGISTRY_TOKEN }}" ]; then
|
if [ -z "${{ secrets.REGISTRY_TOKEN }}" ]; then
|
||||||
echo "Missing registry credentials."
|
echo "Missing REGISTRY_TOKEN secret."
|
||||||
echo "Create repository secrets REGISTRY_USER and REGISTRY_TOKEN."
|
|
||||||
echo "REGISTRY_TOKEN must be a Gitea access token with package write permission."
|
echo "REGISTRY_TOKEN must be a Gitea access token with package write permission."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
- name: Build and push Docker image
|
||||||
shell: sh
|
shell: sh
|
||||||
|
|||||||
@@ -5,4 +5,4 @@
|
|||||||
<script src="assets/js/breakpoints.min.js" defer></script>
|
<script src="assets/js/breakpoints.min.js" defer></script>
|
||||||
<script src="assets/js/util.min.js" defer></script>
|
<script src="assets/js/util.min.js" defer></script>
|
||||||
<script src="assets/js/exif.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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
||||||
<link rel="stylesheet" href="/assets/css/main.min.css" />
|
<link rel="stylesheet" href="/assets/css/main.min.css" />
|
||||||
<style>
|
<style>
|
||||||
.admin-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.25em; align-items: start; }
|
body { background: #111418; }
|
||||||
.admin-section { background: rgba(0,0,0,0.5); padding: 1.5em; border-radius: 4px; min-width: 0; }
|
.dashboard-shell { display:block; padding: 1.25em; max-width: 1320px; margin: 0 auto; width: 100%; }
|
||||||
.admin-section h2 { margin-top: 0; }
|
.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; }
|
.admin-section.wide { grid-column: 1 / -1; }
|
||||||
table { width: 100%; border-collapse: collapse; }
|
.toolbar-row, .compact-form { display:flex; gap:.55em; flex-wrap:wrap; align-items:flex-end; }
|
||||||
th, td { text-align: left; padding: 10px; border-bottom: 1px solid rgba(255,255,255,0.2); }
|
.compact-form .field-inline { min-width: 150px; flex: 1 1 150px; }
|
||||||
.badge { display: inline-block; padding: 2px 8px; border-radius: 3px; font-size: 0.8em; }
|
.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-admin { background: #e44d26; }
|
||||||
.badge-user { background: #2196F3; }
|
.badge-user { background: #2196F3; }
|
||||||
.badge-client { background: #4CAF50; }
|
.badge-client { background: #4CAF50; }
|
||||||
.badge-expired { background: #666; }
|
.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 { cursor: pointer; transition: background 0.2s; }
|
||||||
.folder-row:hover { background: rgba(255,255,255,0.1); }
|
.folder-row:hover { background: rgba(255,255,255,0.1); }
|
||||||
.folder-row.assigned { background: rgba(76,175,80,0.25); }
|
.folder-row.assigned { background: rgba(76,175,80,0.25); }
|
||||||
@@ -26,9 +37,25 @@
|
|||||||
.folder-status.yes { color: #4CAF50; }
|
.folder-status.yes { color: #4CAF50; }
|
||||||
.folder-status.no { color: #888; }
|
.folder-status.no { color: #888; }
|
||||||
#folder-table-wrap { display: none; margin-top: 1em; }
|
#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) {
|
@media screen and (max-width: 900px) {
|
||||||
.admin-grid { grid-template-columns: 1fr; }
|
.admin-grid { grid-template-columns: 1fr; }
|
||||||
.admin-section { padding: 1.25em; }
|
.admin-section { padding: .9em; }
|
||||||
|
.dashboard-shell { padding: .75em; }
|
||||||
|
table { font-size:.82em; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -41,22 +68,22 @@
|
|||||||
</ul></nav>
|
</ul></nav>
|
||||||
</header>
|
</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-grid">
|
||||||
|
|
||||||
<div class="admin-section">
|
<div class="admin-section">
|
||||||
<h2>Nutzer erstellen</h2>
|
<h2>Nutzer erstellen</h2>
|
||||||
<form id="create-user-form" style="display:flex; gap:10px; flex-wrap:wrap; align-items:flex-end;">
|
<form id="create-user-form" class="compact-form">
|
||||||
<div>
|
<div class="field-inline">
|
||||||
<label>Username</label><br/>
|
<label>Username</label>
|
||||||
<input type="text" id="new-username" placeholder="Nutzername" required />
|
<input type="text" id="new-username" placeholder="Nutzername" required />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="field-inline">
|
||||||
<label>Passwort</label><br/>
|
<label>Passwort</label>
|
||||||
<input type="password" id="new-password" placeholder="Passwort" required />
|
<input type="password" id="new-password" placeholder="Passwort" required />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="field-inline">
|
||||||
<label>Rolle</label><br/>
|
<label>Rolle</label>
|
||||||
<select id="new-role">
|
<select id="new-role">
|
||||||
<option value="client">Client (30 Tage)</option>
|
<option value="client">Client (30 Tage)</option>
|
||||||
<option value="user">User (permanent)</option>
|
<option value="user">User (permanent)</option>
|
||||||
@@ -71,47 +98,54 @@
|
|||||||
|
|
||||||
<div class="admin-section">
|
<div class="admin-section">
|
||||||
<h2>Website aktualisieren</h2>
|
<h2>Website aktualisieren</h2>
|
||||||
<p>Laedt das neueste Docker-Image und startet den Website-Container neu.</p>
|
<p>Laedt das neueste Registry-Image und startet den Container neu.</p>
|
||||||
<button id="sync-site">Sync starten</button>
|
<button id="sync-site" class="btn-small">Sync starten</button>
|
||||||
<span id="sync-status" style="margin-left:10px;"></span>
|
<span id="sync-status" style="margin-left:10px;"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="admin-section wide">
|
<div class="admin-section wide">
|
||||||
<h2>Nutzerverwaltung</h2>
|
<h2>Nutzer, Rollen und Ordnerzugriffe</h2>
|
||||||
<table>
|
<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>
|
<tbody id="users-tbody"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="admin-section wide">
|
<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>
|
<h2>Ordner-Zuweisung</h2>
|
||||||
<p>Waehle einen Nutzer und klicke auf Ordner um sie zuzuweisen oder zu entfernen.</p>
|
<p>Nutzer waehlen, Ordner anhaken, fertig.</p>
|
||||||
<select id="user-select">
|
<select id="user-select"><option value="">Nutzer waehlen...</option></select>
|
||||||
<option value="">Nutzer waehlen...</option>
|
|
||||||
</select>
|
|
||||||
<div id="folder-table-wrap">
|
<div id="folder-table-wrap">
|
||||||
<table id="folder-table">
|
<div id="folder-access-list" class="access-list"></div>
|
||||||
<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>
|
|
||||||
<span id="assign-status" style="display:inline-block; margin-top:10px;"></span>
|
<span id="assign-status" style="display:inline-block; margin-top:10px;"></span>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="admin-section">
|
||||||
<h2>Eigenes Passwort aendern</h2>
|
<h2>Eigenes Passwort aendern</h2>
|
||||||
<form id="admin-pw-form" style="display:flex; gap:10px; flex-wrap:wrap; align-items:flex-end;">
|
<form id="admin-pw-form" class="compact-form">
|
||||||
<div>
|
<div class="field-inline">
|
||||||
<label>Altes Passwort</label><br/>
|
<label>Altes Passwort</label>
|
||||||
<input type="password" id="admin-pw-old" required />
|
<input type="password" id="admin-pw-old" required />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="field-inline">
|
||||||
<label>Neues Passwort</label><br/>
|
<label>Neues Passwort</label>
|
||||||
<input type="password" id="admin-pw-new" required />
|
<input type="password" id="admin-pw-new" required />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button type="submit">Aendern</button>
|
<button type="submit" class="btn-small">Aendern</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,6 +154,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/assets/js/jquery.min.js"></script>
|
<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>
|
</body>
|
||||||
</html>
|
</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() {
|
$(document).ready(function() {
|
||||||
// Auth check
|
var users = [];
|
||||||
|
var folders = [];
|
||||||
|
var assignedFolders = [];
|
||||||
|
var selectedUserId = "";
|
||||||
|
|
||||||
$.get('/api/auth/me')
|
$.get('/api/auth/me')
|
||||||
.done(function(user) {
|
.done(function(user) {
|
||||||
if(user.role !== 'admin') {
|
if(user.role !== 'admin') {
|
||||||
alert('Kein Zugriff!');
|
alert('Kein Zugriff!');
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
} else {
|
return;
|
||||||
loadUsers();
|
|
||||||
loadFolders();
|
|
||||||
$('body').removeClass('is-preload');
|
|
||||||
}
|
}
|
||||||
|
refreshDashboard();
|
||||||
|
$('body').removeClass('is-preload');
|
||||||
})
|
})
|
||||||
.fail(function() { window.location.href = '/'; });
|
.fail(function() { window.location.href = '/'; });
|
||||||
|
|
||||||
// Logout
|
|
||||||
$('#logout-btn').click(function(e) {
|
$('#logout-btn').click(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
$.post('/api/auth/logout').done(function() { window.location.href = '/'; });
|
$.post('/api/auth/logout').done(function() { window.location.href = '/'; });
|
||||||
});
|
});
|
||||||
|
|
||||||
// ==================== NUTZER ====================
|
function refreshDashboard() {
|
||||||
|
$.when(loadUsers(), loadFolders()).done(function() {
|
||||||
function loadUsers() {
|
renderUsers();
|
||||||
$.get('/api/admin/users').done(function(users) {
|
renderUserSelect();
|
||||||
$('#users-tbody').empty();
|
renderFolders();
|
||||||
$('#user-select').html('<option value="">Nutzer waehlen...</option>');
|
if (selectedUserId) {
|
||||||
|
loadAssignments(selectedUserId);
|
||||||
if (!users || users.length === 0) {
|
|
||||||
$('#users-tbody').html('<tr><td colspan="5" 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 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 = $('<span></span>').addClass('badge badge-expired').text('abgelaufen');
|
|
||||||
info = 'Abgelaufen: ' + expDate;
|
|
||||||
} else {
|
|
||||||
info = 'Bis: ' + expDate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var row = $('<tr></tr>');
|
|
||||||
var userCell = $('<td></td>').text(u.username + ' ').append(badge);
|
|
||||||
var actions = $('<td></td>');
|
|
||||||
$('<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>')
|
|
||||||
.data('user-id', u.id)
|
|
||||||
.data('username', u.username)
|
|
||||||
.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 + ')'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).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>');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nutzer erstellen
|
function loadUsers() {
|
||||||
|
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>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 || '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 = $('<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 btn-danger user-delete">Loeschen</button>')
|
||||||
|
.data('user-id', u.id)
|
||||||
|
.data('username', u.username)
|
||||||
|
.appendTo($actions);
|
||||||
|
|
||||||
|
$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) {
|
||||||
|
$('#folder-preview-note').text((xhr.responseJSON && xhr.responseJSON.error) || 'Ordner konnte nicht geladen werden.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$('#create-user-form').submit(function(e) {
|
$('#create-user-form').submit(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var username = $('#new-username').val().trim();
|
var username = $('#new-username').val().trim();
|
||||||
@@ -92,34 +274,27 @@ $(document).ready(function() {
|
|||||||
type: 'POST',
|
type: 'POST',
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
data: JSON.stringify({ username: username, password: password, role: role })
|
data: JSON.stringify({ username: username, password: password, role: role })
|
||||||
}).done(function(data) {
|
}).done(function() {
|
||||||
alert('Nutzer "' + username + '" erfolgreich erstellt!');
|
$('#new-username, #new-password').val('');
|
||||||
$('#new-username').val('');
|
refreshDashboard();
|
||||||
$('#new-password').val('');
|
|
||||||
loadUsers();
|
|
||||||
}).fail(function(xhr) {
|
}).fail(function(xhr) {
|
||||||
var msg = 'Unbekannter Fehler';
|
var msg = (xhr.responseJSON && xhr.responseJSON.error) || 'Unbekannter Fehler';
|
||||||
if (xhr.responseJSON && xhr.responseJSON.error) msg = xhr.responseJSON.error;
|
|
||||||
else if (xhr.status) msg = 'HTTP ' + xhr.status + ': ' + xhr.statusText;
|
|
||||||
alert('Fehler beim Erstellen: ' + msg);
|
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() {
|
$('#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)
|
$('#users-tbody').on('click', '.user-password', function() {
|
||||||
function changePassword(id, name) {
|
var id = $(this).data('user-id');
|
||||||
|
var name = $(this).data('username');
|
||||||
var newPw = prompt('Neues Passwort fuer "' + name + '":');
|
var newPw = prompt('Neues Passwort fuer "' + name + '":');
|
||||||
if (!newPw) return;
|
if (!newPw) return;
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@@ -128,142 +303,38 @@ $(document).ready(function() {
|
|||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
data: JSON.stringify({ new_password: newPw })
|
data: JSON.stringify({ new_password: newPw })
|
||||||
}).done(function() {
|
}).done(function() {
|
||||||
alert('Passwort geaendert!');
|
alert('Passwort geaendert.');
|
||||||
}).fail(function(err) {
|
}).fail(function(err) {
|
||||||
alert(err.responseJSON ? err.responseJSON.error : 'Fehler');
|
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() {
|
$('#sync-site').click(function() {
|
||||||
if (!confirm('Neueste Version laden und Website neu starten?')) return;
|
$('#sync-status').text('Sync laeuft...');
|
||||||
|
$('#sync-site').prop('disabled', true);
|
||||||
var $button = $(this);
|
|
||||||
var $status = $('#sync-status');
|
|
||||||
$button.prop('disabled', true);
|
|
||||||
$status.text('Sync laeuft...');
|
|
||||||
|
|
||||||
$.post('/api/admin/sync')
|
$.post('/api/admin/sync')
|
||||||
.done(function(res) {
|
.done(function(resp) {
|
||||||
$status.text(res.message || 'Sync gestartet. Website startet neu...');
|
$('#sync-status').text(resp.message || 'Sync gestartet.');
|
||||||
window.setTimeout(function() {
|
|
||||||
window.location.reload();
|
|
||||||
}, 12000);
|
|
||||||
})
|
})
|
||||||
.fail(function(err) {
|
.fail(function(xhr) {
|
||||||
var msg = err.responseJSON ? err.responseJSON.error : 'Sync fehlgeschlagen';
|
var msg = (xhr.responseJSON && (xhr.responseJSON.error || xhr.responseJSON.details)) || 'Sync fehlgeschlagen';
|
||||||
$status.text(msg);
|
$('#sync-status').text(msg);
|
||||||
$button.prop('disabled', false);
|
})
|
||||||
|
.always(function() {
|
||||||
|
$('#sync-site').prop('disabled', false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ==================== EIGENES PASSWORT ====================
|
|
||||||
|
|
||||||
$('#admin-pw-form').submit(function(e) {
|
$('#admin-pw-form').submit(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
$.post('/api/auth/change-password', {
|
$.post('/api/auth/change-password', {
|
||||||
old_password: $('#admin-pw-old').val(),
|
old_password: $('#admin-pw-old').val(),
|
||||||
new_password: $('#admin-pw-new').val()
|
new_password: $('#admin-pw-new').val()
|
||||||
}).done(function() {
|
}).done(function() {
|
||||||
alert('Passwort geaendert!');
|
alert('Passwort geaendert.');
|
||||||
$('#admin-pw-old, #admin-pw-new').val('');
|
$('#admin-pw-old, #admin-pw-new').val('');
|
||||||
}).fail(function(err) {
|
}).fail(function(xhr) {
|
||||||
alert(err.responseJSON ? err.responseJSON.error : 'Fehler');
|
alert(xhr.responseJSON ? xhr.responseJSON.error : 'Fehler');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -153,10 +153,17 @@
|
|||||||
exifDatas = {};
|
exifDatas = {};
|
||||||
|
|
||||||
var imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
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({
|
$.ajax({
|
||||||
url: '/images/fulls/',
|
url: galleryUrl,
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
cache: false,
|
cache: false,
|
||||||
success: function(files) {
|
success: function(files) {
|
||||||
@@ -296,6 +303,10 @@
|
|||||||
// ==================== SELECTION MODE ====================
|
// ==================== SELECTION MODE ====================
|
||||||
|
|
||||||
function initSelectionMode(allImages) {
|
function initSelectionMode(allImages) {
|
||||||
|
$('#selection-toolbar').remove();
|
||||||
|
$('#select-mode-toggle').closest('li').remove();
|
||||||
|
$('.folder-download-btn').closest('li').remove();
|
||||||
|
|
||||||
var selectedImages = [];
|
var selectedImages = [];
|
||||||
var selectionActive = false;
|
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
|
// Galerie laden
|
||||||
loadGallery();
|
loadGallery();
|
||||||
|
|
||||||
@@ -509,6 +546,7 @@
|
|||||||
$('#login-username, #login-password, #login-submit').hide();
|
$('#login-username, #login-password, #login-submit').hide();
|
||||||
$('#logout-btn').show().css('display', 'inline-block');
|
$('#logout-btn').show().css('display', 'inline-block');
|
||||||
$('#pw-change-section').show();
|
$('#pw-change-section').show();
|
||||||
|
loadAvailableFolders();
|
||||||
if(u.role === 'admin') {
|
if(u.role === 'admin') {
|
||||||
$('#admin-link').html('<a href="/admin.html" class="button">Admin-Dashboard</a>');
|
$('#admin-link').html('<a href="/admin.html" class="button">Admin-Dashboard</a>');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,40 @@
|
|||||||
color: _palette(fg);
|
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 Mode ==================== */
|
||||||
|
|
||||||
#selection-toolbar {
|
#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) {
|
function isSafePath(baseDir, requestedPath) {
|
||||||
const resolvedBase = path.resolve(baseDir);
|
const resolvedBase = path.resolve(baseDir);
|
||||||
const resolvedPath = path.resolve(resolvedBase, requestedPath);
|
const resolvedPath = path.resolve(resolvedBase, requestedPath);
|
||||||
@@ -206,29 +218,52 @@ app.get('/images/fulls/', (req, res) => {
|
|||||||
const rootImages = listImagesInDir(FULLS_DIR).map(name => ({
|
const rootImages = listImagesInDir(FULLS_DIR).map(name => ({
|
||||||
name, type: 'file', folder: null
|
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
|
// Nicht eingeloggt: nur Root-Bilder
|
||||||
if (!req.user) return res.json(rootImages);
|
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);
|
const folders = listSubfolders(FULLS_DIR);
|
||||||
|
|
||||||
// Admin: Nur Root-Bilder (damit die Startseite sauber bleibt)
|
|
||||||
if (req.user.role === 'admin') {
|
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) => {
|
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 assignedFolders = err ? [] : rows.map(r => r.folder_name);
|
||||||
const result = [];
|
return res.json(assignedFolders.filter(folder => folders.includes(folder)).map(getFolderInfo));
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -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 })));
|
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
|
db.all('SELECT user_id, folder_name FROM user_folders ORDER BY folder_name', (folderErr, folderRows) => {
|
||||||
const result = rows.map(u => {
|
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 };
|
const out = { ...u };
|
||||||
if (u.role === 'client' && u.created_at) {
|
if (u.role === 'client' && u.created_at) {
|
||||||
const expires = new Date(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.expires_at = expires.toISOString();
|
||||||
out.expired = new Date() > expires;
|
out.expired = new Date() > expires;
|
||||||
}
|
}
|
||||||
|
out.folders = foldersByUser[u.id] || [];
|
||||||
return out;
|
return out;
|
||||||
|
});
|
||||||
|
res.json(result);
|
||||||
});
|
});
|
||||||
res.json(result);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -298,7 +344,49 @@ app.put('/api/admin/users/:id/password', requireAdmin, (req, res) => {
|
|||||||
|
|
||||||
// Ordner-Verwaltung
|
// Ordner-Verwaltung
|
||||||
app.get('/api/admin/folders', requireAdmin, (req, res) => {
|
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) => {
|
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) => {
|
app.post('/api/admin/assign', requireAdmin, (req, res) => {
|
||||||
const { user_id, folders } = req.body;
|
const { user_id, folders } = req.body;
|
||||||
if (!user_id) return res.status(400).json({ error: 'user_id erforderlich' });
|
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.serialize(() => {
|
||||||
db.run('DELETE FROM user_folders WHERE user_id = ?', [user_id]);
|
db.run('DELETE FROM user_folders WHERE user_id = ?', [user_id]);
|
||||||
const stmt = db.prepare('INSERT INTO user_folders (user_id, folder_name) VALUES (?, ?)');
|
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 {
|
} else {
|
||||||
// Ordner-Bild: nur wenn User den Ordner zugewiesen hat
|
// Ordner-Bild: nur wenn User den Ordner zugewiesen hat
|
||||||
if (!req.user) return checkAccess(false);
|
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 = ?',
|
db.get('SELECT 1 FROM user_folders WHERE user_id = ? AND folder_name = ?',
|
||||||
[req.user.id, folder], (err, row) => {
|
[req.user.id, folder], (err, row) => {
|
||||||
if (err || !row) return checkAccess(false);
|
if (err || !row) return checkAccess(false);
|
||||||
@@ -453,30 +548,37 @@ app.get('/api/download/folder/:folderName', (req, res) => {
|
|||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
return res.status(401).json({ error: 'Nicht eingeloggt' });
|
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 = ?',
|
db.get('SELECT 1 FROM user_folders WHERE user_id = ? AND folder_name = ?',
|
||||||
[req.user.id, folderName], (err, row) => {
|
[req.user.id, folderName], (err, row) => {
|
||||||
if (err || !row) {
|
if (err || !row) {
|
||||||
return res.status(403).json({ error: 'Kein Zugriff auf diesen Ordner' });
|
return res.status(403).json({ error: 'Kein Zugriff auf diesen Ordner' });
|
||||||
}
|
}
|
||||||
const folderPath = path.join(FULLS_DIR, folderName);
|
return sendFolderDownload(folderName, res);
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 ====================
|
// ==================== STATIC FILES ====================
|
||||||
|
|
||||||
app.get(/^\/images\/(fulls|thumbs)\/(.+)$/, serveProtectedImage);
|
app.get(/^\/images\/(fulls|thumbs)\/(.+)$/, serveProtectedImage);
|
||||||
|
|||||||
Reference in New Issue
Block a user