@yackey-labs/yauth-ui-solidjs 0.1.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 +909 -0
- package/package.json +35 -0
- package/src/components/change-password-form.tsx +140 -0
- package/src/components/consent-screen.tsx +152 -0
- package/src/components/forgot-password-form.tsx +83 -0
- package/src/components/login-form.tsx +128 -0
- package/src/components/magic-link-form.tsx +84 -0
- package/src/components/mfa-challenge.tsx +83 -0
- package/src/components/mfa-setup.tsx +158 -0
- package/src/components/oauth-buttons.tsx +30 -0
- package/src/components/passkey-button.tsx +98 -0
- package/src/components/profile-settings.tsx +406 -0
- package/src/components/register-form.tsx +119 -0
- package/src/components/reset-password-form.tsx +83 -0
- package/src/components/verify-email.tsx +54 -0
- package/src/index.ts +14 -0
- package/src/provider.test.tsx +159 -0
- package/src/provider.tsx +73 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { type Component, createResource, createSignal, For } from "solid-js";
|
|
2
|
+
import { Show } from "solid-js/web";
|
|
3
|
+
import { useYAuth } from "../provider";
|
|
4
|
+
|
|
5
|
+
export const ProfileSettings: Component = () => {
|
|
6
|
+
const { client, user, loading: userLoading } = useYAuth();
|
|
7
|
+
|
|
8
|
+
// Passkeys
|
|
9
|
+
const [passkeys, { refetch: refetchPasskeys }] = createResource(async () => {
|
|
10
|
+
try {
|
|
11
|
+
return await client.passkey.list();
|
|
12
|
+
} catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
const [passkeyError, setPasskeyError] = createSignal<string | null>(null);
|
|
17
|
+
const [deletingPasskey, setDeletingPasskey] = createSignal<string | null>(
|
|
18
|
+
null,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// OAuth accounts
|
|
22
|
+
const [oauthAccounts, { refetch: refetchOAuth }] = createResource(
|
|
23
|
+
async () => {
|
|
24
|
+
try {
|
|
25
|
+
return await client.oauth.accounts();
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
const [oauthError, setOauthError] = createSignal<string | null>(null);
|
|
32
|
+
const [unlinkingOAuth, setUnlinkingOAuth] = createSignal<string | null>(null);
|
|
33
|
+
|
|
34
|
+
// MFA state
|
|
35
|
+
const [mfaUri, setMfaUri] = createSignal("");
|
|
36
|
+
const [mfaSecret, setMfaSecret] = createSignal("");
|
|
37
|
+
const [mfaCode, setMfaCode] = createSignal("");
|
|
38
|
+
const [mfaBackupCodes, setMfaBackupCodes] = createSignal<string[]>([]);
|
|
39
|
+
const [mfaStep, setMfaStep] = createSignal<
|
|
40
|
+
"idle" | "setup" | "confirm" | "done"
|
|
41
|
+
>("idle");
|
|
42
|
+
const [mfaError, setMfaError] = createSignal<string | null>(null);
|
|
43
|
+
const [mfaLoading, setMfaLoading] = createSignal(false);
|
|
44
|
+
|
|
45
|
+
const handleDeletePasskey = async (id: string) => {
|
|
46
|
+
setPasskeyError(null);
|
|
47
|
+
setDeletingPasskey(id);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
await client.passkey.delete(id);
|
|
51
|
+
refetchPasskeys();
|
|
52
|
+
} catch (err) {
|
|
53
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
54
|
+
setPasskeyError(error.message);
|
|
55
|
+
} finally {
|
|
56
|
+
setDeletingPasskey(null);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleUnlinkOAuth = async (provider: string) => {
|
|
61
|
+
setOauthError(null);
|
|
62
|
+
setUnlinkingOAuth(provider);
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
await client.oauth.unlink(provider);
|
|
66
|
+
refetchOAuth();
|
|
67
|
+
} catch (err) {
|
|
68
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
69
|
+
setOauthError(error.message);
|
|
70
|
+
} finally {
|
|
71
|
+
setUnlinkingOAuth(null);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleMfaBegin = async () => {
|
|
76
|
+
setMfaError(null);
|
|
77
|
+
setMfaLoading(true);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const result = await client.mfa.setup();
|
|
81
|
+
setMfaUri(result.otpauth_url);
|
|
82
|
+
setMfaSecret(result.secret);
|
|
83
|
+
setMfaBackupCodes(result.backup_codes);
|
|
84
|
+
setMfaStep("confirm");
|
|
85
|
+
} catch (err) {
|
|
86
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
87
|
+
setMfaError(error.message);
|
|
88
|
+
} finally {
|
|
89
|
+
setMfaLoading(false);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const handleMfaConfirm = async (e: SubmitEvent) => {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
setMfaError(null);
|
|
96
|
+
setMfaLoading(true);
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const form = e.currentTarget as HTMLFormElement;
|
|
100
|
+
const formData = new FormData(form);
|
|
101
|
+
await client.mfa.confirm(
|
|
102
|
+
(formData.get("mfa_code") as string) || mfaCode(),
|
|
103
|
+
);
|
|
104
|
+
setMfaStep("done");
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
107
|
+
setMfaError(error.message);
|
|
108
|
+
} finally {
|
|
109
|
+
setMfaLoading(false);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handleMfaDisable = async () => {
|
|
114
|
+
setMfaError(null);
|
|
115
|
+
setMfaLoading(true);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await client.mfa.disable();
|
|
119
|
+
setMfaStep("idle");
|
|
120
|
+
setMfaUri("");
|
|
121
|
+
setMfaSecret("");
|
|
122
|
+
setMfaCode("");
|
|
123
|
+
setMfaBackupCodes([]);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
126
|
+
setMfaError(error.message);
|
|
127
|
+
} finally {
|
|
128
|
+
setMfaLoading(false);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div class="space-y-8">
|
|
134
|
+
<Show when={userLoading()}>
|
|
135
|
+
<div class="text-sm text-muted-foreground">Loading profile...</div>
|
|
136
|
+
</Show>
|
|
137
|
+
|
|
138
|
+
<Show when={user()}>
|
|
139
|
+
{(currentUser) => (
|
|
140
|
+
<>
|
|
141
|
+
{/* User info */}
|
|
142
|
+
<section class="space-y-4">
|
|
143
|
+
<h2 class="text-lg font-semibold tracking-tight">Profile</h2>
|
|
144
|
+
<dl class="grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-sm">
|
|
145
|
+
<dt class="font-medium text-muted-foreground">Email</dt>
|
|
146
|
+
<dd>{currentUser().email}</dd>
|
|
147
|
+
|
|
148
|
+
<Show when={currentUser().display_name}>
|
|
149
|
+
<dt class="font-medium text-muted-foreground">
|
|
150
|
+
Display name
|
|
151
|
+
</dt>
|
|
152
|
+
<dd>{currentUser().display_name}</dd>
|
|
153
|
+
</Show>
|
|
154
|
+
|
|
155
|
+
<dt class="font-medium text-muted-foreground">
|
|
156
|
+
Email verified
|
|
157
|
+
</dt>
|
|
158
|
+
<dd>{currentUser().email_verified ? "Yes" : "No"}</dd>
|
|
159
|
+
|
|
160
|
+
<dt class="font-medium text-muted-foreground">Role</dt>
|
|
161
|
+
<dd>{currentUser().role}</dd>
|
|
162
|
+
</dl>
|
|
163
|
+
</section>
|
|
164
|
+
|
|
165
|
+
{/* Passkeys */}
|
|
166
|
+
<section class="space-y-4">
|
|
167
|
+
<h2 class="text-lg font-semibold tracking-tight">Passkeys</h2>
|
|
168
|
+
|
|
169
|
+
<Show when={passkeyError()}>
|
|
170
|
+
<div class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
|
171
|
+
{passkeyError()}
|
|
172
|
+
</div>
|
|
173
|
+
</Show>
|
|
174
|
+
|
|
175
|
+
<Show when={passkeys.loading}>
|
|
176
|
+
<div class="text-sm text-muted-foreground">
|
|
177
|
+
Loading passkeys...
|
|
178
|
+
</div>
|
|
179
|
+
</Show>
|
|
180
|
+
|
|
181
|
+
<Show when={passkeys()}>
|
|
182
|
+
{(passkeyList) => (
|
|
183
|
+
<Show
|
|
184
|
+
when={passkeyList().length > 0}
|
|
185
|
+
fallback={
|
|
186
|
+
<p class="text-sm text-muted-foreground">
|
|
187
|
+
No passkeys registered.
|
|
188
|
+
</p>
|
|
189
|
+
}
|
|
190
|
+
>
|
|
191
|
+
<ul class="space-y-2">
|
|
192
|
+
<For each={passkeyList()}>
|
|
193
|
+
{(passkey) => (
|
|
194
|
+
<li class="flex items-center justify-between rounded-md border border-input px-3 py-2">
|
|
195
|
+
<div class="space-y-0.5">
|
|
196
|
+
<span class="text-sm font-medium">
|
|
197
|
+
{passkey.name ?? "Unnamed passkey"}
|
|
198
|
+
</span>
|
|
199
|
+
<span class="block text-xs text-muted-foreground">
|
|
200
|
+
Added{" "}
|
|
201
|
+
{new Date(
|
|
202
|
+
passkey.created_at,
|
|
203
|
+
).toLocaleDateString()}
|
|
204
|
+
</span>
|
|
205
|
+
</div>
|
|
206
|
+
<button
|
|
207
|
+
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"
|
|
208
|
+
type="button"
|
|
209
|
+
onClick={() => handleDeletePasskey(passkey.id)}
|
|
210
|
+
disabled={deletingPasskey() === passkey.id}
|
|
211
|
+
>
|
|
212
|
+
{deletingPasskey() === passkey.id
|
|
213
|
+
? "Deleting..."
|
|
214
|
+
: "Delete"}
|
|
215
|
+
</button>
|
|
216
|
+
</li>
|
|
217
|
+
)}
|
|
218
|
+
</For>
|
|
219
|
+
</ul>
|
|
220
|
+
</Show>
|
|
221
|
+
)}
|
|
222
|
+
</Show>
|
|
223
|
+
</section>
|
|
224
|
+
|
|
225
|
+
{/* OAuth accounts */}
|
|
226
|
+
<section class="space-y-4">
|
|
227
|
+
<h2 class="text-lg font-semibold tracking-tight">
|
|
228
|
+
Connected accounts
|
|
229
|
+
</h2>
|
|
230
|
+
|
|
231
|
+
<Show when={oauthError()}>
|
|
232
|
+
<div class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
|
233
|
+
{oauthError()}
|
|
234
|
+
</div>
|
|
235
|
+
</Show>
|
|
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>
|
|
289
|
+
|
|
290
|
+
{/* MFA setup */}
|
|
291
|
+
<section class="space-y-4">
|
|
292
|
+
<h2 class="text-lg font-semibold tracking-tight">
|
|
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>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
<div class="space-y-1">
|
|
341
|
+
<span class="text-sm font-medium leading-none">
|
|
342
|
+
Manual entry key
|
|
343
|
+
</span>
|
|
344
|
+
<code class="block w-full break-all rounded-md border border-input bg-muted px-3 py-2 text-xs font-mono tracking-wider">
|
|
345
|
+
{mfaSecret()}
|
|
346
|
+
</code>
|
|
347
|
+
</div>
|
|
348
|
+
|
|
349
|
+
<form class="space-y-4" onSubmit={handleMfaConfirm}>
|
|
350
|
+
<div class="space-y-2">
|
|
351
|
+
<label
|
|
352
|
+
class="text-sm font-medium leading-none"
|
|
353
|
+
for="yauth-profile-mfa-code"
|
|
354
|
+
>
|
|
355
|
+
Verification code
|
|
356
|
+
</label>
|
|
357
|
+
<input
|
|
358
|
+
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"
|
|
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>
|
|
370
|
+
|
|
371
|
+
<button
|
|
372
|
+
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"
|
|
373
|
+
type="submit"
|
|
374
|
+
disabled={mfaLoading()}
|
|
375
|
+
>
|
|
376
|
+
{mfaLoading() ? "Verifying..." : "Verify and enable"}
|
|
377
|
+
</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
|
+
</div>
|
|
388
|
+
|
|
389
|
+
<ul class="space-y-1">
|
|
390
|
+
<For each={mfaBackupCodes()}>
|
|
391
|
+
{(code) => (
|
|
392
|
+
<li class="rounded-md border border-input bg-muted px-3 py-1.5 text-center font-mono text-sm tracking-wider">
|
|
393
|
+
{code}
|
|
394
|
+
</li>
|
|
395
|
+
)}
|
|
396
|
+
</For>
|
|
397
|
+
</ul>
|
|
398
|
+
</div>
|
|
399
|
+
</Show>
|
|
400
|
+
</section>
|
|
401
|
+
</>
|
|
402
|
+
)}
|
|
403
|
+
</Show>
|
|
404
|
+
</div>
|
|
405
|
+
);
|
|
406
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { type Component, createSignal } from "solid-js";
|
|
2
|
+
import { Show } from "solid-js/web";
|
|
3
|
+
import { useYAuth } from "../provider";
|
|
4
|
+
|
|
5
|
+
export interface RegisterFormProps {
|
|
6
|
+
onSuccess?: (message: string) => void;
|
|
7
|
+
onError?: (error: Error) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const RegisterForm: Component<RegisterFormProps> = (props) => {
|
|
11
|
+
const { client } = useYAuth();
|
|
12
|
+
const [email, setEmail] = createSignal("");
|
|
13
|
+
const [password, setPassword] = createSignal("");
|
|
14
|
+
const [displayName, setDisplayName] = createSignal("");
|
|
15
|
+
const [error, setError] = createSignal<string | null>(null);
|
|
16
|
+
const [loading, setLoading] = createSignal(false);
|
|
17
|
+
|
|
18
|
+
const handleSubmit = async (e: SubmitEvent) => {
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
setError(null);
|
|
21
|
+
setLoading(true);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const form = e.currentTarget as HTMLFormElement;
|
|
25
|
+
const formData = new FormData(form);
|
|
26
|
+
const formDisplayName =
|
|
27
|
+
(formData.get("display_name") as string) || displayName();
|
|
28
|
+
const result = await client.emailPassword.register({
|
|
29
|
+
email: (formData.get("email") as string) || email(),
|
|
30
|
+
password: (formData.get("password") as string) || password(),
|
|
31
|
+
display_name: formDisplayName || undefined,
|
|
32
|
+
});
|
|
33
|
+
props.onSuccess?.(result.message);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
36
|
+
setError(error.message);
|
|
37
|
+
props.onError?.(error);
|
|
38
|
+
} finally {
|
|
39
|
+
setLoading(false);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<form class="space-y-6" onSubmit={handleSubmit}>
|
|
45
|
+
<Show when={error()}>
|
|
46
|
+
<div class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
|
47
|
+
{error()}
|
|
48
|
+
</div>
|
|
49
|
+
</Show>
|
|
50
|
+
|
|
51
|
+
<div class="space-y-2">
|
|
52
|
+
<label
|
|
53
|
+
class="text-sm font-medium leading-none"
|
|
54
|
+
for="yauth-register-email"
|
|
55
|
+
>
|
|
56
|
+
Email
|
|
57
|
+
</label>
|
|
58
|
+
<input
|
|
59
|
+
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"
|
|
60
|
+
id="yauth-register-email"
|
|
61
|
+
name="email"
|
|
62
|
+
type="email"
|
|
63
|
+
value={email()}
|
|
64
|
+
onInput={(e) => setEmail(e.currentTarget.value)}
|
|
65
|
+
required
|
|
66
|
+
autocomplete="email"
|
|
67
|
+
disabled={loading()}
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div class="space-y-2">
|
|
72
|
+
<label
|
|
73
|
+
class="text-sm font-medium leading-none"
|
|
74
|
+
for="yauth-register-password"
|
|
75
|
+
>
|
|
76
|
+
Password
|
|
77
|
+
</label>
|
|
78
|
+
<input
|
|
79
|
+
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"
|
|
80
|
+
id="yauth-register-password"
|
|
81
|
+
name="password"
|
|
82
|
+
type="password"
|
|
83
|
+
value={password()}
|
|
84
|
+
onInput={(e) => setPassword(e.currentTarget.value)}
|
|
85
|
+
required
|
|
86
|
+
autocomplete="new-password"
|
|
87
|
+
disabled={loading()}
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div class="space-y-2">
|
|
92
|
+
<label
|
|
93
|
+
class="text-sm font-medium leading-none"
|
|
94
|
+
for="yauth-register-display-name"
|
|
95
|
+
>
|
|
96
|
+
Display name (optional)
|
|
97
|
+
</label>
|
|
98
|
+
<input
|
|
99
|
+
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"
|
|
100
|
+
id="yauth-register-display-name"
|
|
101
|
+
name="display_name"
|
|
102
|
+
type="text"
|
|
103
|
+
value={displayName()}
|
|
104
|
+
onInput={(e) => setDisplayName(e.currentTarget.value)}
|
|
105
|
+
autocomplete="name"
|
|
106
|
+
disabled={loading()}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<button
|
|
111
|
+
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"
|
|
112
|
+
type="submit"
|
|
113
|
+
disabled={loading()}
|
|
114
|
+
>
|
|
115
|
+
{loading() ? "Creating account..." : "Create account"}
|
|
116
|
+
</button>
|
|
117
|
+
</form>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type Component, createSignal } from "solid-js";
|
|
2
|
+
import { Show } from "solid-js/web";
|
|
3
|
+
import { useYAuth } from "../provider";
|
|
4
|
+
|
|
5
|
+
export interface ResetPasswordFormProps {
|
|
6
|
+
token: string;
|
|
7
|
+
onSuccess?: (message: string) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const ResetPasswordForm: Component<ResetPasswordFormProps> = (props) => {
|
|
11
|
+
const { client } = useYAuth();
|
|
12
|
+
const [password, setPassword] = createSignal("");
|
|
13
|
+
const [error, setError] = createSignal<string | null>(null);
|
|
14
|
+
const [success, setSuccess] = createSignal<string | null>(null);
|
|
15
|
+
const [loading, setLoading] = createSignal(false);
|
|
16
|
+
|
|
17
|
+
const handleSubmit = async (e: SubmitEvent) => {
|
|
18
|
+
e.preventDefault();
|
|
19
|
+
setError(null);
|
|
20
|
+
setSuccess(null);
|
|
21
|
+
setLoading(true);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const form = e.currentTarget as HTMLFormElement;
|
|
25
|
+
const formData = new FormData(form);
|
|
26
|
+
const result = await client.emailPassword.resetPassword(
|
|
27
|
+
props.token,
|
|
28
|
+
(formData.get("password") as string) || password(),
|
|
29
|
+
);
|
|
30
|
+
setSuccess(result.message);
|
|
31
|
+
props.onSuccess?.(result.message);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
34
|
+
setError(error.message);
|
|
35
|
+
} finally {
|
|
36
|
+
setLoading(false);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<form class="space-y-4" onSubmit={handleSubmit}>
|
|
42
|
+
<Show when={error()}>
|
|
43
|
+
<div class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
|
44
|
+
{error()}
|
|
45
|
+
</div>
|
|
46
|
+
</Show>
|
|
47
|
+
|
|
48
|
+
<Show when={success()}>
|
|
49
|
+
<div class="rounded-md bg-emerald-500/10 px-3 py-2 text-sm text-emerald-600 dark:text-emerald-400">
|
|
50
|
+
{success()}
|
|
51
|
+
</div>
|
|
52
|
+
</Show>
|
|
53
|
+
|
|
54
|
+
<div class="space-y-2">
|
|
55
|
+
<label
|
|
56
|
+
class="text-sm font-medium leading-none"
|
|
57
|
+
for="yauth-reset-password-input"
|
|
58
|
+
>
|
|
59
|
+
New password
|
|
60
|
+
</label>
|
|
61
|
+
<input
|
|
62
|
+
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"
|
|
63
|
+
id="yauth-reset-password-input"
|
|
64
|
+
name="password"
|
|
65
|
+
type="password"
|
|
66
|
+
value={password()}
|
|
67
|
+
onInput={(e) => setPassword(e.currentTarget.value)}
|
|
68
|
+
required
|
|
69
|
+
autocomplete="new-password"
|
|
70
|
+
disabled={loading()}
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<button
|
|
75
|
+
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"
|
|
76
|
+
type="submit"
|
|
77
|
+
disabled={loading()}
|
|
78
|
+
>
|
|
79
|
+
{loading() ? "Resetting..." : "Reset password"}
|
|
80
|
+
</button>
|
|
81
|
+
</form>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type Component, createEffect, createSignal } from "solid-js";
|
|
2
|
+
import { Show } from "solid-js/web";
|
|
3
|
+
import { useYAuth } from "../provider";
|
|
4
|
+
|
|
5
|
+
export interface VerifyEmailProps {
|
|
6
|
+
token: string;
|
|
7
|
+
onSuccess?: () => void;
|
|
8
|
+
onError?: (error: Error) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type VerifyState = "loading" | "success" | "error";
|
|
12
|
+
|
|
13
|
+
export const VerifyEmail: Component<VerifyEmailProps> = (props) => {
|
|
14
|
+
const { client } = useYAuth();
|
|
15
|
+
const [state, setState] = createSignal<VerifyState>("loading");
|
|
16
|
+
const [message, setMessage] = createSignal("");
|
|
17
|
+
const [errorMessage, setErrorMessage] = createSignal("");
|
|
18
|
+
|
|
19
|
+
createEffect(async () => {
|
|
20
|
+
try {
|
|
21
|
+
const result = await client.emailPassword.verify(props.token);
|
|
22
|
+
setMessage(result.message);
|
|
23
|
+
setState("success");
|
|
24
|
+
props.onSuccess?.();
|
|
25
|
+
} catch (err) {
|
|
26
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
27
|
+
setErrorMessage(error.message);
|
|
28
|
+
setState("error");
|
|
29
|
+
props.onError?.(error);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div class="space-y-4">
|
|
35
|
+
<Show when={state() === "loading"}>
|
|
36
|
+
<div class="text-sm text-muted-foreground">
|
|
37
|
+
Verifying your email address...
|
|
38
|
+
</div>
|
|
39
|
+
</Show>
|
|
40
|
+
|
|
41
|
+
<Show when={state() === "success"}>
|
|
42
|
+
<div class="rounded-md bg-emerald-500/10 px-3 py-2 text-sm text-emerald-600 dark:text-emerald-400">
|
|
43
|
+
{message()}
|
|
44
|
+
</div>
|
|
45
|
+
</Show>
|
|
46
|
+
|
|
47
|
+
<Show when={state() === "error"}>
|
|
48
|
+
<div class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
|
49
|
+
{errorMessage()}
|
|
50
|
+
</div>
|
|
51
|
+
</Show>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { ChangePasswordForm } from "./components/change-password-form";
|
|
2
|
+
export { ConsentScreen } from "./components/consent-screen";
|
|
3
|
+
export { ForgotPasswordForm } from "./components/forgot-password-form";
|
|
4
|
+
export { LoginForm } from "./components/login-form";
|
|
5
|
+
export { MagicLinkForm } from "./components/magic-link-form";
|
|
6
|
+
export { MfaChallenge } from "./components/mfa-challenge";
|
|
7
|
+
export { MfaSetup } from "./components/mfa-setup";
|
|
8
|
+
export { OAuthButtons } from "./components/oauth-buttons";
|
|
9
|
+
export { PasskeyButton } from "./components/passkey-button";
|
|
10
|
+
export { ProfileSettings } from "./components/profile-settings";
|
|
11
|
+
export { RegisterForm } from "./components/register-form";
|
|
12
|
+
export { ResetPasswordForm } from "./components/reset-password-form";
|
|
13
|
+
export { VerifyEmail } from "./components/verify-email";
|
|
14
|
+
export { useYAuth, YAuthProvider } from "./provider";
|