Skip to content

Commit e39da3e

Browse files
committed
fix(auth): keep passkey login loading active
- Await the complete explicit passkey login ceremony.\n- Reuse the shared pending-action guard for registration and login.\n- Cover pending and duplicate-submission behavior with a focused test.\n\nCo-authored-by: Codex <[email protected]>
1 parent 8f860d8 commit e39da3e

4 files changed

Lines changed: 60 additions & 57 deletions

File tree

src/pages/login/index.tsx

Lines changed: 50 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
getPasskey,
2626
isWebAuthnSupported,
2727
passkeyToJSON,
28+
runPasskeyAction,
2829
webAuthnErrorMessage,
2930
} from "~/utils"
3031
import { PasskeyChallenge, PResp, Resp } from "~/types"
@@ -69,7 +70,7 @@ const Login = () => {
6970
}
7071
},
7172
)
72-
const [finishPasskeyLoading, postauthnlogin] = useFetch(
73+
const [, postauthnlogin] = useFetch(
7374
(
7475
session: string,
7576
credentials: PublicKeyCredential,
@@ -87,7 +88,7 @@ const Login = () => {
8788
},
8889
),
8990
)
90-
const [beginPasskeyLoading, getauthntemp] = useFetch(
91+
const [, getauthntemp] = useFetch(
9192
(
9293
username: string,
9394
signal: AbortSignal | undefined,
@@ -108,6 +109,7 @@ const Login = () => {
108109
}
109110
}
110111
const AuthnSignEnabled = getSettingBool("webauthn_login_enabled")
112+
const [passkeyLoginLoading, setPasskeyLoginLoading] = createSignal(false)
111113
const AuthnSwitch = () => {
112114
setuseauthn(!useauthn())
113115
}
@@ -132,48 +134,51 @@ const Login = () => {
132134
localStorage.removeItem("username")
133135
}
134136
const resp = await getauthntemp(username_login, controller.signal)
135-
handleResp(resp, async (data) => {
136-
try {
137-
const credentials = await getPasskey(
138-
data.options.publicKey,
139-
controller.signal,
140-
conditional,
137+
if (resp.code !== 200) {
138+
handleResp(resp)
139+
return
140+
}
141+
try {
142+
const data = resp.data
143+
const credentials = await getPasskey(
144+
data.options.publicKey,
145+
controller.signal,
146+
conditional,
147+
)
148+
if (!credentials) {
149+
throw new DOMException(
150+
"No passkey credential was returned.",
151+
"NotAllowedError",
141152
)
142-
if (!credentials) {
143-
throw new DOMException(
144-
"No passkey credential was returned.",
145-
"NotAllowedError",
153+
}
154+
const finishResp = await postauthnlogin(
155+
data.session,
156+
credentials,
157+
username_login,
158+
controller.signal,
159+
)
160+
handleRespWithoutAuthAndNotify(
161+
finishResp,
162+
(data) => {
163+
notify.success(t("login.success"))
164+
changeToken(data.token)
165+
to(
166+
decodeURIComponent(searchParams.redirect || base_path || "/"),
167+
true,
146168
)
147-
}
148-
const resp = await postauthnlogin(
149-
data.session,
150-
credentials,
151-
username_login,
152-
controller.signal,
153-
)
154-
handleRespWithoutAuthAndNotify(
155-
resp,
156-
(data) => {
157-
notify.success(t("login.success"))
158-
changeToken(data.token)
159-
to(
160-
decodeURIComponent(searchParams.redirect || base_path || "/"),
161-
true,
162-
)
163-
},
164-
(msg) => {
165-
notify.error(msg)
166-
},
167-
)
168-
} catch (error: unknown) {
169-
if (
170-
!conditional &&
171-
(!(error instanceof Error) || error.name !== "AbortError")
172-
) {
173-
notify.error(webAuthnErrorMessage(error))
174-
}
169+
},
170+
(msg) => {
171+
notify.error(msg)
172+
},
173+
)
174+
} catch (error: unknown) {
175+
if (
176+
!conditional &&
177+
(!(error instanceof Error) || error.name !== "AbortError")
178+
) {
179+
notify.error(webAuthnErrorMessage(error))
175180
}
176-
})
181+
}
177182
}
178183
const AuthnCleanUpHandler = () => AuthnSignal?.abort()
179184
onMount(() => {
@@ -216,7 +221,9 @@ const Login = () => {
216221
},
217222
)
218223
} else {
219-
await AuthnLogin()
224+
await runPasskeyAction(passkeyLoginLoading, setPasskeyLoginLoading, () =>
225+
AuthnLogin(),
226+
)
220227
}
221228
}
222229
const [needOpt, setNeedOpt] = createSignal(false)
@@ -322,11 +329,7 @@ const Login = () => {
322329
<Button
323330
w="$full"
324331
leftIcon={useauthn() ? <PasskeyIcon onButton /> : undefined}
325-
loading={
326-
useauthn()
327-
? beginPasskeyLoading() || finishPasskeyLoading()
328-
: loading()
329-
}
332+
loading={useauthn() ? passkeyLoginLoading() : loading()}
330333
onClick={Login}
331334
>
332335
{useauthn() ? t("login.passkey_login") : t("login.login")}

src/pages/manage/users/Profile.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ import {
4747
notify,
4848
passkeyToJSON,
4949
r,
50-
runPasskeyRegistrationAction,
50+
runPasskeyAction,
5151
webAuthnErrorMessage,
5252
} from "~/utils"
5353
import { WebauthnItem } from "./Webauthnitems"
@@ -149,7 +149,7 @@ const Profile = () => {
149149
notify.error(t("users.webauthn_not_supported"))
150150
return
151151
}
152-
await runPasskeyRegistrationAction(
152+
await runPasskeyAction(
153153
registrationLoading,
154154
setRegistrationLoading,
155155
async () => {

src/utils/webauthn.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import assert from "node:assert/strict"
22
import test from "node:test"
3-
import { runPasskeyRegistrationAction } from "./webauthn.ts"
3+
import { runPasskeyAction } from "./webauthn.ts"
44

5-
test("keeps registration loading until the credential promise settles", async () => {
5+
test("keeps passkey actions loading until the credential promise settles", async () => {
66
let loading = false
77
let actionCalls = 0
88
let resolveCredential: () => void
99
const credential = new Promise<void>((resolve) => {
1010
resolveCredential = resolve
1111
})
1212

13-
const register = () =>
14-
runPasskeyRegistrationAction(
13+
const run = () =>
14+
runPasskeyAction(
1515
() => loading,
1616
(value) => {
1717
loading = value
@@ -22,19 +22,19 @@ test("keeps registration loading until the credential promise settles", async ()
2222
},
2323
)
2424

25-
const pending = register()
25+
const pending = run()
2626
await Promise.resolve()
2727

2828
assert.equal(loading, true)
29-
assert.equal(await register(), false)
29+
assert.equal(await run(), false)
3030
assert.equal(actionCalls, 1)
3131

3232
resolveCredential!()
3333
assert.equal(await pending, true)
3434
assert.equal(loading, false)
3535

3636
await assert.rejects(
37-
runPasskeyRegistrationAction(
37+
runPasskeyAction(
3838
() => loading,
3939
(value) => {
4040
loading = value

src/utils/webauthn.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const webAuthnErrorMessage = (error: unknown) => {
2727
export const passkeyToJSON = (credential: PublicKeyCredential) =>
2828
credential.toJSON()
2929

30-
export const runPasskeyRegistrationAction = async (
30+
export const runPasskeyAction = async (
3131
isLoading: () => boolean,
3232
setLoading: (loading: boolean) => void,
3333
action: () => Promise<void>,

0 commit comments

Comments
 (0)