Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 46s
Forgejo Docker Build / Root app tests (push) Successful in 59s
Forgejo Android APK / Build signed APK (push) Successful in 2m7s
Forgejo Docker Build / Build Docker image (push) Successful in 10s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
Asking someone to find Settings → Security → Create new app password is a poor first run, and it is the step people give up on. Nextcloud has its own answer: Login Flow v2. The person enters their server address, signs in on Nextcloud the way they normally do — SSO, 2FA, a password manager — and Nextcloud generates an app password for this app. We never see their real password. Pasting an app password still works, behind "Use an app password instead". It is the fallback, not the front door. The security of this is all in what is trusted. The remote server chooses both the login URL and the poll endpoint, so both are SSRF-checked and both must be on the host the person actually typed — an endpoint pointing elsewhere would make this a request-forgery gadget aimed at whatever it named. The server Nextcloud reports at the end is re-checked before it is stored. The poll token is a credential, so polling happens server-side and the browser holds only an opaque handle bound to its own account. Flows live in memory with a 20 minute life, matching Nextcloud's own expiry: a login lasts minutes, and a restart mid-flow is a retry rather than a loss. Starting a second flow replaces the first, which is what clicking again means. The tab is opened from the click itself, before the request — opening it after an await is what a popup blocker stops. Removed with Learning Hub: the WebDAV browse path. Its field, its route and its column are gone, since nothing browses Nextcloud any more. nextcloud_folder is a different column and still in use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
123 lines
5.2 KiB
JavaScript
123 lines
5.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. The tab is opened from the click itself, before
|
|
// any await, or a popup blocker eats it — the request that fetches the URL is
|
|
// allowed to finish afterwards and point the already-open tab at it.
|
|
var pollTimer = null;
|
|
|
|
function stopPolling(message, tone) {
|
|
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
|
var status = document.getElementById('nc-login-flow-status');
|
|
if (status) {
|
|
status.textContent = message || '';
|
|
status.style.color = tone === 'bad' ? 'var(--red)' : tone === 'good' ? 'var(--green)' : 'var(--g600)';
|
|
}
|
|
}
|
|
|
|
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; }
|
|
|
|
var tab = window.open('', '_blank'); // claimed while the click is still trusted
|
|
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');
|
|
if (tab) tab.location.href = data.loginUrl; else window.open(data.loginUrl, '_blank');
|
|
stopPolling('Waiting for you to finish signing in…');
|
|
|
|
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) {
|
|
if (tab) tab.close();
|
|
stopPolling(err.message, 'bad');
|
|
});
|
|
});
|
|
|
|
console.log('✅ Nextcloud module loaded');
|
|
});
|