@revealui/auth 0.0.0-canary-20260409021642
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/LICENSE +22 -0
- package/README.md +115 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/react/index.d.ts +19 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +12 -0
- package/dist/react/useMFA.d.ts +83 -0
- package/dist/react/useMFA.d.ts.map +1 -0
- package/dist/react/useMFA.js +182 -0
- package/dist/react/usePasskey.d.ts +88 -0
- package/dist/react/usePasskey.d.ts.map +1 -0
- package/dist/react/usePasskey.js +203 -0
- package/dist/react/useSession.d.ts +32 -0
- package/dist/react/useSession.d.ts.map +1 -0
- package/dist/react/useSession.js +109 -0
- package/dist/react/useSignIn.d.ts +47 -0
- package/dist/react/useSignIn.d.ts.map +1 -0
- package/dist/react/useSignIn.js +121 -0
- package/dist/react/useSignOut.d.ts +30 -0
- package/dist/react/useSignOut.d.ts.map +1 -0
- package/dist/react/useSignOut.js +57 -0
- package/dist/react/useSignUp.d.ts +43 -0
- package/dist/react/useSignUp.d.ts.map +1 -0
- package/dist/react/useSignUp.js +106 -0
- package/dist/server/audit-bridge.d.ts +69 -0
- package/dist/server/audit-bridge.d.ts.map +1 -0
- package/dist/server/audit-bridge.js +169 -0
- package/dist/server/auth.d.ts +46 -0
- package/dist/server/auth.d.ts.map +1 -0
- package/dist/server/auth.js +393 -0
- package/dist/server/brute-force.d.ts +53 -0
- package/dist/server/brute-force.d.ts.map +1 -0
- package/dist/server/brute-force.js +163 -0
- package/dist/server/errors.d.ts +28 -0
- package/dist/server/errors.d.ts.map +1 -0
- package/dist/server/errors.js +51 -0
- package/dist/server/index.d.ts +30 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +23 -0
- package/dist/server/magic-link.d.ts +52 -0
- package/dist/server/magic-link.d.ts.map +1 -0
- package/dist/server/magic-link.js +111 -0
- package/dist/server/mfa-enforcement.d.ts +76 -0
- package/dist/server/mfa-enforcement.d.ts.map +1 -0
- package/dist/server/mfa-enforcement.js +76 -0
- package/dist/server/mfa.d.ts +90 -0
- package/dist/server/mfa.d.ts.map +1 -0
- package/dist/server/mfa.js +304 -0
- package/dist/server/oauth.d.ts +97 -0
- package/dist/server/oauth.d.ts.map +1 -0
- package/dist/server/oauth.js +370 -0
- package/dist/server/passkey.d.ts +132 -0
- package/dist/server/passkey.d.ts.map +1 -0
- package/dist/server/passkey.js +269 -0
- package/dist/server/password-reset.d.ts +72 -0
- package/dist/server/password-reset.d.ts.map +1 -0
- package/dist/server/password-reset.js +289 -0
- package/dist/server/password-validation.d.ts +25 -0
- package/dist/server/password-validation.d.ts.map +1 -0
- package/dist/server/password-validation.js +52 -0
- package/dist/server/providers/github.d.ts +14 -0
- package/dist/server/providers/github.d.ts.map +1 -0
- package/dist/server/providers/github.js +86 -0
- package/dist/server/providers/google.d.ts +11 -0
- package/dist/server/providers/google.d.ts.map +1 -0
- package/dist/server/providers/google.js +68 -0
- package/dist/server/providers/vercel.d.ts +11 -0
- package/dist/server/providers/vercel.d.ts.map +1 -0
- package/dist/server/providers/vercel.js +68 -0
- package/dist/server/rate-limit.d.ts +54 -0
- package/dist/server/rate-limit.d.ts.map +1 -0
- package/dist/server/rate-limit.js +148 -0
- package/dist/server/session.d.ts +111 -0
- package/dist/server/session.d.ts.map +1 -0
- package/dist/server/session.js +416 -0
- package/dist/server/signed-cookie.d.ts +32 -0
- package/dist/server/signed-cookie.d.ts.map +1 -0
- package/dist/server/signed-cookie.js +67 -0
- package/dist/server/storage/database.d.ts +26 -0
- package/dist/server/storage/database.d.ts.map +1 -0
- package/dist/server/storage/database.js +110 -0
- package/dist/server/storage/in-memory.d.ts +28 -0
- package/dist/server/storage/in-memory.d.ts.map +1 -0
- package/dist/server/storage/in-memory.js +80 -0
- package/dist/server/storage/index.d.ts +34 -0
- package/dist/server/storage/index.d.ts.map +1 -0
- package/dist/server/storage/index.js +94 -0
- package/dist/server/storage/interface.d.ts +46 -0
- package/dist/server/storage/interface.d.ts.map +1 -0
- package/dist/server/storage/interface.js +6 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/utils/database.d.ts +53 -0
- package/dist/utils/database.d.ts.map +1 -0
- package/dist/utils/database.js +151 -0
- package/dist/utils/token.d.ts +30 -0
- package/dist/utils/token.d.ts.map +1 -0
- package/dist/utils/token.js +39 -0
- package/package.json +85 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passkey Hooks
|
|
3
|
+
*
|
|
4
|
+
* React hooks for WebAuthn passkey registration and authentication.
|
|
5
|
+
* Uses dynamic imports for @simplewebauthn/browser to avoid SSR issues.
|
|
6
|
+
*/
|
|
7
|
+
'use client';
|
|
8
|
+
import { useEffect, useState } from 'react';
|
|
9
|
+
/**
|
|
10
|
+
* Hook for passkey registration (sign-up and security settings).
|
|
11
|
+
*
|
|
12
|
+
* @returns Register function, loading state, error, and browser support flag
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* function AddPasskey() {
|
|
17
|
+
* const { register, isLoading, error, supported } = usePasskeyRegister();
|
|
18
|
+
*
|
|
19
|
+
* if (!supported) return <p>Passkeys are not supported in this browser.</p>;
|
|
20
|
+
*
|
|
21
|
+
* const handleAdd = async () => {
|
|
22
|
+
* const result = await register({ deviceName: 'My Laptop' });
|
|
23
|
+
* if (result) {
|
|
24
|
+
* // Passkey registered successfully
|
|
25
|
+
* }
|
|
26
|
+
* };
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function usePasskeyRegister() {
|
|
31
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
32
|
+
const [error, setError] = useState(null);
|
|
33
|
+
const [supported, setSupported] = useState(false);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
setSupported(!!window.PublicKeyCredential);
|
|
36
|
+
}, []);
|
|
37
|
+
const register = async (options) => {
|
|
38
|
+
if (!supported) {
|
|
39
|
+
setError('Passkeys are not supported in this browser');
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
setIsLoading(true);
|
|
44
|
+
setError(null);
|
|
45
|
+
// Step 1: Get registration options from the server
|
|
46
|
+
const optionsResponse = await fetch('/api/auth/passkey/register-options', {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'Content-Type': 'application/json' },
|
|
49
|
+
credentials: 'include',
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
email: options?.email,
|
|
52
|
+
name: options?.name,
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
const optionsJson = await optionsResponse.json();
|
|
56
|
+
if (!optionsResponse.ok) {
|
|
57
|
+
const errorData = optionsJson;
|
|
58
|
+
setError(errorData.error ?? 'Failed to get registration options');
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
// Step 2: Start browser WebAuthn registration ceremony
|
|
62
|
+
// Server wraps options: { options: { challenge, ... } }
|
|
63
|
+
// @simplewebauthn/browser v13+ expects { optionsJSON: ... }
|
|
64
|
+
const { startRegistration } = await import('@simplewebauthn/browser');
|
|
65
|
+
const optionsData = optionsJson;
|
|
66
|
+
const attestationResponse = await startRegistration({ optionsJSON: optionsData.options });
|
|
67
|
+
// Step 3: Verify with the server
|
|
68
|
+
const verifyResponse = await fetch('/api/auth/passkey/register-verify', {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: { 'Content-Type': 'application/json' },
|
|
71
|
+
credentials: 'include',
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
attestationResponse,
|
|
74
|
+
deviceName: options?.deviceName,
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
const verifyJson = await verifyResponse.json();
|
|
78
|
+
if (!verifyResponse.ok) {
|
|
79
|
+
const errorData = verifyJson;
|
|
80
|
+
setError(errorData.error ?? 'Failed to verify passkey registration');
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const result = verifyJson;
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
// Handle user cancellation of the WebAuthn ceremony
|
|
88
|
+
if (err instanceof DOMException && err.name === 'NotAllowedError') {
|
|
89
|
+
setError('Passkey registration was cancelled');
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
93
|
+
setError(message);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
setIsLoading(false);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
register,
|
|
102
|
+
isLoading,
|
|
103
|
+
error,
|
|
104
|
+
supported,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Hook for passkey authentication (login page).
|
|
109
|
+
*
|
|
110
|
+
* @returns Sign-in function, loading state, error, and browser support flag
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```tsx
|
|
114
|
+
* function LoginPage() {
|
|
115
|
+
* const { signIn, isLoading, error, supported } = usePasskeySignIn();
|
|
116
|
+
*
|
|
117
|
+
* const handlePasskeyLogin = async () => {
|
|
118
|
+
* const success = await signIn();
|
|
119
|
+
* if (success) {
|
|
120
|
+
* router.push('/admin');
|
|
121
|
+
* }
|
|
122
|
+
* };
|
|
123
|
+
*
|
|
124
|
+
* return (
|
|
125
|
+
* <>
|
|
126
|
+
* {supported && (
|
|
127
|
+
* <button onClick={handlePasskeyLogin} disabled={isLoading}>
|
|
128
|
+
* Sign in with Passkey
|
|
129
|
+
* </button>
|
|
130
|
+
* )}
|
|
131
|
+
* {error && <p>{error}</p>}
|
|
132
|
+
* </>
|
|
133
|
+
* );
|
|
134
|
+
* }
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export function usePasskeySignIn() {
|
|
138
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
139
|
+
const [error, setError] = useState(null);
|
|
140
|
+
const [supported, setSupported] = useState(false);
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
setSupported(!!window.PublicKeyCredential);
|
|
143
|
+
}, []);
|
|
144
|
+
const signIn = async () => {
|
|
145
|
+
if (!supported) {
|
|
146
|
+
setError('Passkeys are not supported in this browser');
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
setIsLoading(true);
|
|
151
|
+
setError(null);
|
|
152
|
+
// Step 1: Get authentication options from the server
|
|
153
|
+
const optionsResponse = await fetch('/api/auth/passkey/authenticate-options', {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
credentials: 'include',
|
|
156
|
+
});
|
|
157
|
+
const optionsJson = await optionsResponse.json();
|
|
158
|
+
if (!optionsResponse.ok) {
|
|
159
|
+
const errorData = optionsJson;
|
|
160
|
+
setError(errorData.error ?? 'Failed to get authentication options');
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
// Step 2: Start browser WebAuthn authentication ceremony
|
|
164
|
+
// @simplewebauthn/browser v13+ expects { optionsJSON: ... }
|
|
165
|
+
const { startAuthentication } = await import('@simplewebauthn/browser');
|
|
166
|
+
const authOptionsData = optionsJson;
|
|
167
|
+
const assertionResponse = await startAuthentication({ optionsJSON: authOptionsData.options });
|
|
168
|
+
// Step 3: Verify with the server
|
|
169
|
+
const verifyResponse = await fetch('/api/auth/passkey/authenticate-verify', {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: { 'Content-Type': 'application/json' },
|
|
172
|
+
credentials: 'include',
|
|
173
|
+
body: JSON.stringify({ authenticationResponse: assertionResponse }),
|
|
174
|
+
});
|
|
175
|
+
const verifyJson = await verifyResponse.json();
|
|
176
|
+
if (!verifyResponse.ok) {
|
|
177
|
+
const errorData = verifyJson;
|
|
178
|
+
setError(errorData.error ?? 'Passkey authentication failed');
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
// Handle user cancellation of the WebAuthn ceremony
|
|
185
|
+
if (err instanceof DOMException && err.name === 'NotAllowedError') {
|
|
186
|
+
setError('Passkey authentication was cancelled');
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
190
|
+
setError(message);
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
setIsLoading(false);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
return {
|
|
198
|
+
signIn,
|
|
199
|
+
isLoading,
|
|
200
|
+
error,
|
|
201
|
+
supported,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSession Hook
|
|
3
|
+
*
|
|
4
|
+
* React hook for accessing the current session.
|
|
5
|
+
* Inspired by Better Auth and TanStack Start patterns.
|
|
6
|
+
*/
|
|
7
|
+
import type { AuthSession } from '../types.js';
|
|
8
|
+
export interface UseSessionResult {
|
|
9
|
+
data: AuthSession | null;
|
|
10
|
+
isLoading: boolean;
|
|
11
|
+
error: Error | null;
|
|
12
|
+
refetch: () => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Hook to get the current session
|
|
16
|
+
*
|
|
17
|
+
* @returns Session data, loading state, error, and refetch function
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* function MyComponent() {
|
|
22
|
+
* const { data: session, isLoading } = useSession()
|
|
23
|
+
*
|
|
24
|
+
* if (isLoading) return <div>Loading...</div>
|
|
25
|
+
* if (!session) return <div>Not signed in</div>
|
|
26
|
+
*
|
|
27
|
+
* return <div>Hello, {session.user.name}</div>
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function useSession(): UseSessionResult;
|
|
32
|
+
//# sourceMappingURL=useSession.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSession.d.ts","sourceRoot":"","sources":["../../src/react/useSession.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAiC/C,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,UAAU,IAAI,gBAAgB,CA2D7C"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSession Hook
|
|
3
|
+
*
|
|
4
|
+
* React hook for accessing the current session.
|
|
5
|
+
* Inspired by Better Auth and TanStack Start patterns.
|
|
6
|
+
*/
|
|
7
|
+
'use client';
|
|
8
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
9
|
+
import { z } from 'zod/v4';
|
|
10
|
+
// Zod validates the required fields; .passthrough() preserves the rest.
|
|
11
|
+
// The API returns JSON-serialized session/user objects (Dates as ISO strings).
|
|
12
|
+
// z.infer output has an index signature incompatible with the concrete
|
|
13
|
+
// AuthSession interface, so we convert via the helper below.
|
|
14
|
+
const AuthSessionSchema = z.object({
|
|
15
|
+
session: z
|
|
16
|
+
.object({
|
|
17
|
+
id: z.string(),
|
|
18
|
+
expiresAt: z.string(),
|
|
19
|
+
})
|
|
20
|
+
.passthrough(),
|
|
21
|
+
user: z
|
|
22
|
+
.object({
|
|
23
|
+
id: z.string(),
|
|
24
|
+
email: z.string(),
|
|
25
|
+
name: z.string().nullable().optional(),
|
|
26
|
+
})
|
|
27
|
+
.passthrough(),
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* Narrow Zod-validated API response data to AuthSession.
|
|
31
|
+
*
|
|
32
|
+
* The cast is safe because: (1) Zod verified the required fields on both
|
|
33
|
+
* session and user, (2) .passthrough() preserves all other properties, and
|
|
34
|
+
* (3) the API serializes full Session + User rows (Dates become ISO strings).
|
|
35
|
+
*/
|
|
36
|
+
function toAuthSession(validated) {
|
|
37
|
+
return validated;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Hook to get the current session
|
|
41
|
+
*
|
|
42
|
+
* @returns Session data, loading state, error, and refetch function
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```tsx
|
|
46
|
+
* function MyComponent() {
|
|
47
|
+
* const { data: session, isLoading } = useSession()
|
|
48
|
+
*
|
|
49
|
+
* if (isLoading) return <div>Loading...</div>
|
|
50
|
+
* if (!session) return <div>Not signed in</div>
|
|
51
|
+
*
|
|
52
|
+
* return <div>Hello, {session.user.name}</div>
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export function useSession() {
|
|
57
|
+
const [data, setData] = useState(null);
|
|
58
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
59
|
+
const [error, setError] = useState(null);
|
|
60
|
+
const abortControllerRef = useRef(null);
|
|
61
|
+
const fetchSession = useCallback(async () => {
|
|
62
|
+
// Abort any in-flight request
|
|
63
|
+
abortControllerRef.current?.abort();
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
abortControllerRef.current = controller;
|
|
66
|
+
try {
|
|
67
|
+
setIsLoading(true);
|
|
68
|
+
setError(null);
|
|
69
|
+
const response = await fetch('/api/auth/session', {
|
|
70
|
+
credentials: 'include',
|
|
71
|
+
signal: controller.signal,
|
|
72
|
+
});
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
if (response.status === 401) {
|
|
75
|
+
setData(null);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`Failed to fetch session: ${response.statusText}`);
|
|
79
|
+
}
|
|
80
|
+
const json = await response.json();
|
|
81
|
+
const validated = AuthSessionSchema.parse(json);
|
|
82
|
+
setData(toAuthSession(validated));
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
if (err instanceof DOMException && err.name === 'AbortError') {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
89
|
+
setError(error);
|
|
90
|
+
setData(null);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
setIsLoading(false);
|
|
94
|
+
}
|
|
95
|
+
}, []);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
void fetchSession();
|
|
98
|
+
return () => {
|
|
99
|
+
// Clean up: abort in-flight request on unmount
|
|
100
|
+
abortControllerRef.current?.abort();
|
|
101
|
+
};
|
|
102
|
+
}, [fetchSession]);
|
|
103
|
+
return {
|
|
104
|
+
data,
|
|
105
|
+
isLoading,
|
|
106
|
+
error,
|
|
107
|
+
refetch: fetchSession,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSignIn Hook
|
|
3
|
+
*
|
|
4
|
+
* React hook for signing in a user.
|
|
5
|
+
*/
|
|
6
|
+
import type { User } from '../types.js';
|
|
7
|
+
export interface SignInInput {
|
|
8
|
+
email: string;
|
|
9
|
+
password: string;
|
|
10
|
+
}
|
|
11
|
+
export interface UseSignInResult {
|
|
12
|
+
signIn: (input: SignInInput) => Promise<{
|
|
13
|
+
success: true;
|
|
14
|
+
user: User;
|
|
15
|
+
} | {
|
|
16
|
+
success: false;
|
|
17
|
+
error: string;
|
|
18
|
+
} | {
|
|
19
|
+
success: false;
|
|
20
|
+
requiresMfa: true;
|
|
21
|
+
mfaUserId: string;
|
|
22
|
+
}>;
|
|
23
|
+
isLoading: boolean;
|
|
24
|
+
error: Error | null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Hook to sign in a user
|
|
28
|
+
*
|
|
29
|
+
* @returns Sign in function, loading state, and error
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* function SignInForm() {
|
|
34
|
+
* const { signIn, isLoading } = useSignIn()
|
|
35
|
+
*
|
|
36
|
+
* const handleSubmit = async (e: FormEvent) => {
|
|
37
|
+
* e.preventDefault()
|
|
38
|
+
* const result = await signIn({ email, password })
|
|
39
|
+
* if (result.success) {
|
|
40
|
+
* router.push('/dashboard')
|
|
41
|
+
* }
|
|
42
|
+
* }
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare function useSignIn(): UseSignInResult;
|
|
47
|
+
//# sourceMappingURL=useSignIn.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSignIn.d.ts","sourceRoot":"","sources":["../../src/react/useSignIn.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAkCxC,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,CACN,KAAK,EAAE,WAAW,KACf,OAAO,CACR;QAAE,OAAO,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,IAAI,CAAA;KAAE,GAC7B;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GACjC;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,WAAW,EAAE,IAAI,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAC3D,CAAC;IACF,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,SAAS,IAAI,eAAe,CAsE3C"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSignIn Hook
|
|
3
|
+
*
|
|
4
|
+
* React hook for signing in a user.
|
|
5
|
+
*/
|
|
6
|
+
'use client';
|
|
7
|
+
import { useCallback, useRef, useState } from 'react';
|
|
8
|
+
import { z } from 'zod/v4';
|
|
9
|
+
// Zod validates the required fields; .passthrough() preserves the rest.
|
|
10
|
+
// The API returns JSON-serialized User objects (Dates as ISO strings).
|
|
11
|
+
// z.infer output has an index signature incompatible with the concrete User
|
|
12
|
+
// interface, so we extract the validated user via the helper below.
|
|
13
|
+
const SignInUserSchema = z
|
|
14
|
+
.object({
|
|
15
|
+
id: z.string(),
|
|
16
|
+
email: z.string(),
|
|
17
|
+
name: z.string().nullable().optional(),
|
|
18
|
+
})
|
|
19
|
+
.passthrough();
|
|
20
|
+
/**
|
|
21
|
+
* Narrow Zod-validated API response data to User.
|
|
22
|
+
*
|
|
23
|
+
* The cast is safe because: (1) Zod verified the required fields (id, email),
|
|
24
|
+
* (2) .passthrough() preserves all other properties from the API response,
|
|
25
|
+
* and (3) the API serializes a full User row (Dates become ISO strings in JSON).
|
|
26
|
+
*/
|
|
27
|
+
function toUser(validated) {
|
|
28
|
+
return validated;
|
|
29
|
+
}
|
|
30
|
+
// Validation schemas for sign-in response
|
|
31
|
+
const SignInErrorResponseSchema = z.object({
|
|
32
|
+
error: z.string().optional(),
|
|
33
|
+
});
|
|
34
|
+
const SignInSuccessResponseSchema = z.object({
|
|
35
|
+
user: SignInUserSchema,
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* Hook to sign in a user
|
|
39
|
+
*
|
|
40
|
+
* @returns Sign in function, loading state, and error
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```tsx
|
|
44
|
+
* function SignInForm() {
|
|
45
|
+
* const { signIn, isLoading } = useSignIn()
|
|
46
|
+
*
|
|
47
|
+
* const handleSubmit = async (e: FormEvent) => {
|
|
48
|
+
* e.preventDefault()
|
|
49
|
+
* const result = await signIn({ email, password })
|
|
50
|
+
* if (result.success) {
|
|
51
|
+
* router.push('/dashboard')
|
|
52
|
+
* }
|
|
53
|
+
* }
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function useSignIn() {
|
|
58
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
59
|
+
const [error, setError] = useState(null);
|
|
60
|
+
const abortControllerRef = useRef(null);
|
|
61
|
+
const signIn = useCallback(async (input) => {
|
|
62
|
+
// Abort any in-flight request
|
|
63
|
+
abortControllerRef.current?.abort();
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
abortControllerRef.current = controller;
|
|
66
|
+
try {
|
|
67
|
+
setIsLoading(true);
|
|
68
|
+
setError(null);
|
|
69
|
+
const response = await fetch('/api/auth/sign-in', {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: {
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
},
|
|
74
|
+
credentials: 'include',
|
|
75
|
+
body: JSON.stringify(input),
|
|
76
|
+
signal: controller.signal,
|
|
77
|
+
});
|
|
78
|
+
const json = await response.json();
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
const errorData = SignInErrorResponseSchema.parse(json);
|
|
81
|
+
return {
|
|
82
|
+
success: false,
|
|
83
|
+
error: errorData.error || 'Failed to sign in',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// Check for MFA challenge before parsing as full success
|
|
87
|
+
const jsonObj = json;
|
|
88
|
+
if (jsonObj.requiresMfa === true && typeof jsonObj.mfaUserId === 'string') {
|
|
89
|
+
return {
|
|
90
|
+
success: false,
|
|
91
|
+
requiresMfa: true,
|
|
92
|
+
mfaUserId: jsonObj.mfaUserId,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const successData = SignInSuccessResponseSchema.parse(json);
|
|
96
|
+
return {
|
|
97
|
+
success: true,
|
|
98
|
+
user: toUser(successData.user),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
if (err instanceof DOMException && err.name === 'AbortError') {
|
|
103
|
+
return { success: false, error: 'Request was cancelled' };
|
|
104
|
+
}
|
|
105
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
106
|
+
setError(error);
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
error: error.message,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
setIsLoading(false);
|
|
114
|
+
}
|
|
115
|
+
}, []);
|
|
116
|
+
return {
|
|
117
|
+
signIn,
|
|
118
|
+
isLoading,
|
|
119
|
+
error,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSignOut Hook
|
|
3
|
+
*
|
|
4
|
+
* React hook for signing out a user.
|
|
5
|
+
*/
|
|
6
|
+
export interface UseSignOutResult {
|
|
7
|
+
signOut: () => Promise<void>;
|
|
8
|
+
isLoading: boolean;
|
|
9
|
+
error: Error | null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Hook to sign out a user
|
|
13
|
+
*
|
|
14
|
+
* @returns Sign out function, loading state, and error
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* function SignOutButton() {
|
|
19
|
+
* const { signOut, isLoading } = useSignOut()
|
|
20
|
+
*
|
|
21
|
+
* return (
|
|
22
|
+
* <button onClick={signOut} disabled={isLoading}>
|
|
23
|
+
* {isLoading ? 'Signing out...' : 'Sign Out'}
|
|
24
|
+
* </button>
|
|
25
|
+
* )
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare function useSignOut(): UseSignOutResult;
|
|
30
|
+
//# sourceMappingURL=useSignOut.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSignOut.d.ts","sourceRoot":"","sources":["../../src/react/useSignOut.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,IAAI,gBAAgB,CAkC7C"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSignOut Hook
|
|
3
|
+
*
|
|
4
|
+
* React hook for signing out a user.
|
|
5
|
+
*/
|
|
6
|
+
'use client';
|
|
7
|
+
import { useState } from 'react';
|
|
8
|
+
/**
|
|
9
|
+
* Hook to sign out a user
|
|
10
|
+
*
|
|
11
|
+
* @returns Sign out function, loading state, and error
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* function SignOutButton() {
|
|
16
|
+
* const { signOut, isLoading } = useSignOut()
|
|
17
|
+
*
|
|
18
|
+
* return (
|
|
19
|
+
* <button onClick={signOut} disabled={isLoading}>
|
|
20
|
+
* {isLoading ? 'Signing out...' : 'Sign Out'}
|
|
21
|
+
* </button>
|
|
22
|
+
* )
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function useSignOut() {
|
|
27
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
28
|
+
const [error, setError] = useState(null);
|
|
29
|
+
const signOut = async () => {
|
|
30
|
+
try {
|
|
31
|
+
setIsLoading(true);
|
|
32
|
+
setError(null);
|
|
33
|
+
const response = await fetch('/api/auth/sign-out', {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
credentials: 'include',
|
|
36
|
+
});
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw new Error('Failed to sign out');
|
|
39
|
+
}
|
|
40
|
+
// Clear session on client side
|
|
41
|
+
window.location.href = '/login';
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
45
|
+
setError(error);
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
setIsLoading(false);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
signOut,
|
|
54
|
+
isLoading,
|
|
55
|
+
error,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSignUp Hook
|
|
3
|
+
*
|
|
4
|
+
* React hook for signing up a new user.
|
|
5
|
+
*/
|
|
6
|
+
import type { User } from '../types.js';
|
|
7
|
+
export interface SignUpInput {
|
|
8
|
+
email: string;
|
|
9
|
+
password: string;
|
|
10
|
+
name: string;
|
|
11
|
+
tosAccepted: true;
|
|
12
|
+
}
|
|
13
|
+
export interface UseSignUpResult {
|
|
14
|
+
signUp: (input: SignUpInput) => Promise<{
|
|
15
|
+
success: boolean;
|
|
16
|
+
user?: User;
|
|
17
|
+
error?: string;
|
|
18
|
+
}>;
|
|
19
|
+
isLoading: boolean;
|
|
20
|
+
error: Error | null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Hook to sign up a new user
|
|
24
|
+
*
|
|
25
|
+
* @returns Sign up function, loading state, and error
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```tsx
|
|
29
|
+
* function SignUpForm() {
|
|
30
|
+
* const { signUp, isLoading } = useSignUp()
|
|
31
|
+
*
|
|
32
|
+
* const handleSubmit = async (e: FormEvent) => {
|
|
33
|
+
* e.preventDefault()
|
|
34
|
+
* const result = await signUp({ email, password, name })
|
|
35
|
+
* if (result.success) {
|
|
36
|
+
* router.push('/dashboard')
|
|
37
|
+
* }
|
|
38
|
+
* }
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare function useSignUp(): UseSignUpResult;
|
|
43
|
+
//# sourceMappingURL=useSignUp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSignUp.d.ts","sourceRoot":"","sources":["../../src/react/useSignUp.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAmCxC,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,IAAI,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3F,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,SAAS,IAAI,eAAe,CAoD3C"}
|