pediatric-ai-scribe-v3/public/js/nextcloud.js
Daniel c68e3a6219 fix: "Sign in with Nextcloud" opens the sign-in page
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
2026-09-12 21:36:51 +02:00

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');
});