nextcloud.url (or NEXTCLOUD_URL) names the site's Nextcloud. With it set, the address field is hidden and the page reads "Sign in with Nextcloud", with the app-password route underneath for whoever needs it; both routes use the default when no address is given. Without it, nothing changes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
161 lines
6.9 KiB
JavaScript
161 lines
6.9 KiB
JavaScript
var _inited = false;
|
|
document.addEventListener('tabChanged', function(e) {
|
|
if (e.detail.tab !== 'settings' || _inited) return;
|
|
_inited = true;
|
|
function setNextcloudConnectedStatus(url, user) {
|
|
var statusEl = document.getElementById('nc-status');
|
|
if (!statusEl) return;
|
|
statusEl.textContent = 'Connected to ';
|
|
var strong = document.createElement('strong');
|
|
strong.textContent = url || '';
|
|
statusEl.appendChild(strong);
|
|
statusEl.appendChild(document.createTextNode(' as ' + (user || '')));
|
|
}
|
|
|
|
// The site's Nextcloud, when there is one: the address is filled in and its
|
|
// field hidden, so the page reads "Sign in with Nextcloud" and nothing else.
|
|
function loadNextcloudDefault() {
|
|
fetch('/api/nextcloud/config', { headers: getAuthHeaders() })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (data) {
|
|
var group = document.getElementById('nc-url-group');
|
|
var url = document.getElementById('nc-url');
|
|
if (!data || !data.defaultUrl || !group || !url) return;
|
|
if (!url.value.trim()) url.value = data.defaultUrl;
|
|
group.classList.add('hidden');
|
|
})
|
|
.catch(function () {});
|
|
}
|
|
|
|
function loadNextcloudStatus() {
|
|
loadNextcloudDefault();
|
|
fetch('/api/auth/me', { headers: getAuthHeaders() })
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (data.user && data.user.nextcloud_url) {
|
|
setNextcloudConnectedStatus(data.user.nextcloud_url, data.user.nextcloud_user);
|
|
document.getElementById('nc-url').value = data.user.nextcloud_url;
|
|
document.getElementById('nc-user').value = data.user.nextcloud_user;
|
|
document.getElementById('btn-nc-disconnect').classList.remove('hidden');
|
|
} else {
|
|
document.getElementById('nc-status').textContent = 'Not connected';
|
|
document.getElementById('btn-nc-disconnect').classList.add('hidden');
|
|
}
|
|
});
|
|
}
|
|
|
|
window.loadNextcloudStatus = loadNextcloudStatus;
|
|
|
|
document.getElementById('btn-nc-connect').addEventListener('click', function() {
|
|
var url = document.getElementById('nc-url').value.trim().replace(/\/+$/, '');
|
|
var user = document.getElementById('nc-user').value.trim();
|
|
var pass = document.getElementById('nc-pass').value.trim();
|
|
|
|
if (!url || !user || !pass) { showToast('Fill all Nextcloud fields', 'error'); return; }
|
|
|
|
showLoading('Connecting to Nextcloud...');
|
|
|
|
fetch('/api/nextcloud/connect', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({ nextcloudUrl: url, username: user, appPassword: pass })
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
hideLoading();
|
|
if (data.success) {
|
|
showToast(data.message, 'success');
|
|
loadNextcloudStatus();
|
|
document.getElementById('nc-pass').value = '';
|
|
} else {
|
|
showToast(data.error || 'Connection failed', 'error');
|
|
}
|
|
})
|
|
.catch(function(err) { hideLoading(); showToast(err.message, 'error'); });
|
|
});
|
|
|
|
document.getElementById('btn-nc-disconnect').addEventListener('click', function() {
|
|
fetch('/api/nextcloud/disconnect', { method: 'POST', headers: getAuthHeaders() })
|
|
.then(function() { showToast('Disconnected', 'info'); loadNextcloudStatus(); });
|
|
});
|
|
|
|
// ── Sign in with Nextcloud ────────────────────────────────────────────
|
|
// Nextcloud's own login flow: ask our server to start one, send the person to
|
|
// the URL Nextcloud hands back, and poll until they finish.
|
|
//
|
|
// The first version opened a blank tab during the click and pointed it at the
|
|
// URL once the request came back, which is the usual way around a popup
|
|
// blocker. It cannot work here: this app sends
|
|
// Cross-Origin-Opener-Policy: same-origin, so the handle to that tab is
|
|
// severed the moment it goes cross-origin, and assigning its location did
|
|
// nothing at all — a blank tab, and a button that looked broken.
|
|
//
|
|
// So no handle. window.open with 'noopener' needs none, and a click's user
|
|
// activation survives the fetch, so it is not treated as a popup. And the
|
|
// status line always offers the link itself, which works whatever the browser
|
|
// decides about opening windows.
|
|
var pollTimer = null;
|
|
|
|
function stopPolling(message, tone) {
|
|
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
|
setFlowStatus(message, tone);
|
|
}
|
|
|
|
function setFlowStatus(message, tone, link) {
|
|
var status = document.getElementById('nc-login-flow-status');
|
|
if (!status) return;
|
|
status.textContent = message || '';
|
|
status.style.color = tone === 'bad' ? 'var(--red)' : tone === 'good' ? 'var(--green)' : 'var(--g600)';
|
|
if (!link) return;
|
|
status.appendChild(document.createTextNode(' '));
|
|
var a = document.createElement('a');
|
|
a.href = link;
|
|
a.target = '_blank';
|
|
a.rel = 'noopener noreferrer';
|
|
a.textContent = 'Open the sign-in page';
|
|
status.appendChild(a);
|
|
}
|
|
|
|
document.getElementById('btn-nc-login-flow').addEventListener('click', function () {
|
|
var url = (document.getElementById('nc-url').value || '').trim();
|
|
if (!url) { showToast('Enter your Nextcloud address first', 'error'); return; }
|
|
|
|
stopPolling('Opening Nextcloud…');
|
|
|
|
fetch('/api/nextcloud/login-flow/start', {
|
|
method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ nextcloudUrl: url })
|
|
})
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (data) {
|
|
if (!data.success) throw new Error(data.error || 'Could not start sign-in');
|
|
// Opened without a handle, so COOP has nothing to sever. If the browser
|
|
// blocks it anyway, the link in the status line is the way through.
|
|
window.open(data.loginUrl, '_blank', 'noopener');
|
|
setFlowStatus('Waiting for you to finish signing in.', null, data.loginUrl);
|
|
|
|
var until = Date.now() + 10 * 60 * 1000;
|
|
pollTimer = setInterval(function () {
|
|
if (Date.now() > until) return stopPolling('Sign-in timed out. Try again.', 'bad');
|
|
fetch('/api/nextcloud/login-flow/poll', {
|
|
method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ handle: data.handle })
|
|
})
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (result) {
|
|
if (result.connected) {
|
|
stopPolling('Connected as ' + result.username + '.', 'good');
|
|
loadNextcloudStatus();
|
|
showToast('Nextcloud connected', 'success');
|
|
} else if (!result.success) {
|
|
stopPolling(result.error || 'Sign-in failed.', 'bad');
|
|
}
|
|
})
|
|
.catch(function () { /* a dropped poll is not a failed sign-in */ });
|
|
}, 3000);
|
|
})
|
|
.catch(function (err) {
|
|
stopPolling(err.message, 'bad');
|
|
});
|
|
});
|
|
|
|
console.log('✅ Nextcloud module loaded');
|
|
});
|