fix: the account boundary explains itself instead of reading as an SSO failure
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 55s
Forgejo Docker Build / Root app tests (push) Successful in 1m0s
Forgejo Android APK / Build signed APK (push) Successful in 2m3s
Forgejo Docker Build / Build Docker image (push) Successful in 13s
Forgejo Docker Build / Deploy to the host (push) Failing after 2s

Tapping SSO on a frozen realm did nothing at all — no navigation, no message.
The guard is correct: a page bound to one account must not start a login for
another. But silence is indistinguishable from a broken button, and it was
reported as "single sign on error" when no SSO request had been made; the
server logged no OIDC activity because the click never left the page.

The button now says what is happening and names the control that clears it.
The guard itself is unchanged — same checks, same order, same outcomes.

The recovery screen said "your saved credentials no longer match this session",
which describes the mechanism rather than the situation. The ordinary cause is
signing in as a different account on a device that already held one. It now
says that, and says plainly that nothing was lost and no data was mixed —
which is the reassurance the safeguard has earned and was not giving.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
This commit is contained in:
Daniel 2026-09-12 18:24:14 +02:00
parent 74aa0c1b89
commit ac1adddd50
2 changed files with 21 additions and 3 deletions

View file

@ -14,8 +14,14 @@
function recoverSignIn() {
signInRequired = true;
freeze();
// Say what happened, not only that something did. This fires when the page
// is bound to one account and the session belongs to another — switching
// accounts on a shared device is the ordinary cause, and it reads as a
// failure when the message does not name it.
document.querySelector('#account-recovery p').textContent =
'Your saved credentials no longer match this session. Sign in again in a fresh page.';
'This page was opened for a different account than the one now signed in. ' +
'That is a safeguard, not an error: nothing is lost, and no data from either ' +
'account has been mixed. Start a fresh session to continue.';
document.querySelector('#account-recovery button').textContent = 'Sign in again';
}
function read() {

View file

@ -191,9 +191,21 @@ document.addEventListener('DOMContentLoaded', function() {
} catch (e) {}
var ssoButton = document.getElementById('btn-sso');
if (ssoButton) ssoButton.addEventListener('click', async function(e) {
if (!e.isTrusted || authenticationPending || boundary.blocked()) { e.preventDefault(); return; }
// A frozen realm cannot start a login, and silently doing nothing reads as
// "SSO is broken" — it is the account boundary holding, not a failure. Say
// so, and point at the one control that clears it.
if (boundary.blocked()) {
e.preventDefault();
showToast('This page is locked to a different account. Tap "Sign in again" to start a fresh session, then use SSO.', 'error');
return;
}
if (!e.isTrusted || authenticationPending) { e.preventDefault(); return; }
boundary.capture(); // Detect a replaced sibling session before capturing credentials.
if (boundary.blocked()) { e.preventDefault(); return; }
if (boundary.blocked()) {
e.preventDefault();
showToast('This page is locked to a different account. Tap "Sign in again" to start a fresh session, then use SSO.', 'error');
return;
}
// No owner marker can also mean bootstrap failed with an old cookie still
// valid. Every SSO start must prove cookie absence before issuing intent.
e.preventDefault();