@yackey-labs/yauth-ui-solidjs 0.2.6 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +636 -558
- package/package.json +3 -3
- package/src/components/change-password-form.tsx +6 -1
- package/src/components/forgot-password-form.tsx +5 -3
- package/src/components/login-form.tsx +6 -8
- package/src/components/magic-link-form.tsx +5 -3
- package/src/components/mfa-challenge.tsx +7 -5
- package/src/components/mfa-setup.tsx +6 -2
- package/src/components/oauth-buttons.tsx +3 -1
- package/src/components/passkey-button.tsx +20 -9
- package/src/components/profile-settings.tsx +245 -230
- package/src/components/register-form.tsx +3 -1
- package/src/components/reset-password-form.tsx +6 -4
- package/src/components/verify-email.tsx +3 -1
- package/src/index.ts +1 -0
- package/src/provider.tsx +53 -15
|
@@ -5,10 +5,18 @@ import { useYAuth } from "../provider";
|
|
|
5
5
|
export const ProfileSettings: Component = () => {
|
|
6
6
|
const { client, user, loading: userLoading } = useYAuth();
|
|
7
7
|
|
|
8
|
+
const passkeyClient = client?.passkey;
|
|
9
|
+
const oauthClient = client?.oauth;
|
|
10
|
+
const mfaClient = client?.mfa;
|
|
11
|
+
const hasPasskey = !!passkeyClient;
|
|
12
|
+
const hasOAuth = !!oauthClient;
|
|
13
|
+
const hasMfa = !!mfaClient;
|
|
14
|
+
|
|
8
15
|
// Passkeys
|
|
9
16
|
const [passkeys, { refetch: refetchPasskeys }] = createResource(async () => {
|
|
17
|
+
if (!passkeyClient) return [];
|
|
10
18
|
try {
|
|
11
|
-
return await
|
|
19
|
+
return await passkeyClient.list();
|
|
12
20
|
} catch {
|
|
13
21
|
return [];
|
|
14
22
|
}
|
|
@@ -21,8 +29,9 @@ export const ProfileSettings: Component = () => {
|
|
|
21
29
|
// OAuth accounts
|
|
22
30
|
const [oauthAccounts, { refetch: refetchOAuth }] = createResource(
|
|
23
31
|
async () => {
|
|
32
|
+
if (!oauthClient) return [];
|
|
24
33
|
try {
|
|
25
|
-
return await
|
|
34
|
+
return await oauthClient.accounts();
|
|
26
35
|
} catch {
|
|
27
36
|
return [];
|
|
28
37
|
}
|
|
@@ -47,7 +56,7 @@ export const ProfileSettings: Component = () => {
|
|
|
47
56
|
setDeletingPasskey(id);
|
|
48
57
|
|
|
49
58
|
try {
|
|
50
|
-
await
|
|
59
|
+
await passkeyClient!.delete(id);
|
|
51
60
|
refetchPasskeys();
|
|
52
61
|
} catch (err) {
|
|
53
62
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -62,7 +71,7 @@ export const ProfileSettings: Component = () => {
|
|
|
62
71
|
setUnlinkingOAuth(provider);
|
|
63
72
|
|
|
64
73
|
try {
|
|
65
|
-
await
|
|
74
|
+
await oauthClient!.unlink(provider);
|
|
66
75
|
refetchOAuth();
|
|
67
76
|
} catch (err) {
|
|
68
77
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -77,7 +86,7 @@ export const ProfileSettings: Component = () => {
|
|
|
77
86
|
setMfaLoading(true);
|
|
78
87
|
|
|
79
88
|
try {
|
|
80
|
-
const result = await
|
|
89
|
+
const result = await mfaClient!.setup();
|
|
81
90
|
setMfaUri(result.otpauth_url);
|
|
82
91
|
setMfaSecret(result.secret);
|
|
83
92
|
setMfaBackupCodes(result.backup_codes);
|
|
@@ -98,9 +107,9 @@ export const ProfileSettings: Component = () => {
|
|
|
98
107
|
try {
|
|
99
108
|
const form = e.currentTarget as HTMLFormElement;
|
|
100
109
|
const formData = new FormData(form);
|
|
101
|
-
await
|
|
102
|
-
(formData.get("mfa_code") as string) || mfaCode(),
|
|
103
|
-
);
|
|
110
|
+
await mfaClient!.confirm({
|
|
111
|
+
code: (formData.get("mfa_code") as string) || mfaCode(),
|
|
112
|
+
});
|
|
104
113
|
setMfaStep("done");
|
|
105
114
|
} catch (err) {
|
|
106
115
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -115,7 +124,7 @@ export const ProfileSettings: Component = () => {
|
|
|
115
124
|
setMfaLoading(true);
|
|
116
125
|
|
|
117
126
|
try {
|
|
118
|
-
await
|
|
127
|
+
await mfaClient!.disable();
|
|
119
128
|
setMfaStep("idle");
|
|
120
129
|
setMfaUri("");
|
|
121
130
|
setMfaSecret("");
|
|
@@ -163,241 +172,247 @@ export const ProfileSettings: Component = () => {
|
|
|
163
172
|
</section>
|
|
164
173
|
|
|
165
174
|
{/* Passkeys */}
|
|
166
|
-
<
|
|
167
|
-
<
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
{(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
<
|
|
193
|
-
{(
|
|
194
|
-
|
|
195
|
-
<
|
|
196
|
-
<
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
175
|
+
<Show when={hasPasskey}>
|
|
176
|
+
<section class="space-y-4">
|
|
177
|
+
<h2 class="text-lg font-semibold tracking-tight">Passkeys</h2>
|
|
178
|
+
|
|
179
|
+
<Show when={passkeyError()}>
|
|
180
|
+
<div class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
|
181
|
+
{passkeyError()}
|
|
182
|
+
</div>
|
|
183
|
+
</Show>
|
|
184
|
+
|
|
185
|
+
<Show when={passkeys.loading}>
|
|
186
|
+
<div class="text-sm text-muted-foreground">
|
|
187
|
+
Loading passkeys...
|
|
188
|
+
</div>
|
|
189
|
+
</Show>
|
|
190
|
+
|
|
191
|
+
<Show when={passkeys()}>
|
|
192
|
+
{(passkeyList) => (
|
|
193
|
+
<Show
|
|
194
|
+
when={passkeyList().length > 0}
|
|
195
|
+
fallback={
|
|
196
|
+
<p class="text-sm text-muted-foreground">
|
|
197
|
+
No passkeys registered.
|
|
198
|
+
</p>
|
|
199
|
+
}
|
|
200
|
+
>
|
|
201
|
+
<ul class="space-y-2">
|
|
202
|
+
<For each={passkeyList()}>
|
|
203
|
+
{(passkey) => (
|
|
204
|
+
<li class="flex items-center justify-between rounded-md border border-input px-3 py-2">
|
|
205
|
+
<div class="space-y-0.5">
|
|
206
|
+
<span class="text-sm font-medium">
|
|
207
|
+
{passkey.name ?? "Unnamed passkey"}
|
|
208
|
+
</span>
|
|
209
|
+
<span class="block text-xs text-muted-foreground">
|
|
210
|
+
Added{" "}
|
|
211
|
+
{new Date(
|
|
212
|
+
passkey.created_at,
|
|
213
|
+
).toLocaleDateString()}
|
|
214
|
+
</span>
|
|
215
|
+
</div>
|
|
216
|
+
<button
|
|
217
|
+
class="inline-flex h-8 cursor-pointer items-center justify-center rounded-md border border-input bg-background px-3 text-xs font-medium shadow-sm transition-colors hover:bg-destructive hover:text-destructive-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
|
218
|
+
type="button"
|
|
219
|
+
onClick={() => handleDeletePasskey(passkey.id)}
|
|
220
|
+
disabled={deletingPasskey() === passkey.id}
|
|
221
|
+
>
|
|
222
|
+
{deletingPasskey() === passkey.id
|
|
223
|
+
? "Deleting..."
|
|
224
|
+
: "Delete"}
|
|
225
|
+
</button>
|
|
226
|
+
</li>
|
|
227
|
+
)}
|
|
228
|
+
</For>
|
|
229
|
+
</ul>
|
|
230
|
+
</Show>
|
|
231
|
+
)}
|
|
232
|
+
</Show>
|
|
233
|
+
</section>
|
|
234
|
+
</Show>
|
|
224
235
|
|
|
225
236
|
{/* OAuth accounts */}
|
|
226
|
-
<
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
<
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
<Show when={oauthAccounts.loading}>
|
|
238
|
-
<div class="text-sm text-muted-foreground">
|
|
239
|
-
Loading accounts...
|
|
240
|
-
</div>
|
|
241
|
-
</Show>
|
|
242
|
-
|
|
243
|
-
<Show when={oauthAccounts()}>
|
|
244
|
-
{(accountList) => (
|
|
245
|
-
<Show
|
|
246
|
-
when={accountList().length > 0}
|
|
247
|
-
fallback={
|
|
248
|
-
<p class="text-sm text-muted-foreground">
|
|
249
|
-
No connected accounts.
|
|
250
|
-
</p>
|
|
251
|
-
}
|
|
252
|
-
>
|
|
253
|
-
<ul class="space-y-2">
|
|
254
|
-
<For each={accountList()}>
|
|
255
|
-
{(account) => (
|
|
256
|
-
<li class="flex items-center justify-between rounded-md border border-input px-3 py-2">
|
|
257
|
-
<div class="space-y-0.5">
|
|
258
|
-
<span class="text-sm font-medium">
|
|
259
|
-
{account.provider.charAt(0).toUpperCase() +
|
|
260
|
-
account.provider.slice(1)}
|
|
261
|
-
</span>
|
|
262
|
-
<span class="block text-xs text-muted-foreground">
|
|
263
|
-
Connected{" "}
|
|
264
|
-
{new Date(
|
|
265
|
-
account.created_at,
|
|
266
|
-
).toLocaleDateString()}
|
|
267
|
-
</span>
|
|
268
|
-
</div>
|
|
269
|
-
<button
|
|
270
|
-
class="inline-flex h-8 cursor-pointer items-center justify-center rounded-md border border-input bg-background px-3 text-xs font-medium shadow-sm transition-colors hover:bg-destructive hover:text-destructive-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
|
271
|
-
type="button"
|
|
272
|
-
onClick={() =>
|
|
273
|
-
handleUnlinkOAuth(account.provider)
|
|
274
|
-
}
|
|
275
|
-
disabled={unlinkingOAuth() === account.provider}
|
|
276
|
-
>
|
|
277
|
-
{unlinkingOAuth() === account.provider
|
|
278
|
-
? "Unlinking..."
|
|
279
|
-
: "Unlink"}
|
|
280
|
-
</button>
|
|
281
|
-
</li>
|
|
282
|
-
)}
|
|
283
|
-
</For>
|
|
284
|
-
</ul>
|
|
285
|
-
</Show>
|
|
286
|
-
)}
|
|
287
|
-
</Show>
|
|
288
|
-
</section>
|
|
237
|
+
<Show when={hasOAuth}>
|
|
238
|
+
<section class="space-y-4">
|
|
239
|
+
<h2 class="text-lg font-semibold tracking-tight">
|
|
240
|
+
Connected accounts
|
|
241
|
+
</h2>
|
|
242
|
+
|
|
243
|
+
<Show when={oauthError()}>
|
|
244
|
+
<div class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
|
245
|
+
{oauthError()}
|
|
246
|
+
</div>
|
|
247
|
+
</Show>
|
|
289
248
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
Two-factor authentication
|
|
294
|
-
</h2>
|
|
295
|
-
|
|
296
|
-
<Show when={mfaError()}>
|
|
297
|
-
<div class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
|
298
|
-
{mfaError()}
|
|
299
|
-
</div>
|
|
300
|
-
</Show>
|
|
301
|
-
|
|
302
|
-
<Show when={mfaStep() === "idle"}>
|
|
303
|
-
<div class="flex gap-2">
|
|
304
|
-
<button
|
|
305
|
-
class="inline-flex h-9 cursor-pointer items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
|
306
|
-
type="button"
|
|
307
|
-
onClick={handleMfaBegin}
|
|
308
|
-
disabled={mfaLoading()}
|
|
309
|
-
>
|
|
310
|
-
{mfaLoading() ? "Setting up..." : "Set up 2FA"}
|
|
311
|
-
</button>
|
|
312
|
-
|
|
313
|
-
<button
|
|
314
|
-
class="inline-flex h-9 cursor-pointer items-center justify-center rounded-md border border-input bg-background px-4 py-2 text-sm font-medium shadow-sm transition-colors hover:bg-destructive hover:text-destructive-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
|
315
|
-
type="button"
|
|
316
|
-
onClick={handleMfaDisable}
|
|
317
|
-
disabled={mfaLoading()}
|
|
318
|
-
>
|
|
319
|
-
{mfaLoading() ? "Disabling..." : "Disable 2FA"}
|
|
320
|
-
</button>
|
|
321
|
-
</div>
|
|
322
|
-
</Show>
|
|
323
|
-
|
|
324
|
-
<Show when={mfaStep() === "confirm"}>
|
|
325
|
-
<div class="space-y-4">
|
|
326
|
-
<p class="text-sm text-muted-foreground">
|
|
327
|
-
Add this account to your authenticator app, then enter the
|
|
328
|
-
verification code.
|
|
329
|
-
</p>
|
|
330
|
-
|
|
331
|
-
<div class="space-y-1">
|
|
332
|
-
<span class="text-sm font-medium leading-none">
|
|
333
|
-
OTP Auth URI
|
|
334
|
-
</span>
|
|
335
|
-
<code class="block w-full break-all rounded-md border border-input bg-muted px-3 py-2 text-xs">
|
|
336
|
-
{mfaUri()}
|
|
337
|
-
</code>
|
|
249
|
+
<Show when={oauthAccounts.loading}>
|
|
250
|
+
<div class="text-sm text-muted-foreground">
|
|
251
|
+
Loading accounts...
|
|
338
252
|
</div>
|
|
253
|
+
</Show>
|
|
254
|
+
|
|
255
|
+
<Show when={oauthAccounts()}>
|
|
256
|
+
{(accountList) => (
|
|
257
|
+
<Show
|
|
258
|
+
when={accountList().length > 0}
|
|
259
|
+
fallback={
|
|
260
|
+
<p class="text-sm text-muted-foreground">
|
|
261
|
+
No connected accounts.
|
|
262
|
+
</p>
|
|
263
|
+
}
|
|
264
|
+
>
|
|
265
|
+
<ul class="space-y-2">
|
|
266
|
+
<For each={accountList()}>
|
|
267
|
+
{(account) => (
|
|
268
|
+
<li class="flex items-center justify-between rounded-md border border-input px-3 py-2">
|
|
269
|
+
<div class="space-y-0.5">
|
|
270
|
+
<span class="text-sm font-medium">
|
|
271
|
+
{account.provider.charAt(0).toUpperCase() +
|
|
272
|
+
account.provider.slice(1)}
|
|
273
|
+
</span>
|
|
274
|
+
<span class="block text-xs text-muted-foreground">
|
|
275
|
+
Connected{" "}
|
|
276
|
+
{new Date(
|
|
277
|
+
account.created_at,
|
|
278
|
+
).toLocaleDateString()}
|
|
279
|
+
</span>
|
|
280
|
+
</div>
|
|
281
|
+
<button
|
|
282
|
+
class="inline-flex h-8 cursor-pointer items-center justify-center rounded-md border border-input bg-background px-3 text-xs font-medium shadow-sm transition-colors hover:bg-destructive hover:text-destructive-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
|
283
|
+
type="button"
|
|
284
|
+
onClick={() =>
|
|
285
|
+
handleUnlinkOAuth(account.provider)
|
|
286
|
+
}
|
|
287
|
+
disabled={unlinkingOAuth() === account.provider}
|
|
288
|
+
>
|
|
289
|
+
{unlinkingOAuth() === account.provider
|
|
290
|
+
? "Unlinking..."
|
|
291
|
+
: "Unlink"}
|
|
292
|
+
</button>
|
|
293
|
+
</li>
|
|
294
|
+
)}
|
|
295
|
+
</For>
|
|
296
|
+
</ul>
|
|
297
|
+
</Show>
|
|
298
|
+
)}
|
|
299
|
+
</Show>
|
|
300
|
+
</section>
|
|
301
|
+
</Show>
|
|
339
302
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
303
|
+
{/* MFA setup */}
|
|
304
|
+
<Show when={hasMfa}>
|
|
305
|
+
<section class="space-y-4">
|
|
306
|
+
<h2 class="text-lg font-semibold tracking-tight">
|
|
307
|
+
Two-factor authentication
|
|
308
|
+
</h2>
|
|
309
|
+
|
|
310
|
+
<Show when={mfaError()}>
|
|
311
|
+
<div class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
|
312
|
+
{mfaError()}
|
|
347
313
|
</div>
|
|
314
|
+
</Show>
|
|
348
315
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
id="yauth-profile-mfa-code"
|
|
360
|
-
name="mfa_code"
|
|
361
|
-
type="text"
|
|
362
|
-
inputmode="numeric"
|
|
363
|
-
autocomplete="one-time-code"
|
|
364
|
-
value={mfaCode()}
|
|
365
|
-
onInput={(e) => setMfaCode(e.currentTarget.value)}
|
|
366
|
-
required
|
|
367
|
-
disabled={mfaLoading()}
|
|
368
|
-
/>
|
|
369
|
-
</div>
|
|
316
|
+
<Show when={mfaStep() === "idle"}>
|
|
317
|
+
<div class="flex gap-2">
|
|
318
|
+
<button
|
|
319
|
+
class="inline-flex h-9 cursor-pointer items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
|
320
|
+
type="button"
|
|
321
|
+
onClick={handleMfaBegin}
|
|
322
|
+
disabled={mfaLoading()}
|
|
323
|
+
>
|
|
324
|
+
{mfaLoading() ? "Setting up..." : "Set up 2FA"}
|
|
325
|
+
</button>
|
|
370
326
|
|
|
371
327
|
<button
|
|
372
|
-
class="inline-flex h-9
|
|
373
|
-
type="
|
|
328
|
+
class="inline-flex h-9 cursor-pointer items-center justify-center rounded-md border border-input bg-background px-4 py-2 text-sm font-medium shadow-sm transition-colors hover:bg-destructive hover:text-destructive-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
|
329
|
+
type="button"
|
|
330
|
+
onClick={handleMfaDisable}
|
|
374
331
|
disabled={mfaLoading()}
|
|
375
332
|
>
|
|
376
|
-
{mfaLoading() ? "
|
|
333
|
+
{mfaLoading() ? "Disabling..." : "Disable 2FA"}
|
|
377
334
|
</button>
|
|
378
|
-
</form>
|
|
379
|
-
</div>
|
|
380
|
-
</Show>
|
|
381
|
-
|
|
382
|
-
<Show when={mfaStep() === "done"}>
|
|
383
|
-
<div class="space-y-4">
|
|
384
|
-
<div class="rounded-md bg-emerald-500/10 px-3 py-2 text-sm text-emerald-600 dark:text-emerald-400">
|
|
385
|
-
Two-factor authentication is enabled. Save these backup
|
|
386
|
-
codes in a safe place.
|
|
387
335
|
</div>
|
|
336
|
+
</Show>
|
|
388
337
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
338
|
+
<Show when={mfaStep() === "confirm"}>
|
|
339
|
+
<div class="space-y-4">
|
|
340
|
+
<p class="text-sm text-muted-foreground">
|
|
341
|
+
Add this account to your authenticator app, then enter the
|
|
342
|
+
verification code.
|
|
343
|
+
</p>
|
|
344
|
+
|
|
345
|
+
<div class="space-y-1">
|
|
346
|
+
<span class="text-sm font-medium leading-none">
|
|
347
|
+
OTP Auth URI
|
|
348
|
+
</span>
|
|
349
|
+
<code class="block w-full break-all rounded-md border border-input bg-muted px-3 py-2 text-xs">
|
|
350
|
+
{mfaUri()}
|
|
351
|
+
</code>
|
|
352
|
+
</div>
|
|
353
|
+
|
|
354
|
+
<div class="space-y-1">
|
|
355
|
+
<span class="text-sm font-medium leading-none">
|
|
356
|
+
Manual entry key
|
|
357
|
+
</span>
|
|
358
|
+
<code class="block w-full break-all rounded-md border border-input bg-muted px-3 py-2 text-xs font-mono tracking-wider">
|
|
359
|
+
{mfaSecret()}
|
|
360
|
+
</code>
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
<form class="space-y-4" onSubmit={handleMfaConfirm}>
|
|
364
|
+
<div class="space-y-2">
|
|
365
|
+
<label
|
|
366
|
+
class="text-sm font-medium leading-none"
|
|
367
|
+
for="yauth-profile-mfa-code"
|
|
368
|
+
>
|
|
369
|
+
Verification code
|
|
370
|
+
</label>
|
|
371
|
+
<input
|
|
372
|
+
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
|
|
373
|
+
id="yauth-profile-mfa-code"
|
|
374
|
+
name="mfa_code"
|
|
375
|
+
type="text"
|
|
376
|
+
inputmode="numeric"
|
|
377
|
+
autocomplete="one-time-code"
|
|
378
|
+
value={mfaCode()}
|
|
379
|
+
onInput={(e) => setMfaCode(e.currentTarget.value)}
|
|
380
|
+
required
|
|
381
|
+
disabled={mfaLoading()}
|
|
382
|
+
/>
|
|
383
|
+
</div>
|
|
384
|
+
|
|
385
|
+
<button
|
|
386
|
+
class="inline-flex h-9 w-full cursor-pointer items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
|
387
|
+
type="submit"
|
|
388
|
+
disabled={mfaLoading()}
|
|
389
|
+
>
|
|
390
|
+
{mfaLoading() ? "Verifying..." : "Verify and enable"}
|
|
391
|
+
</button>
|
|
392
|
+
</form>
|
|
393
|
+
</div>
|
|
394
|
+
</Show>
|
|
395
|
+
|
|
396
|
+
<Show when={mfaStep() === "done"}>
|
|
397
|
+
<div class="space-y-4">
|
|
398
|
+
<div class="rounded-md bg-emerald-500/10 px-3 py-2 text-sm text-emerald-600 dark:text-emerald-400">
|
|
399
|
+
Two-factor authentication is enabled. Save these backup
|
|
400
|
+
codes in a safe place.
|
|
401
|
+
</div>
|
|
402
|
+
|
|
403
|
+
<ul class="space-y-1">
|
|
404
|
+
<For each={mfaBackupCodes()}>
|
|
405
|
+
{(code) => (
|
|
406
|
+
<li class="rounded-md border border-input bg-muted px-3 py-1.5 text-center font-mono text-sm tracking-wider">
|
|
407
|
+
{code}
|
|
408
|
+
</li>
|
|
409
|
+
)}
|
|
410
|
+
</For>
|
|
411
|
+
</ul>
|
|
412
|
+
</div>
|
|
413
|
+
</Show>
|
|
414
|
+
</section>
|
|
415
|
+
</Show>
|
|
401
416
|
</>
|
|
402
417
|
)}
|
|
403
418
|
</Show>
|
|
@@ -9,6 +9,8 @@ export interface RegisterFormProps {
|
|
|
9
9
|
|
|
10
10
|
export const RegisterForm: Component<RegisterFormProps> = (props) => {
|
|
11
11
|
const { client } = useYAuth();
|
|
12
|
+
const ep = client?.emailPassword;
|
|
13
|
+
if (!ep) return null;
|
|
12
14
|
const [email, setEmail] = createSignal("");
|
|
13
15
|
const [password, setPassword] = createSignal("");
|
|
14
16
|
const [displayName, setDisplayName] = createSignal("");
|
|
@@ -25,7 +27,7 @@ export const RegisterForm: Component<RegisterFormProps> = (props) => {
|
|
|
25
27
|
const formData = new FormData(form);
|
|
26
28
|
const formDisplayName =
|
|
27
29
|
(formData.get("display_name") as string) || displayName();
|
|
28
|
-
const result = await
|
|
30
|
+
const result = await ep.register({
|
|
29
31
|
email: (formData.get("email") as string) || email(),
|
|
30
32
|
password: (formData.get("password") as string) || password(),
|
|
31
33
|
display_name: formDisplayName || undefined,
|
|
@@ -9,6 +9,8 @@ export interface ResetPasswordFormProps {
|
|
|
9
9
|
|
|
10
10
|
export const ResetPasswordForm: Component<ResetPasswordFormProps> = (props) => {
|
|
11
11
|
const { client } = useYAuth();
|
|
12
|
+
const ep = client?.emailPassword;
|
|
13
|
+
if (!ep) return null;
|
|
12
14
|
const [password, setPassword] = createSignal("");
|
|
13
15
|
const [error, setError] = createSignal<string | null>(null);
|
|
14
16
|
const [success, setSuccess] = createSignal<string | null>(null);
|
|
@@ -23,10 +25,10 @@ export const ResetPasswordForm: Component<ResetPasswordFormProps> = (props) => {
|
|
|
23
25
|
try {
|
|
24
26
|
const form = e.currentTarget as HTMLFormElement;
|
|
25
27
|
const formData = new FormData(form);
|
|
26
|
-
const result = await
|
|
27
|
-
props.token,
|
|
28
|
-
(formData.get("password") as string) || password(),
|
|
29
|
-
);
|
|
28
|
+
const result = await ep.resetPassword({
|
|
29
|
+
token: props.token,
|
|
30
|
+
password: (formData.get("password") as string) || password(),
|
|
31
|
+
});
|
|
30
32
|
setSuccess(result.message);
|
|
31
33
|
props.onSuccess?.(result.message);
|
|
32
34
|
} catch (err) {
|
|
@@ -12,13 +12,15 @@ type VerifyState = "loading" | "success" | "error";
|
|
|
12
12
|
|
|
13
13
|
export const VerifyEmail: Component<VerifyEmailProps> = (props) => {
|
|
14
14
|
const { client } = useYAuth();
|
|
15
|
+
const ep = client?.emailPassword;
|
|
16
|
+
if (!ep) return null;
|
|
15
17
|
const [state, setState] = createSignal<VerifyState>("loading");
|
|
16
18
|
const [message, setMessage] = createSignal("");
|
|
17
19
|
const [errorMessage, setErrorMessage] = createSignal("");
|
|
18
20
|
|
|
19
21
|
createEffect(async () => {
|
|
20
22
|
try {
|
|
21
|
-
const result = await
|
|
23
|
+
const result = await ep.verify({ token: props.token });
|
|
22
24
|
setMessage(result.message);
|
|
23
25
|
setState("success");
|
|
24
26
|
props.onSuccess?.();
|
package/src/index.ts
CHANGED
|
@@ -11,4 +11,5 @@ export { ProfileSettings } from "./components/profile-settings";
|
|
|
11
11
|
export { RegisterForm } from "./components/register-form";
|
|
12
12
|
export { ResetPasswordForm } from "./components/reset-password-form";
|
|
13
13
|
export { VerifyEmail } from "./components/verify-email";
|
|
14
|
+
export type { YAuthClient } from "./provider";
|
|
14
15
|
export { useYAuth, YAuthProvider } from "./provider";
|