The button did nothing. It claimed a blank tab during the click and pointed it at the login URL once the request returned — the standard way around a popup blocker, and it cannot work in this app: helmet sends Cross-Origin-Opener-Policy: same-origin, which severs the handle to that tab the moment it goes cross-origin. Assigning its location was a no-op. A blank tab opened, nothing else happened. The handle was never needed. window.open with 'noopener' asks for none, and a click's user activation outlives the fetch, so the browser does not treat it as a popup. The status line now also carries the sign-in URL as an ordinary link, so there is a way through whatever any particular browser decides about opening windows. Verified the server side against the real Nextcloud first: the flow starts, both returned URLs pass the SSRF guard and the same-host check. The fault was entirely in the browser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
145 lines
6.2 KiB
JavaScript
145 lines
6.2 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 || '')));
|
|
}
|
|
|
|
function loadNextcloudStatus() {
|
|
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');
|
|
});
|