mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-18 16:13:51 -06:00
On a fresh install with no users, typing an email address made the password field disappear. Identifier-first login asks the server which identity provider an address uses before offering a credential, so someone whose organization requires its own IdP is never shown a password box that will be refused. First-run setup opted out of that by setting `identified = true` up front - there is nobody to identify, the operator is creating the first account - and then the "editing the address returns to the identifier step" listener fired on the very first keystroke, set it back to false, and re-rendered. Password gone, mid-typing. The same re-render also replaced "Create admin account" on the button with "Sign in" and then "Next". Initialising a flag to the right value is not the same as the flow being inert: any later event could undo it, and one did. The decision now lives in frontend/js/lib/login-form-state.js as a pure function that ignores `identified` and `ssoOnlyDomain` entirely when there are no users, so no event can take a field away. The input listener also returns early during setup, which additionally stops it spending an org-lookup rate-limit budget that has nothing to answer. Two assertions in login-identifier-first.test.js pinned those expressions to their old address inside login.js. Their intent - "the two drivers must be combined in one place so they cannot disagree" - is better served by the extracted module, so they now follow the logic there rather than being loosened. Verified: the extracted pre-fix logic returns showPassword=false and buttonKey=auth.next for setup-plus-one-keystroke, which is the reported symptom exactly; the new truth table covers it. i18n/login/auth/session/sso suites go from 108 to 116 passing, none failing. Not addressed here, and worth its own change: loadAuthConfig() has no error handling and caches its first result, so a /api/auth/config that fails leaves needsSetup undefined and the form falls back to the restrictive layout - the same visible symptom from a different cause. Claude-Session: https://claude.ai/code/session_014kfhrUPit5MCqxeTQyqr56 Co-authored-by: Dan Walters <dan.walters@bytetinker.net> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
40 lines
1.9 KiB
JavaScript
40 lines
1.9 KiB
JavaScript
/*
|
|
* What the login form shows, as a pure decision.
|
|
*
|
|
* Extracted because it got this wrong in a way nobody could see from reading the handlers: the
|
|
* state lived in two mutable flags updated from four event listeners, and one of those listeners
|
|
* undid first-run setup on the first keystroke.
|
|
*
|
|
* THE BUG. On a fresh install with no users, the page set identified = true so both fields were
|
|
* available, because there is nobody to identify - the operator is creating the first account.
|
|
* Then typing in the email box fired the "editing the address returns to the identifier step"
|
|
* listener, which set identified = false and re-applied the state, hiding the password field
|
|
* mid-typing and relabelling the button "Next".
|
|
*
|
|
* Identifier-first exists to ask the server which identity provider an EXISTING address uses, so
|
|
* an SSO-only user is never shown a password box that will be refused. With an empty user table
|
|
* there is no such question, so the whole two-step flow has to be inert - not merely initialised
|
|
* to a state that a later event can undo.
|
|
*/
|
|
|
|
/**
|
|
* @param {{isSetup: boolean, identified: boolean, ssoOnlyDomain: boolean}} state
|
|
* @returns {{showPassword: boolean, showButton: boolean, buttonKey: string}}
|
|
*/
|
|
export function loginFormState({ isSetup, identified, ssoOnlyDomain }) {
|
|
// First-run setup: every field is needed at once and no event may take one away. Deliberately
|
|
// ignores `identified` and `ssoOnlyDomain` rather than trusting them to hold the right values -
|
|
// that trust is what broke.
|
|
if (isSetup) {
|
|
return { showPassword: true, showButton: true, buttonKey: 'auth.create_admin_account' };
|
|
}
|
|
|
|
const known = identified && !ssoOnlyDomain;
|
|
return {
|
|
showPassword: known,
|
|
// An SSO-only domain has nothing to press: the provider button is the only way in.
|
|
showButton: !ssoOnlyDomain,
|
|
buttonKey: known ? 'auth.sign_in' : 'auth.next',
|
|
};
|
|
}
|