feat(login): prevent login form flash on OIDC callback
This commit is contained in:
@@ -117,6 +117,16 @@
|
||||
// config, or the local account is already linked to a different
|
||||
// identity). See docs/plan/oidc-account-linking.md § Auto-link.
|
||||
let loginErrorNotice = $state<string | null>(null);
|
||||
// True while we're mid-OIDC-callback and about to redirect into the
|
||||
// app. Read synchronously at script-init from `?oidc_code=…` so the
|
||||
// FIRST paint suppresses the form and shows a loader instead —
|
||||
// without this the SPA briefly renders the empty username/password
|
||||
// fields between the IdP redirect and the exchange-then-goto,
|
||||
// making it look like the login screen "flashed." Cleared in
|
||||
// onMount if the exchange fails so the normal form takes over.
|
||||
let willRedirect = $state(
|
||||
typeof window !== 'undefined' && page.url.searchParams.has('oidc_code')
|
||||
);
|
||||
// Refs used by the mode-driven auto-focus effect. Bound with
|
||||
// `bind:this` on the first input of each mode's form so the effect
|
||||
// can focus the "primary" field each time the mode changes without
|
||||
@@ -368,7 +378,10 @@
|
||||
await goto(resolve(redirectTarget), { replaceState: true });
|
||||
return;
|
||||
}
|
||||
// Exchange failed — fall through to the normal login UI.
|
||||
// Exchange failed — fall through to the normal login UI. Drop
|
||||
// the loader guard so the form appears; if we leave it true
|
||||
// the user stares at a spinner indefinitely.
|
||||
willRedirect = false;
|
||||
}
|
||||
|
||||
// 2) Existing-session probe: if already authenticated, skip the form.
|
||||
@@ -444,7 +457,24 @@
|
||||
<title>{t('app.title', 'OxiCloud')}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="auth-container">
|
||||
{#if willRedirect}
|
||||
<!-- Full-viewport overlay that visually continues the boot splash
|
||||
(see app.html `#app-splash`) — same background, same spinner —
|
||||
so the OIDC callback landing goes spinner → spinner → /files
|
||||
with no intermediate flash of the auth-panel shell (logo +
|
||||
language selector). Auth-container below is skipped entirely
|
||||
during this window; if the exchange fails, `willRedirect`
|
||||
flips false and the normal login form takes over. -->
|
||||
<div
|
||||
class="auth-splash"
|
||||
role="status"
|
||||
aria-label="Signing in"
|
||||
data-testid="login-redirect-loader"
|
||||
>
|
||||
<div class="auth-splash__spinner" aria-hidden="true"></div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="auth-container">
|
||||
<div class="auth-panel">
|
||||
<div class="auth-logo">
|
||||
<div class="auth-logo-icon">
|
||||
@@ -705,7 +735,12 @@
|
||||
{#if regError}<div class="auth-error" style="display: block" role="alert">
|
||||
{regError}
|
||||
</div>{/if}
|
||||
<form class="auth-form" data-testid="login-register-form" onsubmit={onRegister} novalidate>
|
||||
<form
|
||||
class="auth-form"
|
||||
data-testid="login-register-form"
|
||||
onsubmit={onRegister}
|
||||
novalidate
|
||||
>
|
||||
<!-- Email is the only required identifier since PR 18 — the
|
||||
backend accepts email-only signup and mints a welcome
|
||||
magic-link. Username is optional at this stage; the user
|
||||
@@ -854,7 +889,9 @@
|
||||
{#if setupError}<div class="auth-error" style="display: block" role="alert">
|
||||
{setupError}
|
||||
</div>{/if}
|
||||
{#if setupSuccess}<div class="auth-success" style="display: block">{setupSuccess}</div>{/if}
|
||||
{#if setupSuccess}<div class="auth-success" style="display: block">
|
||||
{setupSuccess}
|
||||
</div>{/if}
|
||||
|
||||
<form class="auth-form" data-testid="login-setup-form" onsubmit={onSetup} novalidate>
|
||||
<div class="auth-input-group">
|
||||
@@ -1000,7 +1037,8 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.auth-lang {
|
||||
@@ -1053,4 +1091,30 @@
|
||||
color: var(--color-text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* OIDC-callback continuation splash — VISUALLY IDENTICAL to the
|
||||
boot splash in app.html so the transition boot-splash →
|
||||
post-exchange overlay is imperceptible: same background, same
|
||||
spinner dimensions, same colors, same animation cadence.
|
||||
Tokens match by design — `--color-bg-page` resolves to the
|
||||
same `light-dark(#f5f7fa, #0f172a)` the boot splash hard-codes;
|
||||
ditto `--color-border` and `--color-accent`. If you ever retone
|
||||
the splash, update BOTH sites at once (app.html + this file). */
|
||||
.auth-splash {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: var(--color-bg-page);
|
||||
}
|
||||
|
||||
.auth-splash__spinner {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border: 3px solid var(--color-border);
|
||||
border-top-color: var(--color-accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user