@@ -9,6 +9,8 @@ User=host-manager |
||
| 9 | 9 |
Group=host-manager |
| 10 | 10 |
WorkingDirectory=/usr/local/xdev-host-manager |
| 11 | 11 |
EnvironmentFile=/etc/xdev/host-manager.env |
| 12 |
+ExecStartPre=+/usr/bin/install -d -o host-manager -g host-manager -m 0755 /usr/local/xdev-host-manager/var /usr/local/xdev-host-manager/backups |
|
| 13 |
+ExecStartPre=+/usr/bin/chown -R host-manager:host-manager /usr/local/xdev-host-manager/var /usr/local/xdev-host-manager/backups |
|
| 12 | 14 |
ExecStart=/usr/bin/perl /usr/local/xdev-host-manager/scripts/host_manager.pl --bind 127.0.0.1 --port 8088 |
| 13 | 15 |
Restart=on-failure |
| 14 | 16 |
RestartSec=3 |
@@ -96,6 +96,7 @@ if [[ "$DRY_RUN" -eq 1 ]]; then |
||
| 96 | 96 |
fi |
| 97 | 97 |
|
| 98 | 98 |
ssh "$TARGET_HOST" "mkdir -p '$TARGET_DIR/scripts' '$TARGET_DIR/deploy' '$TARGET_DIR/.doc' '$TARGET_DIR/var'" |
| 99 |
+ssh "$TARGET_HOST" "sudo -n install -d -o host-manager -g host-manager -m 0755 '$TARGET_DIR/var' '$TARGET_DIR/backups'" |
|
| 99 | 100 |
rsync "${rsync_args[@]}" scripts/ "$TARGET_HOST:$TARGET_DIR/scripts/"
|
| 100 | 101 |
rsync "${rsync_args[@]}" deploy/ "$TARGET_HOST:$TARGET_DIR/deploy/"
|
| 101 | 102 |
rsync "${rsync_args[@]}" .doc/ "$TARGET_HOST:$TARGET_DIR/.doc/"
|
@@ -2756,10 +2756,39 @@ sub app_html {
|
||
| 2756 | 2756 |
'/debug': 'debug', |
| 2757 | 2757 |
}; |
| 2758 | 2758 |
|
| 2759 |
+ function isAuthLost(error) {
|
|
| 2760 |
+ return !!(error && error.authLost); |
|
| 2761 |
+ } |
|
| 2762 |
+ |
|
| 2763 |
+ function authLostError(message) {
|
|
| 2764 |
+ const error = new Error(message || 'Sesiunea a expirat. Autentifica-te din nou.'); |
|
| 2765 |
+ error.authLost = true; |
|
| 2766 |
+ return error; |
|
| 2767 |
+ } |
|
| 2768 |
+ |
|
| 2769 |
+ function handleAuthLost(message) {
|
|
| 2770 |
+ state.authenticated = false; |
|
| 2771 |
+ msg('');
|
|
| 2772 |
+ showLogin(message || 'Sesiunea a expirat. Autentifica-te din nou.'); |
|
| 2773 |
+ } |
|
| 2774 |
+ |
|
| 2759 | 2775 |
async function api(path, options = {}) {
|
| 2760 | 2776 |
const res = await fetch(path, options); |
| 2761 |
- const body = await res.json(); |
|
| 2762 |
- if (!res.ok) throw new Error(body.error || res.statusText); |
|
| 2777 |
+ let body = {};
|
|
| 2778 |
+ try {
|
|
| 2779 |
+ body = await res.json(); |
|
| 2780 |
+ } catch (_) {
|
|
| 2781 |
+ body = {};
|
|
| 2782 |
+ } |
|
| 2783 |
+ const errorCode = body.error || ''; |
|
| 2784 |
+ if (!res.ok) {
|
|
| 2785 |
+ if (res.status === 401 && !(path === '/api/login' && errorCode === 'invalid_otp')) {
|
|
| 2786 |
+ const error = authLostError(); |
|
| 2787 |
+ handleAuthLost(error.message); |
|
| 2788 |
+ throw error; |
|
| 2789 |
+ } |
|
| 2790 |
+ throw new Error(errorCode || res.statusText); |
|
| 2791 |
+ } |
|
| 2763 | 2792 |
return body; |
| 2764 | 2793 |
} |
| 2765 | 2794 |
|
@@ -2781,11 +2810,14 @@ sub app_html {
|
||
| 2781 | 2810 |
history.pushState({ page: target }, '', href);
|
| 2782 | 2811 |
} |
| 2783 | 2812 |
if (state.authenticated && target === 'debug') {
|
| 2784 |
- renderDebugDatabase().catch(e => msg(e.message)); |
|
| 2813 |
+ renderDebugDatabase().catch(e => {
|
|
| 2814 |
+ if (!isAuthLost(e)) msg(e.message); |
|
| 2815 |
+ }); |
|
| 2785 | 2816 |
} |
| 2786 | 2817 |
} |
| 2787 | 2818 |
|
| 2788 | 2819 |
function showLogin(errorText) {
|
| 2820 |
+ state.authenticated = false; |
|
| 2789 | 2821 |
document.body.classList.remove('is-app');
|
| 2790 | 2822 |
document.body.classList.add('is-login');
|
| 2791 | 2823 |
$('app').style.display = 'none';
|
@@ -2805,7 +2837,7 @@ sub app_html {
|
||
| 2805 | 2837 |
async function refresh() {
|
| 2806 | 2838 |
const session = await api('/api/session');
|
| 2807 | 2839 |
state.authenticated = session.authenticated; |
| 2808 |
- if (!state.authenticated) { showLogin(); return; }
|
|
| 2840 |
+ if (!state.authenticated) { showLogin('Autentifica-te pentru a continua.'); return; }
|
|
| 2809 | 2841 |
showApp(); |
| 2810 | 2842 |
const data = await api('/api/hosts');
|
| 2811 | 2843 |
state.hosts = data.hosts || []; |
@@ -2884,6 +2916,7 @@ sub app_html {
|
||
| 2884 | 2916 |
</tr>`; |
| 2885 | 2917 |
}).join('') : '<tr><td colspan="6" class="muted">No issued certificates.</td></tr>';
|
| 2886 | 2918 |
} catch (e) {
|
| 2919 |
+ if (isAuthLost(e)) return; |
|
| 2887 | 2920 |
$('ca-status').innerHTML = `<div class="problem"><strong>CA status unavailable</strong> ${escapeHtml(e.message)}</div>`;
|
| 2888 | 2921 |
$('ca-certs-summary').innerHTML = '';
|
| 2889 | 2922 |
$('ca-certs').innerHTML = '<tr><td colspan="6" class="muted">Certificate list unavailable.</td></tr>';
|
@@ -2958,6 +2991,7 @@ sub app_html {
|
||
| 2958 | 2991 |
document.querySelectorAll('[data-wo-checklist]').forEach(input => input.addEventListener('change', () => updateWorkOrderChecklist(input.dataset.woChecklist, input.dataset.itemId, input.checked)));
|
| 2959 | 2992 |
document.querySelectorAll('[data-confirm-wo]').forEach(button => button.addEventListener('click', () => confirmWorkOrder(button.dataset.confirmWo)));
|
| 2960 | 2993 |
} catch (e) {
|
| 2994 |
+ if (isAuthLost(e)) return; |
|
| 2961 | 2995 |
$('work-orders').innerHTML = `<div class="problem"><strong>Work orders unavailable</strong> ${escapeHtml(e.message)}</div>`;
|
| 2962 | 2996 |
} |
| 2963 | 2997 |
} |
@@ -3030,7 +3064,13 @@ sub app_html {
|
||
| 3030 | 3064 |
}); |
| 3031 | 3065 |
msg('work order updated');
|
| 3032 | 3066 |
await refresh(); |
| 3033 |
- } catch (e) { msg(e.message); await refresh(); }
|
|
| 3067 |
+ } catch (e) {
|
|
| 3068 |
+ if (isAuthLost(e)) return; |
|
| 3069 |
+ msg(e.message); |
|
| 3070 |
+ await refresh().catch(refreshError => {
|
|
| 3071 |
+ if (!isAuthLost(refreshError)) msg(refreshError.message); |
|
| 3072 |
+ }); |
|
| 3073 |
+ } |
|
| 3034 | 3074 |
} |
| 3035 | 3075 |
|
| 3036 | 3076 |
async function confirmWorkOrder(id) {
|
@@ -3044,7 +3084,10 @@ sub app_html {
|
||
| 3044 | 3084 |
}); |
| 3045 | 3085 |
msg('work order confirmed; local-hosts.tsv written');
|
| 3046 | 3086 |
await refresh(); |
| 3047 |
- } catch (e) { msg(e.message); }
|
|
| 3087 |
+ } catch (e) {
|
|
| 3088 |
+ if (isAuthLost(e)) return; |
|
| 3089 |
+ msg(e.message); |
|
| 3090 |
+ } |
|
| 3048 | 3091 |
} |
| 3049 | 3092 |
|
| 3050 | 3093 |
function renderHosts() {
|
@@ -3321,11 +3364,17 @@ sub app_html {
|
||
| 3321 | 3364 |
window.location.replace('/?logged_out=' + Date.now());
|
| 3322 | 3365 |
}); |
| 3323 | 3366 |
|
| 3324 |
- $('refresh').addEventListener('click', () => refresh().catch(e => msg(e.message)));
|
|
| 3367 |
+ $('refresh').addEventListener('click', () => refresh().catch(e => {
|
|
| 3368 |
+ if (!isAuthLost(e)) msg(e.message); |
|
| 3369 |
+ })); |
|
| 3325 | 3370 |
$('filter').addEventListener('input', renderHosts);
|
| 3326 | 3371 |
$('new-host').addEventListener('click', newHost);
|
| 3327 |
- $('debug-db-refresh').addEventListener('click', () => renderDebugDatabase().catch(e => msg(e.message)));
|
|
| 3328 |
- $('debug-db-table').addEventListener('change', () => renderDebugTable($('debug-db-table').value).catch(e => msg(e.message)));
|
|
| 3372 |
+ $('debug-db-refresh').addEventListener('click', () => renderDebugDatabase().catch(e => {
|
|
| 3373 |
+ if (!isAuthLost(e)) msg(e.message); |
|
| 3374 |
+ })); |
|
| 3375 |
+ $('debug-db-table').addEventListener('change', () => renderDebugTable($('debug-db-table').value).catch(e => {
|
|
| 3376 |
+ if (!isAuthLost(e)) msg(e.message); |
|
| 3377 |
+ })); |
|
| 3329 | 3378 |
$('close-host-modal').addEventListener('click', requestCloseHostModal);
|
| 3330 | 3379 |
$('host-modal').addEventListener('click', (event) => {
|
| 3331 | 3380 |
if (event.target === $('host-modal') && !$('save-host').disabled) closeHostModal();
|
@@ -3345,6 +3394,7 @@ sub app_html {
|
||
| 3345 | 3394 |
msg('host saved');
|
| 3346 | 3395 |
await refresh(); |
| 3347 | 3396 |
} catch (e) {
|
| 3397 |
+ if (isAuthLost(e)) return; |
|
| 3348 | 3398 |
setHostFormMessage(e.message, true); |
| 3349 | 3399 |
msg(e.message); |
| 3350 | 3400 |
} finally {
|
@@ -3373,6 +3423,7 @@ sub app_html {
|
||
| 3373 | 3423 |
msg('host deleted');
|
| 3374 | 3424 |
await refresh(); |
| 3375 | 3425 |
} catch (e) {
|
| 3426 |
+ if (isAuthLost(e)) return; |
|
| 3376 | 3427 |
setHostFormMessage(e.message, true); |
| 3377 | 3428 |
msg(e.message); |
| 3378 | 3429 |
} finally {
|
@@ -3385,10 +3436,14 @@ sub app_html {
|
||
| 3385 | 3436 |
try {
|
| 3386 | 3437 |
await api('/api/render/local-hosts-tsv', { method: 'POST' });
|
| 3387 | 3438 |
msg('local-hosts.tsv written');
|
| 3388 |
- } catch (e) { msg(e.message); }
|
|
| 3439 |
+ } catch (e) {
|
|
| 3440 |
+ if (!isAuthLost(e)) msg(e.message); |
|
| 3441 |
+ } |
|
| 3389 | 3442 |
}); |
| 3390 | 3443 |
|
| 3391 |
- refresh().catch(() => showLogin()); |
|
| 3444 |
+ refresh().catch(e => {
|
|
| 3445 |
+ if (!isAuthLost(e)) showLogin(e.message); |
|
| 3446 |
+ }); |
|
| 3392 | 3447 |
</script> |
| 3393 | 3448 |
</body> |
| 3394 | 3449 |
</html> |