@revealui/auth 0.2.0 → 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/README.md +58 -34
- package/dist/index.d.ts.map +1 -1
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +2 -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.map +1 -1
- package/dist/react/useSession.js +16 -5
- package/dist/react/useSignIn.d.ts +9 -3
- package/dist/react/useSignIn.d.ts.map +1 -1
- package/dist/react/useSignIn.js +32 -10
- package/dist/react/useSignOut.d.ts.map +1 -1
- package/dist/react/useSignUp.d.ts +1 -0
- package/dist/react/useSignUp.d.ts.map +1 -1
- package/dist/react/useSignUp.js +25 -9
- package/dist/server/auth.d.ts +2 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +93 -5
- package/dist/server/brute-force.d.ts +10 -1
- package/dist/server/brute-force.d.ts.map +1 -1
- package/dist/server/brute-force.js +46 -23
- package/dist/server/errors.d.ts +4 -0
- package/dist/server/errors.d.ts.map +1 -1
- package/dist/server/errors.js +8 -0
- package/dist/server/index.d.ts +17 -6
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +12 -5
- 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.d.ts +87 -0
- package/dist/server/mfa.d.ts.map +1 -0
- package/dist/server/mfa.js +263 -0
- package/dist/server/oauth.d.ts +86 -0
- package/dist/server/oauth.d.ts.map +1 -0
- package/dist/server/oauth.js +355 -0
- package/dist/server/passkey.d.ts +132 -0
- package/dist/server/passkey.d.ts.map +1 -0
- package/dist/server/passkey.js +257 -0
- package/dist/server/password-reset.d.ts +32 -6
- package/dist/server/password-reset.d.ts.map +1 -1
- package/dist/server/password-reset.js +116 -47
- package/dist/server/password-validation.d.ts.map +1 -1
- 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 +89 -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 +69 -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 +63 -0
- package/dist/server/rate-limit.d.ts +10 -1
- package/dist/server/rate-limit.d.ts.map +1 -1
- package/dist/server/rate-limit.js +61 -43
- package/dist/server/session.d.ts +48 -1
- package/dist/server/session.d.ts.map +1 -1
- package/dist/server/session.js +126 -7
- 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 +10 -1
- package/dist/server/storage/database.d.ts.map +1 -1
- package/dist/server/storage/database.js +43 -5
- package/dist/server/storage/in-memory.d.ts +4 -0
- package/dist/server/storage/in-memory.d.ts.map +1 -1
- package/dist/server/storage/in-memory.js +16 -6
- package/dist/server/storage/index.d.ts +11 -3
- package/dist/server/storage/index.d.ts.map +1 -1
- package/dist/server/storage/index.js +18 -4
- package/dist/server/storage/interface.d.ts +11 -1
- package/dist/server/storage/interface.d.ts.map +1 -1
- package/dist/server/storage/interface.js +1 -1
- package/dist/types.d.ts +23 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -2
- package/dist/utils/database.d.ts.map +1 -1
- package/dist/utils/database.js +12 -2
- package/dist/utils/token.d.ts +9 -1
- package/dist/utils/token.d.ts.map +1 -1
- package/dist/utils/token.js +9 -1
- package/package.json +26 -8
|
@@ -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
|
+
}
|
|
@@ -1 +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,
|
|
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"}
|
package/dist/react/useSession.js
CHANGED
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
'use client';
|
|
8
8
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
9
9
|
import { z } from 'zod/v4';
|
|
10
|
-
//
|
|
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.
|
|
11
14
|
const AuthSessionSchema = z.object({
|
|
12
15
|
session: z
|
|
13
16
|
.object({
|
|
@@ -21,8 +24,18 @@ const AuthSessionSchema = z.object({
|
|
|
21
24
|
email: z.string(),
|
|
22
25
|
name: z.string().nullable().optional(),
|
|
23
26
|
})
|
|
24
|
-
.passthrough(),
|
|
27
|
+
.passthrough(),
|
|
25
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
|
+
}
|
|
26
39
|
/**
|
|
27
40
|
* Hook to get the current session
|
|
28
41
|
*
|
|
@@ -66,9 +79,7 @@ export function useSession() {
|
|
|
66
79
|
}
|
|
67
80
|
const json = await response.json();
|
|
68
81
|
const validated = AuthSessionSchema.parse(json);
|
|
69
|
-
|
|
70
|
-
// The API returns serialized data (Dates as strings), so we cast to expected type
|
|
71
|
-
setData(validated);
|
|
82
|
+
setData(toAuthSession(validated));
|
|
72
83
|
}
|
|
73
84
|
catch (err) {
|
|
74
85
|
if (err instanceof DOMException && err.name === 'AbortError') {
|
|
@@ -10,9 +10,15 @@ export interface SignInInput {
|
|
|
10
10
|
}
|
|
11
11
|
export interface UseSignInResult {
|
|
12
12
|
signIn: (input: SignInInput) => Promise<{
|
|
13
|
-
success:
|
|
14
|
-
user
|
|
15
|
-
|
|
13
|
+
success: true;
|
|
14
|
+
user: User;
|
|
15
|
+
} | {
|
|
16
|
+
success: false;
|
|
17
|
+
error: string;
|
|
18
|
+
} | {
|
|
19
|
+
success: false;
|
|
20
|
+
requiresMfa: true;
|
|
21
|
+
mfaUserId: string;
|
|
16
22
|
}>;
|
|
17
23
|
isLoading: boolean;
|
|
18
24
|
error: Error | null;
|
|
@@ -1 +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,
|
|
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"}
|
package/dist/react/useSignIn.js
CHANGED
|
@@ -6,18 +6,33 @@
|
|
|
6
6
|
'use client';
|
|
7
7
|
import { useCallback, useRef, useState } from 'react';
|
|
8
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
|
+
}
|
|
9
30
|
// Validation schemas for sign-in response
|
|
10
31
|
const SignInErrorResponseSchema = z.object({
|
|
11
32
|
error: z.string().optional(),
|
|
12
33
|
});
|
|
13
34
|
const SignInSuccessResponseSchema = z.object({
|
|
14
|
-
user:
|
|
15
|
-
.object({
|
|
16
|
-
id: z.string(),
|
|
17
|
-
email: z.string(),
|
|
18
|
-
name: z.string().nullable().optional(),
|
|
19
|
-
})
|
|
20
|
-
.passthrough(), // Allow all other User properties
|
|
35
|
+
user: SignInUserSchema,
|
|
21
36
|
});
|
|
22
37
|
/**
|
|
23
38
|
* Hook to sign in a user
|
|
@@ -68,12 +83,19 @@ export function useSignIn() {
|
|
|
68
83
|
error: errorData.error || 'Failed to sign in',
|
|
69
84
|
};
|
|
70
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
|
+
}
|
|
71
95
|
const successData = SignInSuccessResponseSchema.parse(json);
|
|
72
96
|
return {
|
|
73
97
|
success: true,
|
|
74
|
-
|
|
75
|
-
// The API returns serialized data, so we cast to expected type
|
|
76
|
-
user: successData.user,
|
|
98
|
+
user: toUser(successData.user),
|
|
77
99
|
};
|
|
78
100
|
}
|
|
79
101
|
catch (err) {
|
|
@@ -1 +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,
|
|
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"}
|
|
@@ -1 +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,
|
|
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"}
|
package/dist/react/useSignUp.js
CHANGED
|
@@ -6,18 +6,34 @@
|
|
|
6
6
|
'use client';
|
|
7
7
|
import { useState } from 'react';
|
|
8
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 SignUpUserSchema = 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
|
+
}
|
|
9
30
|
// Validation schemas for sign-up response
|
|
10
31
|
const SignUpErrorResponseSchema = z.object({
|
|
32
|
+
message: z.string().optional(),
|
|
11
33
|
error: z.string().optional(),
|
|
12
34
|
});
|
|
13
35
|
const SignUpSuccessResponseSchema = z.object({
|
|
14
|
-
user:
|
|
15
|
-
.object({
|
|
16
|
-
id: z.string(),
|
|
17
|
-
email: z.string(),
|
|
18
|
-
name: z.string().nullable().optional(),
|
|
19
|
-
})
|
|
20
|
-
.passthrough(), // Allow all other User properties
|
|
36
|
+
user: SignUpUserSchema,
|
|
21
37
|
});
|
|
22
38
|
/**
|
|
23
39
|
* Hook to sign up a new user
|
|
@@ -59,7 +75,7 @@ export function useSignUp() {
|
|
|
59
75
|
const errorData = SignUpErrorResponseSchema.parse(json);
|
|
60
76
|
return {
|
|
61
77
|
success: false,
|
|
62
|
-
error: errorData.error || 'Failed to sign up',
|
|
78
|
+
error: errorData.message || errorData.error || 'Failed to sign up',
|
|
63
79
|
};
|
|
64
80
|
}
|
|
65
81
|
const successData = SignUpSuccessResponseSchema.parse(json);
|
|
@@ -67,7 +83,7 @@ export function useSignUp() {
|
|
|
67
83
|
success: true,
|
|
68
84
|
// Type assertion through unknown is safe because Zod validation ensures the shape is correct
|
|
69
85
|
// The API returns serialized data, so we cast to expected type
|
|
70
|
-
user: successData.user,
|
|
86
|
+
user: toUser(successData.user),
|
|
71
87
|
};
|
|
72
88
|
}
|
|
73
89
|
catch (err) {
|
package/dist/server/auth.d.ts
CHANGED
|
@@ -40,5 +40,7 @@ export declare function isSignupAllowed(email: string): boolean;
|
|
|
40
40
|
export declare function signUp(email: string, password: string, name: string, options?: {
|
|
41
41
|
userAgent?: string;
|
|
42
42
|
ipAddress?: string;
|
|
43
|
+
tosAcceptedAt?: Date;
|
|
44
|
+
tosVersion?: string;
|
|
43
45
|
}): Promise<SignUpResult>;
|
|
44
46
|
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/server/auth.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/server/auth.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAQ,MAAM,aAAa,CAAC;AASpE;;;;;;;GAOG;AACH,wBAAsB,MAAM,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GACA,OAAO,CAAC,YAAY,CAAC,CA2JvB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAiBtD;AAED;;;;;;;;GAQG;AACH,wBAAsB,MAAM,CAC1B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GACA,OAAO,CAAC,YAAY,CAAC,CAsLvB"}
|
package/dist/server/auth.js
CHANGED
|
@@ -3,15 +3,18 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Sign in and sign up functionality with password hashing.
|
|
5
5
|
*/
|
|
6
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
6
7
|
import { logger } from '@revealui/core/observability/logger';
|
|
7
8
|
import { getClient } from '@revealui/db/client';
|
|
8
|
-
import { users } from '@revealui/db/schema';
|
|
9
|
+
import { oauthAccounts, users } from '@revealui/db/schema';
|
|
9
10
|
import bcrypt from 'bcryptjs';
|
|
10
|
-
import { eq } from 'drizzle-orm';
|
|
11
|
+
import { and, eq, isNull } from 'drizzle-orm';
|
|
11
12
|
import { clearFailedAttempts, isAccountLocked, recordFailedAttempt } from './brute-force.js';
|
|
12
13
|
import { validatePasswordStrength } from './password-validation.js';
|
|
13
14
|
import { checkRateLimit } from './rate-limit.js';
|
|
14
15
|
import { createSession } from './session.js';
|
|
16
|
+
/** Grace period after signup during which unverified users can still sign in (24 hours) */
|
|
17
|
+
const EMAIL_VERIFICATION_GRACE_PERIOD_MS = 24 * 60 * 60 * 1000;
|
|
15
18
|
/**
|
|
16
19
|
* Sign in with email and password
|
|
17
20
|
*
|
|
@@ -28,6 +31,7 @@ export async function signIn(email, password, options) {
|
|
|
28
31
|
if (!rateLimit.allowed) {
|
|
29
32
|
return {
|
|
30
33
|
success: false,
|
|
34
|
+
reason: 'rate_limited',
|
|
31
35
|
error: 'Too many login attempts. Please try again later.',
|
|
32
36
|
};
|
|
33
37
|
}
|
|
@@ -39,6 +43,7 @@ export async function signIn(email, password, options) {
|
|
|
39
43
|
: 30;
|
|
40
44
|
return {
|
|
41
45
|
success: false,
|
|
46
|
+
reason: 'account_locked',
|
|
42
47
|
error: `Account locked due to too many failed attempts. Please try again in ${lockMinutes} minutes.`,
|
|
43
48
|
};
|
|
44
49
|
}
|
|
@@ -50,19 +55,25 @@ export async function signIn(email, password, options) {
|
|
|
50
55
|
logger.error('Error getting database client');
|
|
51
56
|
return {
|
|
52
57
|
success: false,
|
|
58
|
+
reason: 'database_error',
|
|
53
59
|
error: 'Database connection failed',
|
|
54
60
|
};
|
|
55
61
|
}
|
|
56
62
|
// Find user by email
|
|
57
63
|
let user;
|
|
58
64
|
try {
|
|
59
|
-
const result = await db
|
|
65
|
+
const result = await db
|
|
66
|
+
.select()
|
|
67
|
+
.from(users)
|
|
68
|
+
.where(and(eq(users.email, email), isNull(users.deletedAt)))
|
|
69
|
+
.limit(1);
|
|
60
70
|
user = result[0];
|
|
61
71
|
}
|
|
62
72
|
catch {
|
|
63
73
|
logger.error('Error querying user');
|
|
64
74
|
return {
|
|
65
75
|
success: false,
|
|
76
|
+
reason: 'database_error',
|
|
66
77
|
error: 'Database error',
|
|
67
78
|
};
|
|
68
79
|
}
|
|
@@ -72,6 +83,7 @@ export async function signIn(email, password, options) {
|
|
|
72
83
|
await recordFailedAttempt(email);
|
|
73
84
|
return {
|
|
74
85
|
success: false,
|
|
86
|
+
reason: 'invalid_credentials',
|
|
75
87
|
error: invalidCredentialsMessage,
|
|
76
88
|
};
|
|
77
89
|
}
|
|
@@ -80,6 +92,7 @@ export async function signIn(email, password, options) {
|
|
|
80
92
|
await recordFailedAttempt(email);
|
|
81
93
|
return {
|
|
82
94
|
success: false,
|
|
95
|
+
reason: 'invalid_credentials',
|
|
83
96
|
error: invalidCredentialsMessage,
|
|
84
97
|
};
|
|
85
98
|
}
|
|
@@ -93,6 +106,7 @@ export async function signIn(email, password, options) {
|
|
|
93
106
|
await recordFailedAttempt(email);
|
|
94
107
|
return {
|
|
95
108
|
success: false,
|
|
109
|
+
reason: 'invalid_credentials',
|
|
96
110
|
error: invalidCredentialsMessage,
|
|
97
111
|
};
|
|
98
112
|
}
|
|
@@ -100,11 +114,31 @@ export async function signIn(email, password, options) {
|
|
|
100
114
|
await recordFailedAttempt(email);
|
|
101
115
|
return {
|
|
102
116
|
success: false,
|
|
117
|
+
reason: 'invalid_credentials',
|
|
103
118
|
error: invalidCredentialsMessage,
|
|
104
119
|
};
|
|
105
120
|
}
|
|
106
121
|
// Successful login - clear failed attempts
|
|
107
122
|
await clearFailedAttempts(email);
|
|
123
|
+
// Check email verification (with grace period for new accounts)
|
|
124
|
+
if (!user.emailVerified) {
|
|
125
|
+
const accountAge = Date.now() - user.createdAt.getTime();
|
|
126
|
+
if (accountAge > EMAIL_VERIFICATION_GRACE_PERIOD_MS) {
|
|
127
|
+
return {
|
|
128
|
+
success: false,
|
|
129
|
+
reason: 'email_not_verified',
|
|
130
|
+
error: 'Please verify your email address before signing in.',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Check if MFA is enabled — if so, return early and require TOTP verification
|
|
135
|
+
if (user.mfaEnabled) {
|
|
136
|
+
return {
|
|
137
|
+
success: true,
|
|
138
|
+
requiresMfa: true,
|
|
139
|
+
mfaUserId: user.id,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
108
142
|
// Create session
|
|
109
143
|
let token;
|
|
110
144
|
try {
|
|
@@ -118,6 +152,7 @@ export async function signIn(email, password, options) {
|
|
|
118
152
|
logger.error('Error creating session');
|
|
119
153
|
return {
|
|
120
154
|
success: false,
|
|
155
|
+
reason: 'session_error',
|
|
121
156
|
error: 'Failed to create session',
|
|
122
157
|
};
|
|
123
158
|
}
|
|
@@ -131,6 +166,7 @@ export async function signIn(email, password, options) {
|
|
|
131
166
|
logger.error('Unexpected error in signIn');
|
|
132
167
|
return {
|
|
133
168
|
success: false,
|
|
169
|
+
reason: 'unexpected_error',
|
|
134
170
|
error: 'Unexpected error',
|
|
135
171
|
};
|
|
136
172
|
}
|
|
@@ -207,7 +243,9 @@ export async function signUp(email, password, name, options) {
|
|
|
207
243
|
error: 'Database connection failed',
|
|
208
244
|
};
|
|
209
245
|
}
|
|
210
|
-
// Check if user already exists
|
|
246
|
+
// Check if user already exists (by email in users table or OAuth accounts).
|
|
247
|
+
// Both checks prevent account collision: a password signup must not collide
|
|
248
|
+
// with an existing OAuth identity for the same email address.
|
|
211
249
|
let existing;
|
|
212
250
|
try {
|
|
213
251
|
const result = await db.select().from(users).where(eq(users.email, email)).limit(1);
|
|
@@ -226,6 +264,29 @@ export async function signUp(email, password, name, options) {
|
|
|
226
264
|
error: 'Unable to create account',
|
|
227
265
|
};
|
|
228
266
|
}
|
|
267
|
+
// Block signup if an OAuth account already uses this email.
|
|
268
|
+
// Without this check, an attacker could create a password account
|
|
269
|
+
// for an email that was registered via OAuth, enabling account takeover.
|
|
270
|
+
try {
|
|
271
|
+
const [existingOAuth] = await db
|
|
272
|
+
.select({ id: oauthAccounts.id })
|
|
273
|
+
.from(oauthAccounts)
|
|
274
|
+
.where(eq(oauthAccounts.providerEmail, email))
|
|
275
|
+
.limit(1);
|
|
276
|
+
if (existingOAuth) {
|
|
277
|
+
return {
|
|
278
|
+
success: false,
|
|
279
|
+
error: 'Unable to create account',
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
logger.error('Error checking OAuth accounts');
|
|
285
|
+
return {
|
|
286
|
+
success: false,
|
|
287
|
+
error: 'Database error',
|
|
288
|
+
};
|
|
289
|
+
}
|
|
229
290
|
// Hash password
|
|
230
291
|
let hashedPassword;
|
|
231
292
|
try {
|
|
@@ -238,6 +299,14 @@ export async function signUp(email, password, name, options) {
|
|
|
238
299
|
error: 'Failed to process password',
|
|
239
300
|
};
|
|
240
301
|
}
|
|
302
|
+
// Generate email verification token.
|
|
303
|
+
// Store the SHA-256 hash in the DB; send the raw token in the email link.
|
|
304
|
+
// A DB breach cannot be used to verify arbitrary emails without the raw token.
|
|
305
|
+
const rawEmailVerificationToken = randomBytes(32).toString('hex');
|
|
306
|
+
const emailVerificationToken = createHash('sha256')
|
|
307
|
+
.update(rawEmailVerificationToken)
|
|
308
|
+
.digest('hex');
|
|
309
|
+
const emailVerificationTokenExpiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24h
|
|
241
310
|
// Create user
|
|
242
311
|
let user;
|
|
243
312
|
try {
|
|
@@ -248,6 +317,11 @@ export async function signUp(email, password, name, options) {
|
|
|
248
317
|
email,
|
|
249
318
|
name,
|
|
250
319
|
password: hashedPassword,
|
|
320
|
+
emailVerified: false,
|
|
321
|
+
emailVerificationToken,
|
|
322
|
+
emailVerificationTokenExpiresAt,
|
|
323
|
+
tosAcceptedAt: options?.tosAcceptedAt ?? null,
|
|
324
|
+
tosVersion: options?.tosVersion ?? null,
|
|
251
325
|
})
|
|
252
326
|
.returning();
|
|
253
327
|
user = result[0];
|
|
@@ -276,14 +350,28 @@ export async function signUp(email, password, name, options) {
|
|
|
276
350
|
}
|
|
277
351
|
catch {
|
|
278
352
|
logger.error('Error creating session');
|
|
353
|
+
// Clean up orphaned user so the email isn't permanently locked out.
|
|
354
|
+
// Without this, a retry would fail with "Unable to create account"
|
|
355
|
+
// because the user row already exists but has no valid session.
|
|
356
|
+
try {
|
|
357
|
+
await db.delete(users).where(eq(users.id, user.id));
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
logger.error('Failed to clean up orphaned user after session creation failure', undefined, {
|
|
361
|
+
userId: user.id,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
279
364
|
return {
|
|
280
365
|
success: false,
|
|
281
366
|
error: 'Failed to create session',
|
|
282
367
|
};
|
|
283
368
|
}
|
|
369
|
+
// Return the raw (unhashed) token so the caller can include it in the
|
|
370
|
+
// verification email link. The DB holds only the hash.
|
|
371
|
+
const userWithRawToken = { ...user, emailVerificationToken: rawEmailVerificationToken };
|
|
284
372
|
return {
|
|
285
373
|
success: true,
|
|
286
|
-
user,
|
|
374
|
+
user: userWithRawToken,
|
|
287
375
|
sessionToken: token,
|
|
288
376
|
};
|
|
289
377
|
}
|