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.
Files changed (23) hide show
  1. {devpi_admin-1.1.0 → devpi_admin-1.1.2}/PKG-INFO +1 -1
  2. {devpi_admin-1.1.0 → devpi_admin-1.1.2}/src/devpi_admin/_version.py +2 -2
  3. {devpi_admin-1.1.0 → devpi_admin-1.1.2}/src/devpi_admin/main.py +16 -0
  4. {devpi_admin-1.1.0 → devpi_admin-1.1.2}/src/devpi_admin/static/css/style.css +56 -3
  5. devpi_admin-1.1.2/src/devpi_admin/static/favicon.svg +10 -0
  6. {devpi_admin-1.1.0 → devpi_admin-1.1.2}/src/devpi_admin/static/index.html +1 -0
  7. {devpi_admin-1.1.0 → devpi_admin-1.1.2}/src/devpi_admin/static/js/api.js +7 -0
  8. {devpi_admin-1.1.0 → devpi_admin-1.1.2}/src/devpi_admin/static/js/app.js +199 -83
  9. {devpi_admin-1.1.0 → devpi_admin-1.1.2}/.gitignore +0 -0
  10. {devpi_admin-1.1.0 → devpi_admin-1.1.2}/LICENSE +0 -0
  11. {devpi_admin-1.1.0 → devpi_admin-1.1.2}/README.md +0 -0
  12. {devpi_admin-1.1.0 → devpi_admin-1.1.2}/pyproject.toml +0 -0
  13. {devpi_admin-1.1.0 → devpi_admin-1.1.2}/src/devpi_admin/__init__.py +0 -0
  14. {devpi_admin-1.1.0 → devpi_admin-1.1.2}/src/devpi_admin/static/js/marked.min.js +0 -0
  15. {devpi_admin-1.1.0 → devpi_admin-1.1.2}/src/devpi_admin/static/js/theme.js +0 -0
  16. {devpi_admin-1.1.0 → devpi_admin-1.1.2}/tests/__init__.py +0 -0
  17. {devpi_admin-1.1.0 → devpi_admin-1.1.2}/tests/test_cached_versions.py +0 -0
  18. {devpi_admin-1.1.0 → devpi_admin-1.1.2}/tests/test_helpers.py +0 -0
  19. {devpi_admin-1.1.0 → devpi_admin-1.1.2}/tests/test_hooks.py +0 -0
  20. {devpi_admin-1.1.0 → devpi_admin-1.1.2}/tests/test_json_safe.py +0 -0
  21. {devpi_admin-1.1.0 → devpi_admin-1.1.2}/tests/test_package.py +0 -0
  22. {devpi_admin-1.1.0 → devpi_admin-1.1.2}/tests/test_tween.py +0 -0
  23. {devpi_admin-1.1.0 → devpi_admin-1.1.2}/tests/test_wants_html.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devpi-admin
3
- Version: 1.1.0
3
+ Version: 1.1.2
4
4
  Summary: Modern web UI plugin for devpi-server — drop-in replacement for devpi-web
5
5
  Author-email: Pavel Revak <pavelrevak@gmail.com>
6
6
  License: MIT
@@ -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.0'
22
- __version_tuple__ = version_tuple = (1, 1, 0)
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: var(--text);
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 .user-btn-action {
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>
@@ -4,6 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>devpi admin</title>
7
+ <link rel="icon" type="image/svg+xml" href="favicon.svg">
7
8
  <link rel="stylesheet" href="css/style.css">
8
9
  </head>
9
10
  <body>
@@ -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
- if (err && err.status === 401) {
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
- logoutBtn.appendChild(el('span', {className: 'user-btn-name', textContent: user}));
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
- navUsers.hidden = false;
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
- logoutBtn.addEventListener('click', function () {
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 header = el('div', {className: 'view-header'}, [
607
- el('h2', {textContent: 'Users'}),
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 table = el('table', {className: 'data-table'});
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 indexCell = el('td');
633
- var indexList = el('div', {className: 'index-list'});
634
- for (var j = 0; j < indexNames.length; j++) {
635
- var idx = indexes[indexNames[j]];
636
- var tagClass = 'tag';
637
- if (idx.type === 'mirror') tagClass += ' tag-mirror';
638
- if (idx.volatile) tagClass += ' tag-volatile';
639
- indexList.appendChild(
640
- el('span', {
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.volatile ? ', volatile' : '') +
645
- (idx.bases ? ', bases: ' + idx.bases.join(', ') : ''),
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
- indexCell.appendChild(indexList);
650
- var actions = el('td', {className: 'actions'}, [
651
- el('button', {
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
- table.appendChild(tbody);
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
- el('input', {type: 'password', id: 'form-password'})
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 password = document.getElementById('form-password').value;
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
- closeModal();
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
- headerContainer.appendChild(el('div', {className: 'view-header'}, [
778
- heading,
779
- el('button', {
780
- className: 'btn btn-primary auth-only',
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 (top-right)
880
- cardHead.appendChild(buildKebabMenu([
881
- {label: 'Edit', onclick: function () { closeAllKebabs(); showIndexModal(idx, result); }},
882
- {label: 'Delete', danger: true, onclick: function () { closeAllKebabs(); deleteIndex(idx._full); }},
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 < userNames.length; u++) {
1036
+ for (var u = 0; u < owners.length; u++) {
945
1037
  ownerSelect.appendChild(el('option', {
946
- value: userNames[u],
947
- textContent: userNames[u],
1038
+ value: owners[u],
1039
+ textContent: owners[u],
948
1040
  }));
949
1041
  }
950
- ownerSelect.value = preOwner || Api.getUser();
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 aclInitial = isEdit ? (editIdx.acl_upload || []) : [Api.getUser()];
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', aclInitial, userNames, [':ANONYMOUS:']),
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