devpi-admin 1.1.0__tar.gz → 1.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {devpi_admin-1.1.0 → devpi_admin-1.1.2}/PKG-INFO +1 -1
- {devpi_admin-1.1.0 → devpi_admin-1.1.2}/src/devpi_admin/_version.py +2 -2
- {devpi_admin-1.1.0 → devpi_admin-1.1.2}/src/devpi_admin/main.py +16 -0
- {devpi_admin-1.1.0 → devpi_admin-1.1.2}/src/devpi_admin/static/css/style.css +56 -3
- devpi_admin-1.1.2/src/devpi_admin/static/favicon.svg +10 -0
- {devpi_admin-1.1.0 → devpi_admin-1.1.2}/src/devpi_admin/static/index.html +1 -0
- {devpi_admin-1.1.0 → devpi_admin-1.1.2}/src/devpi_admin/static/js/api.js +7 -0
- {devpi_admin-1.1.0 → devpi_admin-1.1.2}/src/devpi_admin/static/js/app.js +199 -83
- {devpi_admin-1.1.0 → devpi_admin-1.1.2}/.gitignore +0 -0
- {devpi_admin-1.1.0 → devpi_admin-1.1.2}/LICENSE +0 -0
- {devpi_admin-1.1.0 → devpi_admin-1.1.2}/README.md +0 -0
- {devpi_admin-1.1.0 → devpi_admin-1.1.2}/pyproject.toml +0 -0
- {devpi_admin-1.1.0 → devpi_admin-1.1.2}/src/devpi_admin/__init__.py +0 -0
- {devpi_admin-1.1.0 → devpi_admin-1.1.2}/src/devpi_admin/static/js/marked.min.js +0 -0
- {devpi_admin-1.1.0 → devpi_admin-1.1.2}/src/devpi_admin/static/js/theme.js +0 -0
- {devpi_admin-1.1.0 → devpi_admin-1.1.2}/tests/__init__.py +0 -0
- {devpi_admin-1.1.0 → devpi_admin-1.1.2}/tests/test_cached_versions.py +0 -0
- {devpi_admin-1.1.0 → devpi_admin-1.1.2}/tests/test_helpers.py +0 -0
- {devpi_admin-1.1.0 → devpi_admin-1.1.2}/tests/test_hooks.py +0 -0
- {devpi_admin-1.1.0 → devpi_admin-1.1.2}/tests/test_json_safe.py +0 -0
- {devpi_admin-1.1.0 → devpi_admin-1.1.2}/tests/test_package.py +0 -0
- {devpi_admin-1.1.0 → devpi_admin-1.1.2}/tests/test_tween.py +0 -0
- {devpi_admin-1.1.0 → devpi_admin-1.1.2}/tests/test_wants_html.py +0 -0
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.1.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 1,
|
|
21
|
+
__version__ = version = '1.1.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 1, 2)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -44,6 +44,14 @@ def devpiserver_pyramid_configure(config, pyramid_config):
|
|
|
44
44
|
lambda request: HTTPFound("/+admin/"),
|
|
45
45
|
route_name="devpi_admin_spa_noslash")
|
|
46
46
|
|
|
47
|
+
# Session validity check.
|
|
48
|
+
pyramid_config.add_route(
|
|
49
|
+
"devpi_admin_session",
|
|
50
|
+
"/+admin-api/session")
|
|
51
|
+
pyramid_config.add_view(
|
|
52
|
+
_session_view, route_name="devpi_admin_session",
|
|
53
|
+
request_method="GET")
|
|
54
|
+
|
|
47
55
|
# Cached packages API for mirror indexes.
|
|
48
56
|
pyramid_config.add_route(
|
|
49
57
|
"devpi_admin_cached",
|
|
@@ -82,6 +90,14 @@ def _serve_index(request):
|
|
|
82
90
|
content_type="text/html")
|
|
83
91
|
|
|
84
92
|
|
|
93
|
+
def _session_view(request):
|
|
94
|
+
"""Return whether the current request carries a valid authenticated session."""
|
|
95
|
+
user = request.authenticated_userid
|
|
96
|
+
if user:
|
|
97
|
+
return _json_response({"valid": True, "user": user})
|
|
98
|
+
raise HTTPForbidden(json_body={"valid": False, "error": "not authenticated"})
|
|
99
|
+
|
|
100
|
+
|
|
85
101
|
def _get_stage_or_404(xom, user, index):
|
|
86
102
|
"""Return stage object or raise HTTPNotFound."""
|
|
87
103
|
stage = xom.model.getstage(user, index)
|
|
@@ -196,17 +196,33 @@ body {
|
|
|
196
196
|
.user-btn-name {
|
|
197
197
|
padding: 4px 10px;
|
|
198
198
|
font-weight: 600;
|
|
199
|
-
color:
|
|
199
|
+
color: #22c55e;
|
|
200
200
|
background: var(--bg);
|
|
201
|
+
cursor: pointer;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.user-btn-name:hover {
|
|
205
|
+
background: var(--bg-alt);
|
|
206
|
+
opacity: 0.8;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.user-btn.is-root .user-btn-name {
|
|
210
|
+
color: #f59e0b;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.user-btn-sep {
|
|
214
|
+
padding: 4px 0;
|
|
215
|
+
color: var(--border);
|
|
216
|
+
font-size: 0.85em;
|
|
217
|
+
user-select: none;
|
|
201
218
|
}
|
|
202
219
|
|
|
203
220
|
.user-btn-action {
|
|
204
221
|
padding: 4px 10px;
|
|
205
222
|
color: var(--text-muted);
|
|
206
|
-
border-left: 1px solid var(--border);
|
|
207
223
|
}
|
|
208
224
|
|
|
209
|
-
.user-btn:hover
|
|
225
|
+
.user-btn-action:hover {
|
|
210
226
|
background: var(--error);
|
|
211
227
|
color: #fff;
|
|
212
228
|
}
|
|
@@ -351,6 +367,43 @@ body {
|
|
|
351
367
|
flex-shrink: 0;
|
|
352
368
|
}
|
|
353
369
|
|
|
370
|
+
/* --- User cards --- */
|
|
371
|
+
|
|
372
|
+
.user-card {
|
|
373
|
+
border-left-color: #22c55e;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.user-card.user-root {
|
|
377
|
+
border-left-color: #f59e0b;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.user-card .index-card-name {
|
|
381
|
+
color: #22c55e;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.user-card.user-root .index-card-name {
|
|
385
|
+
color: #f59e0b;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.user-card .kebab-menu {
|
|
389
|
+
margin-left: auto;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.user-card-indexes {
|
|
393
|
+
display: flex;
|
|
394
|
+
flex-wrap: wrap;
|
|
395
|
+
gap: 4px;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.user-card-indexes .tag {
|
|
399
|
+
text-decoration: none;
|
|
400
|
+
cursor: pointer;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.user-card-indexes .tag:hover {
|
|
404
|
+
opacity: 0.8;
|
|
405
|
+
}
|
|
406
|
+
|
|
354
407
|
/* --- Index cards --- */
|
|
355
408
|
|
|
356
409
|
.index-grid {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
2
|
+
<!-- Box bottom face -->
|
|
3
|
+
<polygon points="16,18 2,11 16,4 30,11" fill="#4a9eff" opacity="0.85"/>
|
|
4
|
+
<!-- Box left face -->
|
|
5
|
+
<polygon points="2,11 2,23 16,30 16,18" fill="#2563eb"/>
|
|
6
|
+
<!-- Box right face -->
|
|
7
|
+
<polygon points="30,11 30,23 16,30 16,18" fill="#1d4ed8"/>
|
|
8
|
+
<!-- Highlight stripe on top face -->
|
|
9
|
+
<polygon points="16,6 26,10.5 16,15 6,10.5" fill="#93c5fd" opacity="0.4"/>
|
|
10
|
+
</svg>
|
|
@@ -26,6 +26,13 @@ var Api = (function () {
|
|
|
26
26
|
}
|
|
27
27
|
return fetch(url, opts).then(function (res) {
|
|
28
28
|
if (res.status === 204) return null;
|
|
29
|
+
if (res.status === 401 && _user) {
|
|
30
|
+
logout();
|
|
31
|
+
if (typeof onSessionExpired === 'function') onSessionExpired();
|
|
32
|
+
var err = new Error('Session expired. Please log in again.');
|
|
33
|
+
err.status = 401;
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
29
36
|
return res.json().then(function (json) {
|
|
30
37
|
if (!res.ok) {
|
|
31
38
|
var msg = json.message || json.error || 'Request failed';
|
|
@@ -405,13 +405,7 @@
|
|
|
405
405
|
}
|
|
406
406
|
|
|
407
407
|
function handleApiError(err) {
|
|
408
|
-
|
|
409
|
-
Api.logout();
|
|
410
|
-
updateAuthUI();
|
|
411
|
-
showError(new Error('Session expired. Please log in again.'));
|
|
412
|
-
} else {
|
|
413
|
-
showError(err);
|
|
414
|
-
}
|
|
408
|
+
showError(err);
|
|
415
409
|
}
|
|
416
410
|
|
|
417
411
|
function updateNav() {
|
|
@@ -462,11 +456,15 @@
|
|
|
462
456
|
var user = Api.getUser();
|
|
463
457
|
if (user) {
|
|
464
458
|
clear(logoutBtn);
|
|
465
|
-
|
|
459
|
+
var nameSpan = el('span', {className: 'user-btn-name', textContent: user});
|
|
460
|
+
nameSpan.title = 'Change password';
|
|
461
|
+
logoutBtn.appendChild(nameSpan);
|
|
462
|
+
logoutBtn.appendChild(el('span', {className: 'user-btn-sep', textContent: '|'}));
|
|
466
463
|
logoutBtn.appendChild(el('span', {className: 'user-btn-action', textContent: 'Logout'}));
|
|
467
464
|
loginBtn.hidden = true;
|
|
468
465
|
logoutBtn.hidden = false;
|
|
469
|
-
|
|
466
|
+
logoutBtn.classList.toggle('is-root', user === 'root');
|
|
467
|
+
navUsers.hidden = user !== 'root';
|
|
470
468
|
document.body.classList.add('authenticated');
|
|
471
469
|
} else {
|
|
472
470
|
loginBtn.hidden = false;
|
|
@@ -515,6 +513,7 @@
|
|
|
515
513
|
closeModal();
|
|
516
514
|
updateAuthUI();
|
|
517
515
|
navigate();
|
|
516
|
+
_triggerPasswordSave(user, pass);
|
|
518
517
|
})
|
|
519
518
|
.catch(showModalError);
|
|
520
519
|
}
|
|
@@ -545,7 +544,31 @@
|
|
|
545
544
|
});
|
|
546
545
|
}
|
|
547
546
|
|
|
548
|
-
|
|
547
|
+
// Reload current view when clicking an already-active nav link
|
|
548
|
+
document.getElementById('main-nav').addEventListener('click', function (e) {
|
|
549
|
+
if (e.target.tagName === 'A') {
|
|
550
|
+
var href = e.target.getAttribute('href') || '#';
|
|
551
|
+
var current = window.location.hash || '#';
|
|
552
|
+
if (href === current) {
|
|
553
|
+
e.preventDefault();
|
|
554
|
+
navigate();
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
logoutBtn.addEventListener('click', function (e) {
|
|
560
|
+
// Clicking the username part opens change-password modal
|
|
561
|
+
if (e.target.classList.contains('user-btn-name')) {
|
|
562
|
+
var user = Api.getUser();
|
|
563
|
+
if (user) {
|
|
564
|
+
fetchRoot().then(function (result) {
|
|
565
|
+
showUserModal(user, result[user] || {});
|
|
566
|
+
}).catch(function () {
|
|
567
|
+
showUserModal(user, {});
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
549
572
|
Api.logout();
|
|
550
573
|
updateAuthUI();
|
|
551
574
|
window.location.hash = '#';
|
|
@@ -581,7 +604,7 @@
|
|
|
581
604
|
} else if ((m = path.match(/^package\/([^/]+\/[^/]+)\/(.+)$/))) {
|
|
582
605
|
loadPackageDetail(m[1], m[2], query.version);
|
|
583
606
|
} else if (path === 'users') {
|
|
584
|
-
if (!Api.getUser()) {
|
|
607
|
+
if (!Api.getUser() || Api.getUser() !== 'root') {
|
|
585
608
|
loadStatus();
|
|
586
609
|
return;
|
|
587
610
|
}
|
|
@@ -603,73 +626,82 @@
|
|
|
603
626
|
showLoading();
|
|
604
627
|
fetchRoot().then(function (result) {
|
|
605
628
|
clear(content);
|
|
606
|
-
var
|
|
607
|
-
|
|
608
|
-
el('button', {
|
|
629
|
+
var headerChildren = [el('h2', {textContent: 'Users'})];
|
|
630
|
+
if (Api.getUser() === 'root') {
|
|
631
|
+
headerChildren.push(el('button', {
|
|
609
632
|
className: 'btn btn-primary',
|
|
610
633
|
textContent: '+ New User',
|
|
611
634
|
onclick: function () { showUserModal(null, null); },
|
|
612
|
-
})
|
|
613
|
-
|
|
635
|
+
}));
|
|
636
|
+
}
|
|
637
|
+
var header = el('div', {className: 'view-header'}, headerChildren);
|
|
614
638
|
content.appendChild(header);
|
|
615
639
|
|
|
616
640
|
var userNames = getAllUserNames(result);
|
|
617
|
-
var
|
|
618
|
-
var thead = el('thead');
|
|
619
|
-
thead.appendChild(el('tr', null, [
|
|
620
|
-
el('th', {textContent: 'User'}),
|
|
621
|
-
el('th', {textContent: 'Email'}),
|
|
622
|
-
el('th', {textContent: 'Indexes'}),
|
|
623
|
-
el('th', {textContent: 'Actions'}),
|
|
624
|
-
]));
|
|
625
|
-
table.appendChild(thead);
|
|
626
|
-
var tbody = el('tbody');
|
|
641
|
+
var grid = el('div', {className: 'index-grid'});
|
|
627
642
|
for (var i = 0; i < userNames.length; i++) {
|
|
628
643
|
(function (name) {
|
|
629
644
|
var info = result[name];
|
|
630
645
|
var indexes = info.indexes || {};
|
|
631
|
-
var indexNames = Object.keys(indexes);
|
|
632
|
-
var
|
|
633
|
-
var
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
646
|
+
var indexNames = Object.keys(indexes).sort();
|
|
647
|
+
var currentUser = Api.getUser();
|
|
648
|
+
var canEdit = currentUser === name || currentUser === 'root';
|
|
649
|
+
|
|
650
|
+
var card = el('div', {className: 'index-card user-card' + (name === 'root' ? ' user-root' : '')});
|
|
651
|
+
|
|
652
|
+
// Card head: username + kebab menu
|
|
653
|
+
var cardHead = el('div', {className: 'index-card-head'});
|
|
654
|
+
cardHead.appendChild(el('a', {
|
|
655
|
+
href: '#indexes/' + name,
|
|
656
|
+
className: 'index-card-name',
|
|
657
|
+
textContent: name,
|
|
658
|
+
}));
|
|
659
|
+
var menuItems = [];
|
|
660
|
+
if (canEdit) {
|
|
661
|
+
menuItems.push({label: 'Edit', onclick: function () { closeAllKebabs(); showUserModal(name, info); }});
|
|
662
|
+
}
|
|
663
|
+
if (currentUser === 'root' && name !== 'root') {
|
|
664
|
+
menuItems.push({label: 'Delete', danger: true, onclick: function () { closeAllKebabs(); deleteUser(name); }});
|
|
665
|
+
}
|
|
666
|
+
if (menuItems.length) {
|
|
667
|
+
cardHead.appendChild(buildKebabMenu(menuItems));
|
|
668
|
+
}
|
|
669
|
+
card.appendChild(cardHead);
|
|
670
|
+
|
|
671
|
+
// Details
|
|
672
|
+
var details = el('div', {className: 'index-card-details'});
|
|
673
|
+
if (info.email) {
|
|
674
|
+
details.appendChild(el('div', {className: 'index-card-row'}, [
|
|
675
|
+
el('span', {className: 'index-card-label', textContent: 'Email'}),
|
|
676
|
+
el('span', {textContent: info.email}),
|
|
677
|
+
]));
|
|
678
|
+
}
|
|
679
|
+
if (indexNames.length) {
|
|
680
|
+
var tagsWrap = el('div', {className: 'index-card-row'});
|
|
681
|
+
tagsWrap.appendChild(el('span', {className: 'index-card-label', textContent: 'Indexes'}));
|
|
682
|
+
var tagsGroup = el('div', {className: 'user-card-indexes'});
|
|
683
|
+
for (var j = 0; j < indexNames.length; j++) {
|
|
684
|
+
var idx = indexes[indexNames[j]];
|
|
685
|
+
var tagClass = 'tag';
|
|
686
|
+
if (idx.type === 'mirror') tagClass += ' tag-mirror';
|
|
687
|
+
else if (idx.volatile) tagClass += ' tag-volatile';
|
|
688
|
+
tagsGroup.appendChild(el('a', {
|
|
689
|
+
href: '#packages/' + name + '/' + indexNames[j],
|
|
641
690
|
className: tagClass,
|
|
642
691
|
textContent: indexNames[j],
|
|
643
|
-
title: idx.type +
|
|
644
|
-
(idx.
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
);
|
|
692
|
+
title: idx.type + (idx.volatile ? ', volatile' : '') +
|
|
693
|
+
(idx.bases && idx.bases.length ? ', bases: ' + idx.bases.join(', ') : ''),
|
|
694
|
+
}));
|
|
695
|
+
}
|
|
696
|
+
tagsWrap.appendChild(tagsGroup);
|
|
697
|
+
details.appendChild(tagsWrap);
|
|
648
698
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
className: 'btn btn-small',
|
|
653
|
-
textContent: 'Edit',
|
|
654
|
-
onclick: function () { showUserModal(name, info); },
|
|
655
|
-
}),
|
|
656
|
-
el('button', {
|
|
657
|
-
className: 'btn btn-small btn-danger',
|
|
658
|
-
textContent: 'Delete',
|
|
659
|
-
onclick: function () { deleteUser(name); },
|
|
660
|
-
}),
|
|
661
|
-
]);
|
|
662
|
-
var tr = el('tr', null, [
|
|
663
|
-
el('td', {textContent: name}),
|
|
664
|
-
el('td', {textContent: info.email || ''}),
|
|
665
|
-
indexCell,
|
|
666
|
-
actions,
|
|
667
|
-
]);
|
|
668
|
-
tbody.appendChild(tr);
|
|
699
|
+
card.appendChild(details);
|
|
700
|
+
|
|
701
|
+
grid.appendChild(card);
|
|
669
702
|
})(userNames[i]);
|
|
670
703
|
}
|
|
671
|
-
|
|
672
|
-
content.appendChild(table);
|
|
704
|
+
content.appendChild(grid);
|
|
673
705
|
}).catch(handleApiError);
|
|
674
706
|
}
|
|
675
707
|
|
|
@@ -686,9 +718,30 @@
|
|
|
686
718
|
id: 'form-email',
|
|
687
719
|
value: (editInfo && editInfo.email) || '',
|
|
688
720
|
})));
|
|
721
|
+
var isSelf = isEdit && editName === Api.getUser();
|
|
722
|
+
// Hidden username input so browser associates saved password correctly
|
|
723
|
+
if (isSelf) {
|
|
724
|
+
var hiddenUser = el('input', {type: 'text', id: 'form-hidden-username'});
|
|
725
|
+
hiddenUser.setAttribute('autocomplete', 'username');
|
|
726
|
+
hiddenUser.setAttribute('aria-hidden', 'true');
|
|
727
|
+
hiddenUser.style.display = 'none';
|
|
728
|
+
hiddenUser.value = editName;
|
|
729
|
+
body.appendChild(hiddenUser);
|
|
730
|
+
}
|
|
731
|
+
var pwInput;
|
|
732
|
+
if (isSelf) {
|
|
733
|
+
// Own password: type="password" + autocomplete so browser offers to save
|
|
734
|
+
pwInput = el('input', {type: 'password', id: 'form-password'});
|
|
735
|
+
pwInput.setAttribute('autocomplete', 'new-password');
|
|
736
|
+
} else {
|
|
737
|
+
// Other user: plain text — Safari won't offer to save text fields
|
|
738
|
+
pwInput = el('input', {type: 'text', id: 'form-password'});
|
|
739
|
+
pwInput.setAttribute('autocomplete', 'off');
|
|
740
|
+
pwInput.setAttribute('spellcheck', 'false');
|
|
741
|
+
}
|
|
689
742
|
body.appendChild(formGroup(
|
|
690
743
|
isEdit ? 'New Password (leave empty to keep)' : 'Password',
|
|
691
|
-
|
|
744
|
+
pwInput
|
|
692
745
|
));
|
|
693
746
|
},
|
|
694
747
|
[
|
|
@@ -722,7 +775,8 @@
|
|
|
722
775
|
}
|
|
723
776
|
}
|
|
724
777
|
var email = document.getElementById('form-email').value.trim();
|
|
725
|
-
var
|
|
778
|
+
var passwordEl = document.getElementById('form-password');
|
|
779
|
+
var password = passwordEl ? passwordEl.value : null;
|
|
726
780
|
|
|
727
781
|
if (email) data.email = email;
|
|
728
782
|
if (password) data.password = password;
|
|
@@ -732,12 +786,43 @@
|
|
|
732
786
|
var method = isEdit ? Api.patch : Api.put;
|
|
733
787
|
method(url, data)
|
|
734
788
|
.then(function () {
|
|
735
|
-
|
|
789
|
+
if (password && isEdit && editName === Api.getUser()) {
|
|
790
|
+
// Own password — trigger "Save Password" before closing modal
|
|
791
|
+
closeModal();
|
|
792
|
+
_triggerPasswordSave(editName, password, 'users');
|
|
793
|
+
} else {
|
|
794
|
+
// Other user — wipe field value before close so browser has nothing to save
|
|
795
|
+
if (passwordEl) passwordEl.value = '';
|
|
796
|
+
closeModal();
|
|
797
|
+
}
|
|
736
798
|
loadUsers();
|
|
737
799
|
})
|
|
738
800
|
.catch(showModalError);
|
|
739
801
|
}
|
|
740
802
|
|
|
803
|
+
function _triggerPasswordSave(user, password, hash) {
|
|
804
|
+
var form = document.createElement('form');
|
|
805
|
+
form.method = 'post';
|
|
806
|
+
form.action = '/+admin' + (hash ? '#' + hash : ''); // devpi 302-redirects to /+admin/ — PRG pattern, Ctrl+R won't resubmit
|
|
807
|
+
form.style.cssText = 'display:none';
|
|
808
|
+
var u = document.createElement('input');
|
|
809
|
+
u.type = 'text';
|
|
810
|
+
u.name = 'username';
|
|
811
|
+
u.setAttribute('autocomplete', 'username');
|
|
812
|
+
u.value = user;
|
|
813
|
+
var p = document.createElement('input');
|
|
814
|
+
p.type = 'password';
|
|
815
|
+
p.name = 'password';
|
|
816
|
+
p.setAttribute('autocomplete', 'current-password');
|
|
817
|
+
p.value = password;
|
|
818
|
+
form.appendChild(u);
|
|
819
|
+
form.appendChild(p);
|
|
820
|
+
form.addEventListener('submit', function (e) { e.preventDefault(); });
|
|
821
|
+
document.body.appendChild(form);
|
|
822
|
+
form.submit();
|
|
823
|
+
document.body.removeChild(form);
|
|
824
|
+
}
|
|
825
|
+
|
|
741
826
|
function deleteUser(name) {
|
|
742
827
|
if (!confirm('Delete user "' + name + '"? This will also delete all their indexes.')) {
|
|
743
828
|
return;
|
|
@@ -774,16 +859,17 @@
|
|
|
774
859
|
headingChildren.push(el('a', {href: '#indexes/' + _filterUser, textContent: _filterUser}));
|
|
775
860
|
}
|
|
776
861
|
var heading = el('h2', {className: 'page-heading'}, headingChildren);
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
el('button', {
|
|
780
|
-
className: 'btn btn-primary
|
|
862
|
+
var viewHeaderChildren = [heading];
|
|
863
|
+
if (Api.getUser() === 'root') {
|
|
864
|
+
viewHeaderChildren.push(el('button', {
|
|
865
|
+
className: 'btn btn-primary',
|
|
781
866
|
textContent: '+ New Index',
|
|
782
867
|
onclick: function () {
|
|
783
868
|
showIndexModal(null, result, _filterUser);
|
|
784
869
|
},
|
|
785
|
-
})
|
|
786
|
-
|
|
870
|
+
}));
|
|
871
|
+
}
|
|
872
|
+
headerContainer.appendChild(el('div', {className: 'view-header'}, viewHeaderChildren));
|
|
787
873
|
|
|
788
874
|
var container = document.getElementById('indexes-content');
|
|
789
875
|
clear(container);
|
|
@@ -872,15 +958,19 @@
|
|
|
872
958
|
]));
|
|
873
959
|
}
|
|
874
960
|
}
|
|
961
|
+
|
|
875
962
|
card.appendChild(details);
|
|
876
963
|
|
|
877
964
|
card.appendChild(buildIndexPipBlock(idx._full));
|
|
878
965
|
|
|
879
|
-
// Kebab menu
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
966
|
+
// Kebab menu — only for root or the index owner
|
|
967
|
+
var loggedIn = Api.getUser();
|
|
968
|
+
if (loggedIn === 'root' || loggedIn === idx._user) {
|
|
969
|
+
cardHead.appendChild(buildKebabMenu([
|
|
970
|
+
{label: 'Edit', onclick: function () { closeAllKebabs(); showIndexModal(idx, result); }},
|
|
971
|
+
{label: 'Delete', danger: true, onclick: function () { closeAllKebabs(); deleteIndex(idx._full); }},
|
|
972
|
+
]));
|
|
973
|
+
}
|
|
884
974
|
|
|
885
975
|
grid.appendChild(card);
|
|
886
976
|
})(indexes[i]);
|
|
@@ -940,14 +1030,16 @@
|
|
|
940
1030
|
isEdit ? 'Edit Index: ' + editIdx._full : 'New Index',
|
|
941
1031
|
function (body) {
|
|
942
1032
|
if (!isEdit) {
|
|
1033
|
+
var currentUser = Api.getUser();
|
|
1034
|
+
var owners = currentUser === 'root' ? userNames : [currentUser];
|
|
943
1035
|
var ownerSelect = el('select', {id: 'form-owner'});
|
|
944
|
-
for (var u = 0; u <
|
|
1036
|
+
for (var u = 0; u < owners.length; u++) {
|
|
945
1037
|
ownerSelect.appendChild(el('option', {
|
|
946
|
-
value:
|
|
947
|
-
textContent:
|
|
1038
|
+
value: owners[u],
|
|
1039
|
+
textContent: owners[u],
|
|
948
1040
|
}));
|
|
949
1041
|
}
|
|
950
|
-
ownerSelect.value = preOwner ||
|
|
1042
|
+
ownerSelect.value = preOwner || currentUser;
|
|
951
1043
|
body.appendChild(formGroup('Owner', ownerSelect));
|
|
952
1044
|
body.appendChild(formGroup('Index Name', el('input', {type: 'text', id: 'form-index-name'})));
|
|
953
1045
|
}
|
|
@@ -992,10 +1084,10 @@
|
|
|
992
1084
|
]),
|
|
993
1085
|
]));
|
|
994
1086
|
|
|
995
|
-
var
|
|
1087
|
+
var aclUploadInitial = isEdit ? (editIdx.acl_upload || []) : [Api.getUser()];
|
|
996
1088
|
stageFields.appendChild(el('div', {className: 'form-group'}, [
|
|
997
1089
|
el('label', {textContent: 'ACL Upload'}),
|
|
998
|
-
buildTagPicker('form-acl-upload',
|
|
1090
|
+
buildTagPicker('form-acl-upload', aclUploadInitial, userNames, [':ANONYMOUS:']),
|
|
999
1091
|
]));
|
|
1000
1092
|
|
|
1001
1093
|
mirrorFields.appendChild(formGroup('Mirror URL', el('input', {
|
|
@@ -1892,8 +1984,32 @@
|
|
|
1892
1984
|
|
|
1893
1985
|
// --- Init ---
|
|
1894
1986
|
|
|
1987
|
+
window.onSessionExpired = function () {
|
|
1988
|
+
updateAuthUI();
|
|
1989
|
+
closeModal();
|
|
1990
|
+
showError(new Error('Session expired. Please log in again.'));
|
|
1991
|
+
};
|
|
1992
|
+
|
|
1895
1993
|
Api.restore();
|
|
1896
1994
|
updateAuthUI();
|
|
1897
1995
|
updateNav();
|
|
1898
1996
|
navigate();
|
|
1997
|
+
|
|
1998
|
+
var _sessionCheckReady = false;
|
|
1999
|
+
setTimeout(function () { _sessionCheckReady = true; }, 2000);
|
|
2000
|
+
|
|
2001
|
+
function checkSession() {
|
|
2002
|
+
if (!_sessionCheckReady || !Api.getUser()) return;
|
|
2003
|
+
Api.get('/+admin-api/session').catch(function (err) {
|
|
2004
|
+
if (err.status === 403 || err.status === 401) {
|
|
2005
|
+
Api.logout();
|
|
2006
|
+
updateAuthUI();
|
|
2007
|
+
showError(new Error('Session expired. Please log in again.'));
|
|
2008
|
+
}
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
document.addEventListener('visibilitychange', function () {
|
|
2012
|
+
if (document.visibilityState === 'visible') checkSession();
|
|
2013
|
+
});
|
|
2014
|
+
window.addEventListener('focus', checkSession);
|
|
1899
2015
|
})();
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|