@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.
@@ -0,0 +1,83 @@
1
+ import type { AuthUser } from "@yackey-labs/yauth-shared";
2
+ import { type Component, createSignal } from "solid-js";
3
+ import { Show } from "solid-js/web";
4
+ import { useYAuth } from "../provider";
5
+
6
+ export interface MfaChallengeProps {
7
+ pendingSessionId: string;
8
+ onSuccess?: (user: AuthUser) => void;
9
+ onError?: (error: Error) => void;
10
+ }
11
+
12
+ export const MfaChallenge: Component<MfaChallengeProps> = (props) => {
13
+ const { client } = useYAuth();
14
+ const [code, setCode] = 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
+ await client.mfa.verify(
27
+ props.pendingSessionId,
28
+ (formData.get("code") as string) || code(),
29
+ );
30
+ const session = await client.getSession();
31
+ props.onSuccess?.(session.user);
32
+ } catch (err) {
33
+ const error = err instanceof Error ? err : new Error(String(err));
34
+ setError(error.message);
35
+ props.onError?.(error);
36
+ } finally {
37
+ setLoading(false);
38
+ }
39
+ };
40
+
41
+ return (
42
+ <form class="space-y-4" onSubmit={handleSubmit}>
43
+ <Show when={error()}>
44
+ <div class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
45
+ {error()}
46
+ </div>
47
+ </Show>
48
+
49
+ <p class="text-sm text-muted-foreground">
50
+ Enter the code from your authenticator app, or use a backup code.
51
+ </p>
52
+
53
+ <div class="space-y-2">
54
+ <label
55
+ class="text-sm font-medium leading-none"
56
+ for="yauth-mfa-challenge-code"
57
+ >
58
+ Verification code
59
+ </label>
60
+ <input
61
+ 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"
62
+ id="yauth-mfa-challenge-code"
63
+ name="code"
64
+ type="text"
65
+ inputmode="numeric"
66
+ autocomplete="one-time-code"
67
+ value={code()}
68
+ onInput={(e) => setCode(e.currentTarget.value)}
69
+ required
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() ? "Verifying..." : "Verify"}
80
+ </button>
81
+ </form>
82
+ );
83
+ };
@@ -0,0 +1,158 @@
1
+ import { type Component, createSignal, For } from "solid-js";
2
+ import { Show } from "solid-js/web";
3
+ import { useYAuth } from "../provider";
4
+
5
+ export interface MfaSetupProps {
6
+ onComplete?: (backupCodes: string[]) => void;
7
+ }
8
+
9
+ type SetupStep = "begin" | "confirm" | "done";
10
+
11
+ export const MfaSetup: Component<MfaSetupProps> = (props) => {
12
+ const { client } = useYAuth();
13
+ const [step, setStep] = createSignal<SetupStep>("begin");
14
+ const [uri, setUri] = createSignal("");
15
+ const [secret, setSecret] = createSignal("");
16
+ const [code, setCode] = createSignal("");
17
+ const [backupCodes, setBackupCodes] = createSignal<string[]>([]);
18
+ const [error, setError] = createSignal<string | null>(null);
19
+ const [loading, setLoading] = createSignal(false);
20
+
21
+ const handleBegin = async () => {
22
+ setError(null);
23
+ setLoading(true);
24
+
25
+ try {
26
+ const result = await client.mfa.setup();
27
+ setUri(result.otpauth_url);
28
+ setSecret(result.secret);
29
+ setBackupCodes(result.backup_codes);
30
+ setStep("confirm");
31
+ } catch (err) {
32
+ const error = err instanceof Error ? err : new Error(String(err));
33
+ setError(error.message);
34
+ } finally {
35
+ setLoading(false);
36
+ }
37
+ };
38
+
39
+ const handleConfirm = async (e: SubmitEvent) => {
40
+ e.preventDefault();
41
+ setError(null);
42
+ setLoading(true);
43
+
44
+ try {
45
+ const form = e.currentTarget as HTMLFormElement;
46
+ const formData = new FormData(form);
47
+ await client.mfa.confirm((formData.get("code") as string) || code());
48
+ setStep("done");
49
+ props.onComplete?.(backupCodes());
50
+ } catch (err) {
51
+ const error = err instanceof Error ? err : new Error(String(err));
52
+ setError(error.message);
53
+ } finally {
54
+ setLoading(false);
55
+ }
56
+ };
57
+
58
+ return (
59
+ <div class="space-y-4">
60
+ <Show when={error()}>
61
+ <div class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
62
+ {error()}
63
+ </div>
64
+ </Show>
65
+
66
+ <Show when={step() === "begin"}>
67
+ <div class="space-y-4">
68
+ <p class="text-sm text-muted-foreground">
69
+ Set up two-factor authentication to secure your account.
70
+ </p>
71
+ <button
72
+ 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"
73
+ type="button"
74
+ onClick={handleBegin}
75
+ disabled={loading()}
76
+ >
77
+ {loading() ? "Setting up..." : "Set up 2FA"}
78
+ </button>
79
+ </div>
80
+ </Show>
81
+
82
+ <Show when={step() === "confirm"}>
83
+ <div class="space-y-4">
84
+ <p class="text-sm text-muted-foreground">
85
+ Add this account to your authenticator app using the URI below, then
86
+ enter the verification code.
87
+ </p>
88
+
89
+ <div class="space-y-1">
90
+ <span class="text-sm font-medium leading-none">OTP Auth URI</span>
91
+ <code class="block w-full break-all rounded-md border border-input bg-muted px-3 py-2 text-xs">
92
+ {uri()}
93
+ </code>
94
+ </div>
95
+
96
+ <div class="space-y-1">
97
+ <span class="text-sm font-medium leading-none">
98
+ Manual entry key
99
+ </span>
100
+ <code class="block w-full break-all rounded-md border border-input bg-muted px-3 py-2 text-xs font-mono tracking-wider">
101
+ {secret()}
102
+ </code>
103
+ </div>
104
+
105
+ <form class="space-y-4" onSubmit={handleConfirm}>
106
+ <div class="space-y-2">
107
+ <label
108
+ class="text-sm font-medium leading-none"
109
+ for="yauth-mfa-setup-code"
110
+ >
111
+ Verification code
112
+ </label>
113
+ <input
114
+ 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"
115
+ id="yauth-mfa-setup-code"
116
+ name="code"
117
+ type="text"
118
+ inputmode="numeric"
119
+ autocomplete="one-time-code"
120
+ value={code()}
121
+ onInput={(e) => setCode(e.currentTarget.value)}
122
+ required
123
+ disabled={loading()}
124
+ />
125
+ </div>
126
+
127
+ <button
128
+ 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"
129
+ type="submit"
130
+ disabled={loading()}
131
+ >
132
+ {loading() ? "Verifying..." : "Verify and enable"}
133
+ </button>
134
+ </form>
135
+ </div>
136
+ </Show>
137
+
138
+ <Show when={step() === "done"}>
139
+ <div class="space-y-4">
140
+ <div class="rounded-md bg-emerald-500/10 px-3 py-2 text-sm text-emerald-600 dark:text-emerald-400">
141
+ Two-factor authentication has been enabled. Save these backup codes
142
+ in a safe place. Each code can only be used once.
143
+ </div>
144
+
145
+ <ul class="space-y-1">
146
+ <For each={backupCodes()}>
147
+ {(code) => (
148
+ <li class="rounded-md border border-input bg-muted px-3 py-1.5 text-center font-mono text-sm tracking-wider">
149
+ {code}
150
+ </li>
151
+ )}
152
+ </For>
153
+ </ul>
154
+ </div>
155
+ </Show>
156
+ </div>
157
+ );
158
+ };
@@ -0,0 +1,30 @@
1
+ import { type Component, For } from "solid-js";
2
+ import { useYAuth } from "../provider";
3
+
4
+ export interface OAuthButtonsProps {
5
+ providers: string[];
6
+ }
7
+
8
+ export const OAuthButtons: Component<OAuthButtonsProps> = (props) => {
9
+ const { client } = useYAuth();
10
+
11
+ const handleClick = (provider: string) => {
12
+ client.oauth.authorize(provider);
13
+ };
14
+
15
+ return (
16
+ <div class="space-y-2">
17
+ <For each={props.providers}>
18
+ {(provider) => (
19
+ <button
20
+ class="inline-flex h-9 w-full 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-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
21
+ type="button"
22
+ onClick={() => handleClick(provider)}
23
+ >
24
+ Sign in with {provider.charAt(0).toUpperCase() + provider.slice(1)}
25
+ </button>
26
+ )}
27
+ </For>
28
+ </div>
29
+ );
30
+ };
@@ -0,0 +1,98 @@
1
+ import {
2
+ startAuthentication,
3
+ startRegistration,
4
+ } from "@simplewebauthn/browser";
5
+ import type { AuthUser } from "@yackey-labs/yauth-shared";
6
+ import { type Component, createSignal } from "solid-js";
7
+ import { Show } from "solid-js/web";
8
+ import { useYAuth } from "../provider";
9
+
10
+ export interface PasskeyButtonProps {
11
+ mode: "login" | "register";
12
+ email?: string;
13
+ onSuccess?: (user: AuthUser) => void;
14
+ onError?: (error: Error) => void;
15
+ }
16
+
17
+ export const PasskeyButton: Component<PasskeyButtonProps> = (props) => {
18
+ const { client } = useYAuth();
19
+ const [error, setError] = createSignal<string | null>(null);
20
+ const [loading, setLoading] = createSignal(false);
21
+
22
+ const handleLogin = async () => {
23
+ const beginResult = await client.passkey.loginBegin(
24
+ props.email || undefined,
25
+ );
26
+ const rcr = beginResult.options as { publicKey: unknown };
27
+ const credential = await startAuthentication({
28
+ optionsJSON: rcr.publicKey as Parameters<
29
+ typeof startAuthentication
30
+ >[0]["optionsJSON"],
31
+ });
32
+ await client.passkey.loginFinish(beginResult.challenge_id, credential);
33
+ const session = await client.getSession();
34
+ props.onSuccess?.(session.user);
35
+ };
36
+
37
+ const handleRegister = async () => {
38
+ const ccr = (await client.passkey.registerBegin()) as {
39
+ publicKey: unknown;
40
+ };
41
+ const credential = await startRegistration({
42
+ optionsJSON: ccr.publicKey as Parameters<
43
+ typeof startRegistration
44
+ >[0]["optionsJSON"],
45
+ });
46
+ await client.passkey.registerFinish(credential, "Passkey");
47
+ props.onSuccess?.(undefined as unknown as AuthUser);
48
+ };
49
+
50
+ const handleClick = async () => {
51
+ setError(null);
52
+ setLoading(true);
53
+
54
+ try {
55
+ if (props.mode === "login") {
56
+ await handleLogin();
57
+ } else {
58
+ await handleRegister();
59
+ }
60
+ } catch (err) {
61
+ const error = err instanceof Error ? err : new Error(String(err));
62
+ console.error("[yauth] Passkey error:", error);
63
+ const message =
64
+ error.name === "NotAllowedError"
65
+ ? "Passkey authentication was cancelled or not available on this device."
66
+ : error.message;
67
+ setError(message);
68
+ props.onError?.(error);
69
+ } finally {
70
+ setLoading(false);
71
+ }
72
+ };
73
+
74
+ return (
75
+ <div class="space-y-2">
76
+ <Show when={error()}>
77
+ <div class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
78
+ {error()}
79
+ </div>
80
+ </Show>
81
+
82
+ <button
83
+ class="inline-flex h-9 w-full 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-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
84
+ type="button"
85
+ onClick={handleClick}
86
+ disabled={loading()}
87
+ >
88
+ {loading()
89
+ ? props.mode === "login"
90
+ ? "Authenticating..."
91
+ : "Registering..."
92
+ : props.mode === "login"
93
+ ? "Sign in with passkey"
94
+ : "Register passkey"}
95
+ </button>
96
+ </div>
97
+ );
98
+ };