@delmaredigital/payload-better-auth 0.7.4 → 0.7.6
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 +1 -1
- package/dist/components/LoginView.d.ts +23 -1
- package/dist/components/LoginView.js +544 -135
- package/dist/components/LoginViewWrapper.js +4 -0
- package/dist/exports/client.js +4 -0
- package/dist/plugin/index.d.ts +30 -0
- package/dist/utils/loginMethods.d.ts +26 -0
- package/dist/utils/loginMethods.js +24 -0
- package/package.json +11 -10
package/README.md
CHANGED
|
@@ -178,7 +178,7 @@ export default async function Dashboard() {
|
|
|
178
178
|
|
|
179
179
|
---
|
|
180
180
|
|
|
181
|
-
For MongoDB setup, API reference, customization, access control helpers, API key scopes, plugin compatibility, UI components (2FA, passkeys, password reset), recipes, and types — see the **[full documentation](https://delmaredigital.github.io/payload-better-auth/)**.
|
|
181
|
+
For MongoDB setup, API reference, customization, access control helpers, API key scopes, plugin compatibility, UI components (2FA, passkeys, password reset, passwordless login via magic-link & email-OTP), recipes, and types — see the **[full documentation](https://delmaredigital.github.io/payload-better-auth/)**.
|
|
182
182
|
|
|
183
183
|
## License
|
|
184
184
|
|
|
@@ -52,6 +52,28 @@ export type LoginViewProps = {
|
|
|
52
52
|
* The reset token will be appended as ?token=xxx
|
|
53
53
|
*/
|
|
54
54
|
resetPasswordUrl?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Enable email + password sign-in.
|
|
57
|
+
* - true: Always show the password field
|
|
58
|
+
* - false: Hide the password field (passwordless-only)
|
|
59
|
+
* - 'auto' (default): Auto-detect via the /sign-in/email endpoint
|
|
60
|
+
*/
|
|
61
|
+
enablePassword?: boolean | 'auto';
|
|
62
|
+
/**
|
|
63
|
+
* Enable magic-link sign-in ("email me a link").
|
|
64
|
+
* - true / false / 'auto' (default: auto-detect via /sign-in/magic-link)
|
|
65
|
+
*/
|
|
66
|
+
enableMagicLink?: boolean | 'auto';
|
|
67
|
+
/**
|
|
68
|
+
* Enable email-OTP sign-in ("email me a code").
|
|
69
|
+
* - true / false / 'auto' (default: auto-detect via /email-otp/send-verification-otp)
|
|
70
|
+
*/
|
|
71
|
+
enableEmailOtp?: boolean | 'auto';
|
|
72
|
+
/**
|
|
73
|
+
* Where the emailed magic link returns after verification.
|
|
74
|
+
* Default: afterLoginPath
|
|
75
|
+
*/
|
|
76
|
+
magicLinkCallbackURL?: string;
|
|
55
77
|
};
|
|
56
|
-
export declare function LoginView({ authClient: providedClient, logo, title, afterLoginPath, requiredRole, requireAllRoles, enablePasskey, enableSignUp, defaultSignUpRole, enableForgotPassword, resetPasswordUrl, }: LoginViewProps): import("react").JSX.Element;
|
|
78
|
+
export declare function LoginView({ authClient: providedClient, logo, title, afterLoginPath, requiredRole, requireAllRoles, enablePasskey, enableSignUp, defaultSignUpRole, enableForgotPassword, resetPasswordUrl, enablePassword, enableMagicLink, enableEmailOtp, magicLinkCallbackURL, }: LoginViewProps): import("react").JSX.Element;
|
|
57
79
|
export default LoginView;
|
|
@@ -3,8 +3,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import { useState, useEffect, useRef } from 'react';
|
|
4
4
|
import { useRouter } from 'next/navigation.js';
|
|
5
5
|
import { createAuthClient } from 'better-auth/react';
|
|
6
|
-
import { twoFactorClient } from 'better-auth/client/plugins';
|
|
6
|
+
import { twoFactorClient, magicLinkClient, emailOTPClient } from 'better-auth/client/plugins';
|
|
7
7
|
import { hasAnyRole, hasAllRoles } from '../utils/access.js';
|
|
8
|
+
import { resolveAvailability, pickPrimaryMethod } from '../utils/loginMethods.js';
|
|
8
9
|
import { useConfig } from '@payloadcms/ui';
|
|
9
10
|
/**
|
|
10
11
|
* Check if user has the required role(s)
|
|
@@ -21,7 +22,7 @@ import { useConfig } from '@payloadcms/ui';
|
|
|
21
22
|
}
|
|
22
23
|
return hasAnyRole(user, roles);
|
|
23
24
|
}
|
|
24
|
-
export function LoginView({ authClient: providedClient, logo, title = 'Login', afterLoginPath = '/admin', requiredRole = 'admin', requireAllRoles = false, enablePasskey = 'auto', enableSignUp = 'auto', defaultSignUpRole = 'user', enableForgotPassword = 'auto', resetPasswordUrl }) {
|
|
25
|
+
export function LoginView({ authClient: providedClient, logo, title = 'Login', afterLoginPath = '/admin', requiredRole = 'admin', requireAllRoles = false, enablePasskey = 'auto', enableSignUp = 'auto', defaultSignUpRole = 'user', enableForgotPassword = 'auto', resetPasswordUrl, enablePassword = 'auto', enableMagicLink = 'auto', enableEmailOtp = 'auto', magicLinkCallbackURL }) {
|
|
25
26
|
const router = useRouter();
|
|
26
27
|
// Payload Config
|
|
27
28
|
const { config: { routes: { admin: adminRoute, api: apiRoute } } } = useConfig();
|
|
@@ -43,6 +44,15 @@ export function LoginView({ authClient: providedClient, logo, title = 'Login', a
|
|
|
43
44
|
const [passkeyAvailable, setPasskeyAvailable] = useState(enablePasskey === true);
|
|
44
45
|
const [signUpAvailable, setSignUpAvailable] = useState(enableSignUp === true);
|
|
45
46
|
const [forgotPasswordAvailable, setForgotPasswordAvailable] = useState(enableForgotPassword === true);
|
|
47
|
+
// Probe results for the new methods (null = not yet probed).
|
|
48
|
+
// Password is optimistic (shown until a 404 proves the strategy is disabled);
|
|
49
|
+
// magic-link and email-OTP stay hidden until a probe confirms availability.
|
|
50
|
+
const [passwordProbe, setPasswordProbe] = useState(true);
|
|
51
|
+
const [magicLinkProbe, setMagicLinkProbe] = useState(null);
|
|
52
|
+
const [emailOtpProbe, setEmailOtpProbe] = useState(null);
|
|
53
|
+
// Email-OTP code entry state
|
|
54
|
+
const [otp, setOtp] = useState('');
|
|
55
|
+
const [otpLoading, setOtpLoading] = useState(false);
|
|
46
56
|
// Two-factor authentication state
|
|
47
57
|
const [totpCode, setTotpCode] = useState('');
|
|
48
58
|
const [totpLoading, setTotpLoading] = useState(false);
|
|
@@ -55,6 +65,8 @@ export function LoginView({ authClient: providedClient, logo, title = 'Login', a
|
|
|
55
65
|
clientRef.current = createAuthClient({
|
|
56
66
|
plugins: [
|
|
57
67
|
twoFactorClient(),
|
|
68
|
+
magicLinkClient(),
|
|
69
|
+
emailOTPClient(),
|
|
58
70
|
passkeyClient()
|
|
59
71
|
]
|
|
60
72
|
});
|
|
@@ -150,6 +162,54 @@ export function LoginView({ authClient: providedClient, logo, title = 'Login', a
|
|
|
150
162
|
}, [
|
|
151
163
|
enableForgotPassword
|
|
152
164
|
]);
|
|
165
|
+
// Auto-detect password (email) sign-in availability if set to 'auto'
|
|
166
|
+
useEffect(()=>{
|
|
167
|
+
if (enablePassword !== 'auto') return;
|
|
168
|
+
fetch(`${apiRoute}/auth/sign-in/email`, {
|
|
169
|
+
method: 'OPTIONS',
|
|
170
|
+
credentials: 'include'
|
|
171
|
+
}).then((res)=>setPasswordProbe(res.status !== 404)).catch(()=>setPasswordProbe(true)); // core method: assume available on probe error
|
|
172
|
+
}, [
|
|
173
|
+
enablePassword
|
|
174
|
+
]);
|
|
175
|
+
// Auto-detect magic-link availability if set to 'auto'
|
|
176
|
+
useEffect(()=>{
|
|
177
|
+
if (enableMagicLink !== 'auto') return;
|
|
178
|
+
fetch(`${apiRoute}/auth/sign-in/magic-link`, {
|
|
179
|
+
method: 'OPTIONS',
|
|
180
|
+
credentials: 'include'
|
|
181
|
+
}).then((res)=>setMagicLinkProbe(res.status !== 404)).catch(()=>setMagicLinkProbe(false)); // optional method: assume unavailable on error
|
|
182
|
+
}, [
|
|
183
|
+
enableMagicLink
|
|
184
|
+
]);
|
|
185
|
+
// Auto-detect email-OTP availability if set to 'auto'
|
|
186
|
+
useEffect(()=>{
|
|
187
|
+
if (enableEmailOtp !== 'auto') return;
|
|
188
|
+
fetch(`${apiRoute}/auth/email-otp/send-verification-otp`, {
|
|
189
|
+
method: 'OPTIONS',
|
|
190
|
+
credentials: 'include'
|
|
191
|
+
}).then((res)=>setEmailOtpProbe(res.status !== 404)).catch(()=>setEmailOtpProbe(false));
|
|
192
|
+
}, [
|
|
193
|
+
enableEmailOtp
|
|
194
|
+
]);
|
|
195
|
+
/**
|
|
196
|
+
* Shared post-authentication tail: re-fetch the session for complete user data
|
|
197
|
+
* (e.g. roles applied by hooks), enforce the role gate, and redirect on success.
|
|
198
|
+
* Returns the outcome so each caller can reset its own loading flag / show its own
|
|
199
|
+
* "no session" message.
|
|
200
|
+
*/ async function completeSignIn(// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
201
|
+
client) {
|
|
202
|
+
const sessionResult = await client.getSession();
|
|
203
|
+
if (!sessionResult.data?.user) return 'noSession';
|
|
204
|
+
const user = sessionResult.data.user;
|
|
205
|
+
if (!checkUserRoles(user, requiredRole, requireAllRoles)) {
|
|
206
|
+
setAccessDenied(true);
|
|
207
|
+
return 'accessDenied';
|
|
208
|
+
}
|
|
209
|
+
router.push(afterLoginPath);
|
|
210
|
+
router.refresh();
|
|
211
|
+
return 'redirected';
|
|
212
|
+
}
|
|
153
213
|
async function handleSubmit(e) {
|
|
154
214
|
e.preventDefault();
|
|
155
215
|
setLoading(true);
|
|
@@ -174,15 +234,13 @@ export function LoginView({ authClient: providedClient, logo, title = 'Login', a
|
|
|
174
234
|
return;
|
|
175
235
|
}
|
|
176
236
|
if (result.data?.user) {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
237
|
+
const outcome = await completeSignIn(client);
|
|
238
|
+
if (outcome === 'noSession') {
|
|
239
|
+
setError('Sign-in succeeded but session could not be verified');
|
|
240
|
+
setLoading(false);
|
|
241
|
+
} else if (outcome === 'accessDenied') {
|
|
181
242
|
setLoading(false);
|
|
182
|
-
return;
|
|
183
243
|
}
|
|
184
|
-
router.push(afterLoginPath);
|
|
185
|
-
router.refresh();
|
|
186
244
|
}
|
|
187
245
|
} catch {
|
|
188
246
|
setError('An error occurred. Please try again.');
|
|
@@ -221,20 +279,15 @@ export function LoginView({ authClient: providedClient, logo, title = 'Login', a
|
|
|
221
279
|
}
|
|
222
280
|
// Registration successful - either auto-signed in or need to verify email
|
|
223
281
|
if (result.data?.user) {
|
|
224
|
-
// Re-fetch session
|
|
225
|
-
//
|
|
226
|
-
const
|
|
227
|
-
if (
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
setLoading(false);
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
282
|
+
// Re-fetch session via completeSignIn to pick up hook-applied roles
|
|
283
|
+
// (e.g. firstUserAdmin sets the role after creation).
|
|
284
|
+
const outcome = await completeSignIn(client);
|
|
285
|
+
if (outcome === 'noSession') {
|
|
286
|
+
setError('Account created but session could not be verified. Please sign in.');
|
|
287
|
+
setLoading(false);
|
|
288
|
+
} else if (outcome === 'accessDenied') {
|
|
289
|
+
setLoading(false);
|
|
235
290
|
}
|
|
236
|
-
router.push(afterLoginPath);
|
|
237
|
-
router.refresh();
|
|
238
291
|
} else {
|
|
239
292
|
// Likely requires email verification - show success and switch to login
|
|
240
293
|
setSuccessMessage('Account created! Please check your email to verify your account.');
|
|
@@ -286,21 +339,15 @@ export function LoginView({ authClient: providedClient, logo, title = 'Login', a
|
|
|
286
339
|
setTotpLoading(false);
|
|
287
340
|
return;
|
|
288
341
|
}
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
setTotpLoading(false);
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
342
|
+
// verify-totp may not return all user fields (e.g. custom 'role');
|
|
343
|
+
// completeSignIn re-fetches the session for the role gate.
|
|
344
|
+
const outcome = await completeSignIn(client);
|
|
345
|
+
if (outcome === 'noSession') {
|
|
346
|
+
setError('Sign-in succeeded but session could not be verified');
|
|
347
|
+
setTotpLoading(false);
|
|
348
|
+
} else if (outcome === 'accessDenied') {
|
|
349
|
+
setTotpLoading(false);
|
|
301
350
|
}
|
|
302
|
-
router.push(afterLoginPath);
|
|
303
|
-
router.refresh();
|
|
304
351
|
} catch {
|
|
305
352
|
setError('An error occurred. Please try again.');
|
|
306
353
|
setTotpLoading(false);
|
|
@@ -314,6 +361,7 @@ export function LoginView({ authClient: providedClient, logo, title = 'Login', a
|
|
|
314
361
|
if (newView === 'login') {
|
|
315
362
|
setTotpCode('');
|
|
316
363
|
setConfirmPassword('');
|
|
364
|
+
setOtp('');
|
|
317
365
|
} else if (newView === 'register') {
|
|
318
366
|
setPassword('');
|
|
319
367
|
setConfirmPassword('');
|
|
@@ -337,23 +385,14 @@ export function LoginView({ authClient: providedClient, logo, title = 'Login', a
|
|
|
337
385
|
setPasskeyLoading(false);
|
|
338
386
|
return;
|
|
339
387
|
}
|
|
340
|
-
// Passkey sign-in succeeded -
|
|
341
|
-
//
|
|
342
|
-
const
|
|
343
|
-
if (
|
|
344
|
-
const user = sessionResult.data.user;
|
|
345
|
-
// Check role if required
|
|
346
|
-
if (!checkUserRoles(user, requiredRole, requireAllRoles)) {
|
|
347
|
-
setAccessDenied(true);
|
|
348
|
-
setPasskeyLoading(false);
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
router.push(afterLoginPath);
|
|
352
|
-
router.refresh();
|
|
353
|
-
} else {
|
|
354
|
-
// Session fetch failed - shouldn't happen after successful passkey auth
|
|
388
|
+
// Passkey sign-in succeeded - completeSignIn re-fetches the session for full
|
|
389
|
+
// user data (including role), more reliable than result.data.user across SDK versions.
|
|
390
|
+
const outcome = await completeSignIn(client);
|
|
391
|
+
if (outcome === 'noSession') {
|
|
355
392
|
setError('Authentication succeeded but session could not be verified');
|
|
356
393
|
setPasskeyLoading(false);
|
|
394
|
+
} else if (outcome === 'accessDenied') {
|
|
395
|
+
setPasskeyLoading(false);
|
|
357
396
|
}
|
|
358
397
|
} catch (err) {
|
|
359
398
|
if (err instanceof Error && err.name === 'NotAllowedError') {
|
|
@@ -364,6 +403,88 @@ export function LoginView({ authClient: providedClient, logo, title = 'Login', a
|
|
|
364
403
|
setPasskeyLoading(false);
|
|
365
404
|
}
|
|
366
405
|
}
|
|
406
|
+
async function handleSendMagicLink(e) {
|
|
407
|
+
e?.preventDefault();
|
|
408
|
+
if (!email) {
|
|
409
|
+
setError('Please enter your email address first');
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
setLoading(true);
|
|
413
|
+
setError(null);
|
|
414
|
+
setSuccessMessage(null);
|
|
415
|
+
try {
|
|
416
|
+
const client = await getClient();
|
|
417
|
+
const result = await client.signIn.magicLink({
|
|
418
|
+
email,
|
|
419
|
+
callbackURL: magicLinkCallbackURL ?? afterLoginPath
|
|
420
|
+
});
|
|
421
|
+
if (result.error) {
|
|
422
|
+
setError(result.error.message ?? 'Failed to send sign-in link');
|
|
423
|
+
setLoading(false);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
setViewMode('magicLinkSent');
|
|
427
|
+
setLoading(false);
|
|
428
|
+
} catch {
|
|
429
|
+
setError('An error occurred. Please try again.');
|
|
430
|
+
setLoading(false);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async function handleSendEmailOtp(e) {
|
|
434
|
+
e?.preventDefault();
|
|
435
|
+
if (!email) {
|
|
436
|
+
setError('Please enter your email address first');
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
setLoading(true);
|
|
440
|
+
setError(null);
|
|
441
|
+
setSuccessMessage(null);
|
|
442
|
+
try {
|
|
443
|
+
const client = await getClient();
|
|
444
|
+
const result = await client.emailOtp.sendVerificationOtp({
|
|
445
|
+
email,
|
|
446
|
+
type: 'sign-in'
|
|
447
|
+
});
|
|
448
|
+
if (result.error) {
|
|
449
|
+
setError(result.error.message ?? 'Failed to send verification code');
|
|
450
|
+
setLoading(false);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
setOtp('');
|
|
454
|
+
setViewMode('emailOtp');
|
|
455
|
+
setLoading(false);
|
|
456
|
+
} catch {
|
|
457
|
+
setError('An error occurred. Please try again.');
|
|
458
|
+
setLoading(false);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async function handleVerifyEmailOtp(e) {
|
|
462
|
+
e.preventDefault();
|
|
463
|
+
setOtpLoading(true);
|
|
464
|
+
setError(null);
|
|
465
|
+
try {
|
|
466
|
+
const client = await getClient();
|
|
467
|
+
const result = await client.signIn.emailOtp({
|
|
468
|
+
email,
|
|
469
|
+
otp
|
|
470
|
+
});
|
|
471
|
+
if (result.error) {
|
|
472
|
+
setError(result.error.message ?? 'Invalid verification code');
|
|
473
|
+
setOtpLoading(false);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const outcome = await completeSignIn(client);
|
|
477
|
+
if (outcome === 'noSession') {
|
|
478
|
+
setError('Sign-in succeeded but session could not be verified');
|
|
479
|
+
setOtpLoading(false);
|
|
480
|
+
} else if (outcome === 'accessDenied') {
|
|
481
|
+
setOtpLoading(false);
|
|
482
|
+
}
|
|
483
|
+
} catch {
|
|
484
|
+
setError('An error occurred. Please try again.');
|
|
485
|
+
setOtpLoading(false);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
367
488
|
async function handleSignOut() {
|
|
368
489
|
const client = await getClient();
|
|
369
490
|
await client.signOut();
|
|
@@ -592,6 +713,156 @@ export function LoginView({ authClient: providedClient, logo, title = 'Login', a
|
|
|
592
713
|
})
|
|
593
714
|
});
|
|
594
715
|
}
|
|
716
|
+
// Email-OTP code entry view
|
|
717
|
+
if (viewMode === 'emailOtp') {
|
|
718
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
719
|
+
style: {
|
|
720
|
+
minHeight: '100vh',
|
|
721
|
+
display: 'flex',
|
|
722
|
+
alignItems: 'center',
|
|
723
|
+
justifyContent: 'center',
|
|
724
|
+
background: 'var(--theme-bg)',
|
|
725
|
+
padding: 'var(--base)'
|
|
726
|
+
},
|
|
727
|
+
children: /*#__PURE__*/ _jsxs("div", {
|
|
728
|
+
style: {
|
|
729
|
+
background: 'var(--theme-elevation-50)',
|
|
730
|
+
padding: 'calc(var(--base) * 2)',
|
|
731
|
+
borderRadius: 'var(--style-radius-m)',
|
|
732
|
+
boxShadow: '0 2px 20px rgba(0, 0, 0, 0.1)',
|
|
733
|
+
width: '100%',
|
|
734
|
+
maxWidth: '400px'
|
|
735
|
+
},
|
|
736
|
+
children: [
|
|
737
|
+
logo && /*#__PURE__*/ _jsx("div", {
|
|
738
|
+
style: {
|
|
739
|
+
textAlign: 'center',
|
|
740
|
+
marginBottom: 'calc(var(--base) * 1.5)'
|
|
741
|
+
},
|
|
742
|
+
children: logo
|
|
743
|
+
}),
|
|
744
|
+
/*#__PURE__*/ _jsx("h1", {
|
|
745
|
+
style: {
|
|
746
|
+
color: 'var(--theme-text)',
|
|
747
|
+
fontSize: 'var(--font-size-h3)',
|
|
748
|
+
fontWeight: 600,
|
|
749
|
+
margin: '0 0 calc(var(--base) * 0.5) 0',
|
|
750
|
+
textAlign: 'center'
|
|
751
|
+
},
|
|
752
|
+
children: "Enter Your Code"
|
|
753
|
+
}),
|
|
754
|
+
/*#__PURE__*/ _jsxs("p", {
|
|
755
|
+
style: {
|
|
756
|
+
color: 'var(--theme-text)',
|
|
757
|
+
opacity: 0.7,
|
|
758
|
+
fontSize: 'var(--font-size-small)',
|
|
759
|
+
textAlign: 'center',
|
|
760
|
+
marginBottom: 'calc(var(--base) * 1.5)'
|
|
761
|
+
},
|
|
762
|
+
children: [
|
|
763
|
+
"We've sent a verification code to ",
|
|
764
|
+
/*#__PURE__*/ _jsx("strong", {
|
|
765
|
+
children: email
|
|
766
|
+
})
|
|
767
|
+
]
|
|
768
|
+
}),
|
|
769
|
+
/*#__PURE__*/ _jsxs("form", {
|
|
770
|
+
onSubmit: handleVerifyEmailOtp,
|
|
771
|
+
children: [
|
|
772
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
773
|
+
style: {
|
|
774
|
+
marginBottom: 'calc(var(--base) * 1.5)'
|
|
775
|
+
},
|
|
776
|
+
children: [
|
|
777
|
+
/*#__PURE__*/ _jsx("label", {
|
|
778
|
+
htmlFor: "email-otp-code",
|
|
779
|
+
style: {
|
|
780
|
+
display: 'block',
|
|
781
|
+
color: 'var(--theme-text)',
|
|
782
|
+
marginBottom: 'calc(var(--base) * 0.5)',
|
|
783
|
+
fontSize: 'var(--font-size-small)',
|
|
784
|
+
fontWeight: 500
|
|
785
|
+
},
|
|
786
|
+
children: "Verification Code"
|
|
787
|
+
}),
|
|
788
|
+
/*#__PURE__*/ _jsx("input", {
|
|
789
|
+
id: "email-otp-code",
|
|
790
|
+
type: "text",
|
|
791
|
+
inputMode: "numeric",
|
|
792
|
+
autoComplete: "one-time-code",
|
|
793
|
+
value: otp,
|
|
794
|
+
onChange: (e)=>setOtp(e.target.value.replace(/\D/g, '').slice(0, 6)),
|
|
795
|
+
required: true,
|
|
796
|
+
placeholder: "000000",
|
|
797
|
+
style: {
|
|
798
|
+
width: '100%',
|
|
799
|
+
padding: 'calc(var(--base) * 0.75)',
|
|
800
|
+
background: 'var(--theme-input-bg)',
|
|
801
|
+
border: '1px solid var(--theme-elevation-150)',
|
|
802
|
+
borderRadius: 'var(--style-radius-s)',
|
|
803
|
+
color: 'var(--theme-text)',
|
|
804
|
+
fontSize: 'var(--font-size-h4)',
|
|
805
|
+
fontFamily: 'monospace',
|
|
806
|
+
textAlign: 'center',
|
|
807
|
+
letterSpacing: '0.5em',
|
|
808
|
+
outline: 'none',
|
|
809
|
+
boxSizing: 'border-box'
|
|
810
|
+
}
|
|
811
|
+
})
|
|
812
|
+
]
|
|
813
|
+
}),
|
|
814
|
+
error && /*#__PURE__*/ _jsx("div", {
|
|
815
|
+
style: {
|
|
816
|
+
color: 'var(--theme-error-500)',
|
|
817
|
+
marginBottom: 'var(--base)',
|
|
818
|
+
fontSize: 'var(--font-size-small)',
|
|
819
|
+
padding: 'calc(var(--base) * 0.5)',
|
|
820
|
+
background: 'var(--theme-error-50)',
|
|
821
|
+
borderRadius: 'var(--style-radius-s)',
|
|
822
|
+
border: '1px solid var(--theme-error-200)'
|
|
823
|
+
},
|
|
824
|
+
children: error
|
|
825
|
+
}),
|
|
826
|
+
/*#__PURE__*/ _jsx("button", {
|
|
827
|
+
type: "submit",
|
|
828
|
+
disabled: otpLoading || otp.length !== 6,
|
|
829
|
+
style: {
|
|
830
|
+
width: '100%',
|
|
831
|
+
padding: 'calc(var(--base) * 0.75)',
|
|
832
|
+
background: 'var(--theme-elevation-800)',
|
|
833
|
+
border: 'none',
|
|
834
|
+
borderRadius: 'var(--style-radius-s)',
|
|
835
|
+
color: 'var(--theme-elevation-50)',
|
|
836
|
+
fontSize: 'var(--font-size-base)',
|
|
837
|
+
fontWeight: 500,
|
|
838
|
+
cursor: otpLoading || otp.length !== 6 ? 'not-allowed' : 'pointer',
|
|
839
|
+
opacity: otpLoading || otp.length !== 6 ? 0.7 : 1,
|
|
840
|
+
transition: 'opacity 150ms ease'
|
|
841
|
+
},
|
|
842
|
+
children: otpLoading ? 'Verifying...' : 'Verify'
|
|
843
|
+
})
|
|
844
|
+
]
|
|
845
|
+
}),
|
|
846
|
+
/*#__PURE__*/ _jsx("button", {
|
|
847
|
+
type: "button",
|
|
848
|
+
onClick: handleBackToLogin,
|
|
849
|
+
style: {
|
|
850
|
+
width: '100%',
|
|
851
|
+
marginTop: 'var(--base)',
|
|
852
|
+
padding: 'calc(var(--base) * 0.5)',
|
|
853
|
+
background: 'transparent',
|
|
854
|
+
border: 'none',
|
|
855
|
+
color: 'var(--theme-text)',
|
|
856
|
+
opacity: 0.7,
|
|
857
|
+
fontSize: 'var(--font-size-small)',
|
|
858
|
+
cursor: 'pointer'
|
|
859
|
+
},
|
|
860
|
+
children: "← Back to login"
|
|
861
|
+
})
|
|
862
|
+
]
|
|
863
|
+
})
|
|
864
|
+
});
|
|
865
|
+
}
|
|
595
866
|
// Registration view
|
|
596
867
|
if (viewMode === 'register') {
|
|
597
868
|
return /*#__PURE__*/ _jsx("div", {
|
|
@@ -1076,6 +1347,129 @@ export function LoginView({ authClient: providedClient, logo, title = 'Login', a
|
|
|
1076
1347
|
})
|
|
1077
1348
|
});
|
|
1078
1349
|
}
|
|
1350
|
+
// Magic-link sent confirmation view
|
|
1351
|
+
if (viewMode === 'magicLinkSent') {
|
|
1352
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
1353
|
+
style: {
|
|
1354
|
+
minHeight: '100vh',
|
|
1355
|
+
display: 'flex',
|
|
1356
|
+
alignItems: 'center',
|
|
1357
|
+
justifyContent: 'center',
|
|
1358
|
+
background: 'var(--theme-bg)',
|
|
1359
|
+
padding: 'var(--base)'
|
|
1360
|
+
},
|
|
1361
|
+
children: /*#__PURE__*/ _jsxs("div", {
|
|
1362
|
+
style: {
|
|
1363
|
+
background: 'var(--theme-elevation-50)',
|
|
1364
|
+
padding: 'calc(var(--base) * 2)',
|
|
1365
|
+
borderRadius: 'var(--style-radius-m)',
|
|
1366
|
+
boxShadow: '0 2px 20px rgba(0, 0, 0, 0.1)',
|
|
1367
|
+
width: '100%',
|
|
1368
|
+
maxWidth: '400px',
|
|
1369
|
+
textAlign: 'center'
|
|
1370
|
+
},
|
|
1371
|
+
children: [
|
|
1372
|
+
logo && /*#__PURE__*/ _jsx("div", {
|
|
1373
|
+
style: {
|
|
1374
|
+
marginBottom: 'calc(var(--base) * 1.5)'
|
|
1375
|
+
},
|
|
1376
|
+
children: logo
|
|
1377
|
+
}),
|
|
1378
|
+
/*#__PURE__*/ _jsx("div", {
|
|
1379
|
+
style: {
|
|
1380
|
+
width: '64px',
|
|
1381
|
+
height: '64px',
|
|
1382
|
+
background: 'var(--theme-success-100)',
|
|
1383
|
+
borderRadius: '50%',
|
|
1384
|
+
display: 'flex',
|
|
1385
|
+
alignItems: 'center',
|
|
1386
|
+
justifyContent: 'center',
|
|
1387
|
+
margin: '0 auto calc(var(--base) * 1.5)',
|
|
1388
|
+
fontSize: '28px'
|
|
1389
|
+
},
|
|
1390
|
+
children: "✉"
|
|
1391
|
+
}),
|
|
1392
|
+
/*#__PURE__*/ _jsx("h1", {
|
|
1393
|
+
style: {
|
|
1394
|
+
color: 'var(--theme-text)',
|
|
1395
|
+
fontSize: 'var(--font-size-h3)',
|
|
1396
|
+
fontWeight: 600,
|
|
1397
|
+
margin: '0 0 calc(var(--base) * 0.5) 0'
|
|
1398
|
+
},
|
|
1399
|
+
children: "Check Your Email"
|
|
1400
|
+
}),
|
|
1401
|
+
/*#__PURE__*/ _jsxs("p", {
|
|
1402
|
+
style: {
|
|
1403
|
+
color: 'var(--theme-text)',
|
|
1404
|
+
opacity: 0.7,
|
|
1405
|
+
fontSize: 'var(--font-size-small)',
|
|
1406
|
+
marginBottom: 'calc(var(--base) * 1.5)'
|
|
1407
|
+
},
|
|
1408
|
+
children: [
|
|
1409
|
+
"We've sent a sign-in link to ",
|
|
1410
|
+
/*#__PURE__*/ _jsx("strong", {
|
|
1411
|
+
children: email
|
|
1412
|
+
})
|
|
1413
|
+
]
|
|
1414
|
+
}),
|
|
1415
|
+
/*#__PURE__*/ _jsx("button", {
|
|
1416
|
+
type: "button",
|
|
1417
|
+
onClick: handleBackToLogin,
|
|
1418
|
+
style: {
|
|
1419
|
+
padding: 'calc(var(--base) * 0.75) calc(var(--base) * 1.5)',
|
|
1420
|
+
background: 'var(--theme-elevation-150)',
|
|
1421
|
+
border: 'none',
|
|
1422
|
+
borderRadius: 'var(--style-radius-s)',
|
|
1423
|
+
color: 'var(--theme-text)',
|
|
1424
|
+
fontSize: 'var(--font-size-base)',
|
|
1425
|
+
cursor: 'pointer'
|
|
1426
|
+
},
|
|
1427
|
+
children: "Back to login"
|
|
1428
|
+
})
|
|
1429
|
+
]
|
|
1430
|
+
})
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
// Resolve which methods are available and which owns the primary action
|
|
1434
|
+
const passwordAvailable = resolveAvailability(enablePassword, passwordProbe);
|
|
1435
|
+
const magicLinkAvailable = resolveAvailability(enableMagicLink, magicLinkProbe);
|
|
1436
|
+
const emailOtpAvailable = resolveAvailability(enableEmailOtp, emailOtpProbe);
|
|
1437
|
+
const primaryMethod = pickPrimaryMethod({
|
|
1438
|
+
password: passwordAvailable,
|
|
1439
|
+
magicLink: magicLinkAvailable,
|
|
1440
|
+
emailOtp: emailOtpAvailable
|
|
1441
|
+
});
|
|
1442
|
+
const primarySubmit = primaryMethod === 'magicLink' ? handleSendMagicLink : primaryMethod === 'emailOtp' ? handleSendEmailOtp : handleSubmit;
|
|
1443
|
+
const primaryLabel = loading ? primaryMethod === 'password' ? 'Signing in...' : 'Sending...' : primaryMethod === 'magicLink' ? 'Email me a link' : primaryMethod === 'emailOtp' ? 'Email me a code' : 'Sign In';
|
|
1444
|
+
// Secondary methods shown under the "or" divider (available but not the primary)
|
|
1445
|
+
const secondaryMethods = [];
|
|
1446
|
+
if (passkeyAvailable) {
|
|
1447
|
+
secondaryMethods.push({
|
|
1448
|
+
key: 'passkey',
|
|
1449
|
+
icon: '🔐',
|
|
1450
|
+
label: passkeyLoading ? 'Authenticating...' : 'Sign in with Passkey',
|
|
1451
|
+
onClick: handlePasskeySignIn,
|
|
1452
|
+
busy: passkeyLoading
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
if (magicLinkAvailable && primaryMethod !== 'magicLink') {
|
|
1456
|
+
secondaryMethods.push({
|
|
1457
|
+
key: 'magicLink',
|
|
1458
|
+
icon: '✉',
|
|
1459
|
+
label: 'Email me a link',
|
|
1460
|
+
onClick: ()=>handleSendMagicLink(),
|
|
1461
|
+
busy: false
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
if (emailOtpAvailable && primaryMethod !== 'emailOtp') {
|
|
1465
|
+
secondaryMethods.push({
|
|
1466
|
+
key: 'emailOtp',
|
|
1467
|
+
icon: '#️⃣',
|
|
1468
|
+
label: 'Email me a code',
|
|
1469
|
+
onClick: ()=>handleSendEmailOtp(),
|
|
1470
|
+
busy: false
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1079
1473
|
// Main login view
|
|
1080
1474
|
return /*#__PURE__*/ _jsx("div", {
|
|
1081
1475
|
style: {
|
|
@@ -1126,7 +1520,7 @@ export function LoginView({ authClient: providedClient, logo, title = 'Login', a
|
|
|
1126
1520
|
children: successMessage
|
|
1127
1521
|
}),
|
|
1128
1522
|
/*#__PURE__*/ _jsxs("form", {
|
|
1129
|
-
onSubmit:
|
|
1523
|
+
onSubmit: primarySubmit,
|
|
1130
1524
|
children: [
|
|
1131
1525
|
/*#__PURE__*/ _jsxs("div", {
|
|
1132
1526
|
style: {
|
|
@@ -1165,64 +1559,68 @@ export function LoginView({ authClient: providedClient, logo, title = 'Login', a
|
|
|
1165
1559
|
})
|
|
1166
1560
|
]
|
|
1167
1561
|
}),
|
|
1168
|
-
/*#__PURE__*/ _jsxs(
|
|
1169
|
-
style: {
|
|
1170
|
-
marginBottom: 'var(--base)'
|
|
1171
|
-
},
|
|
1562
|
+
passwordAvailable && /*#__PURE__*/ _jsxs(_Fragment, {
|
|
1172
1563
|
children: [
|
|
1173
|
-
/*#__PURE__*/
|
|
1174
|
-
htmlFor: "password",
|
|
1564
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
1175
1565
|
style: {
|
|
1176
|
-
|
|
1177
|
-
color: 'var(--theme-text)',
|
|
1178
|
-
marginBottom: 'calc(var(--base) * 0.5)',
|
|
1179
|
-
fontSize: 'var(--font-size-small)',
|
|
1180
|
-
fontWeight: 500
|
|
1566
|
+
marginBottom: 'var(--base)'
|
|
1181
1567
|
},
|
|
1182
|
-
children:
|
|
1568
|
+
children: [
|
|
1569
|
+
/*#__PURE__*/ _jsx("label", {
|
|
1570
|
+
htmlFor: "password",
|
|
1571
|
+
style: {
|
|
1572
|
+
display: 'block',
|
|
1573
|
+
color: 'var(--theme-text)',
|
|
1574
|
+
marginBottom: 'calc(var(--base) * 0.5)',
|
|
1575
|
+
fontSize: 'var(--font-size-small)',
|
|
1576
|
+
fontWeight: 500
|
|
1577
|
+
},
|
|
1578
|
+
children: "Password"
|
|
1579
|
+
}),
|
|
1580
|
+
/*#__PURE__*/ _jsx("input", {
|
|
1581
|
+
id: "password",
|
|
1582
|
+
type: "password",
|
|
1583
|
+
value: password,
|
|
1584
|
+
onChange: (e)=>setPassword(e.target.value),
|
|
1585
|
+
required: true,
|
|
1586
|
+
autoComplete: "current-password",
|
|
1587
|
+
style: {
|
|
1588
|
+
width: '100%',
|
|
1589
|
+
padding: 'calc(var(--base) * 0.75)',
|
|
1590
|
+
background: 'var(--theme-input-bg)',
|
|
1591
|
+
border: '1px solid var(--theme-elevation-150)',
|
|
1592
|
+
borderRadius: 'var(--style-radius-s)',
|
|
1593
|
+
color: 'var(--theme-text)',
|
|
1594
|
+
fontSize: 'var(--font-size-base)',
|
|
1595
|
+
outline: 'none',
|
|
1596
|
+
boxSizing: 'border-box'
|
|
1597
|
+
}
|
|
1598
|
+
})
|
|
1599
|
+
]
|
|
1183
1600
|
}),
|
|
1184
|
-
/*#__PURE__*/ _jsx("
|
|
1185
|
-
id: "password",
|
|
1186
|
-
type: "password",
|
|
1187
|
-
value: password,
|
|
1188
|
-
onChange: (e)=>setPassword(e.target.value),
|
|
1189
|
-
required: true,
|
|
1190
|
-
autoComplete: "current-password",
|
|
1601
|
+
forgotPasswordAvailable && /*#__PURE__*/ _jsx("div", {
|
|
1191
1602
|
style: {
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1603
|
+
marginBottom: 'calc(var(--base) * 1.5)',
|
|
1604
|
+
textAlign: 'right'
|
|
1605
|
+
},
|
|
1606
|
+
children: /*#__PURE__*/ _jsx("button", {
|
|
1607
|
+
type: "button",
|
|
1608
|
+
onClick: ()=>switchView('forgotPassword'),
|
|
1609
|
+
style: {
|
|
1610
|
+
background: 'none',
|
|
1611
|
+
border: 'none',
|
|
1612
|
+
color: 'var(--theme-text)',
|
|
1613
|
+
opacity: 0.7,
|
|
1614
|
+
cursor: 'pointer',
|
|
1615
|
+
fontSize: 'var(--font-size-small)',
|
|
1616
|
+
padding: 0,
|
|
1617
|
+
textDecoration: 'underline'
|
|
1618
|
+
},
|
|
1619
|
+
children: "Forgot password?"
|
|
1620
|
+
})
|
|
1202
1621
|
})
|
|
1203
1622
|
]
|
|
1204
1623
|
}),
|
|
1205
|
-
forgotPasswordAvailable && /*#__PURE__*/ _jsx("div", {
|
|
1206
|
-
style: {
|
|
1207
|
-
marginBottom: 'calc(var(--base) * 1.5)',
|
|
1208
|
-
textAlign: 'right'
|
|
1209
|
-
},
|
|
1210
|
-
children: /*#__PURE__*/ _jsx("button", {
|
|
1211
|
-
type: "button",
|
|
1212
|
-
onClick: ()=>switchView('forgotPassword'),
|
|
1213
|
-
style: {
|
|
1214
|
-
background: 'none',
|
|
1215
|
-
border: 'none',
|
|
1216
|
-
color: 'var(--theme-text)',
|
|
1217
|
-
opacity: 0.7,
|
|
1218
|
-
cursor: 'pointer',
|
|
1219
|
-
fontSize: 'var(--font-size-small)',
|
|
1220
|
-
padding: 0,
|
|
1221
|
-
textDecoration: 'underline'
|
|
1222
|
-
},
|
|
1223
|
-
children: "Forgot password?"
|
|
1224
|
-
})
|
|
1225
|
-
}),
|
|
1226
1624
|
error && /*#__PURE__*/ _jsx("div", {
|
|
1227
1625
|
style: {
|
|
1228
1626
|
color: 'var(--theme-error-500)',
|
|
@@ -1251,11 +1649,11 @@ export function LoginView({ authClient: providedClient, logo, title = 'Login', a
|
|
|
1251
1649
|
opacity: loading || passkeyLoading ? 0.7 : 1,
|
|
1252
1650
|
transition: 'opacity 150ms ease'
|
|
1253
1651
|
},
|
|
1254
|
-
children:
|
|
1652
|
+
children: primaryLabel
|
|
1255
1653
|
})
|
|
1256
1654
|
]
|
|
1257
1655
|
}),
|
|
1258
|
-
|
|
1656
|
+
secondaryMethods.length > 0 && /*#__PURE__*/ _jsxs(_Fragment, {
|
|
1259
1657
|
children: [
|
|
1260
1658
|
/*#__PURE__*/ _jsxs("div", {
|
|
1261
1659
|
style: {
|
|
@@ -1289,39 +1687,50 @@ export function LoginView({ authClient: providedClient, logo, title = 'Login', a
|
|
|
1289
1687
|
})
|
|
1290
1688
|
]
|
|
1291
1689
|
}),
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1690
|
+
secondaryMethods.map((method)=>/*#__PURE__*/ _jsxs("button", {
|
|
1691
|
+
type: "button",
|
|
1692
|
+
onClick: method.onClick,
|
|
1693
|
+
disabled: loading || passkeyLoading || method.busy,
|
|
1694
|
+
style: {
|
|
1695
|
+
width: '100%',
|
|
1696
|
+
marginBottom: 'calc(var(--base) * 0.5)',
|
|
1697
|
+
padding: 'calc(var(--base) * 0.75)',
|
|
1698
|
+
background: 'transparent',
|
|
1699
|
+
border: '1px solid var(--theme-elevation-300)',
|
|
1700
|
+
borderRadius: 'var(--style-radius-s)',
|
|
1701
|
+
color: 'var(--theme-text)',
|
|
1702
|
+
fontSize: 'var(--font-size-base)',
|
|
1703
|
+
fontWeight: 500,
|
|
1704
|
+
cursor: loading || passkeyLoading || method.busy ? 'not-allowed' : 'pointer',
|
|
1705
|
+
opacity: loading || passkeyLoading || method.busy ? 0.7 : 1,
|
|
1706
|
+
transition: 'opacity 150ms ease',
|
|
1707
|
+
display: 'flex',
|
|
1708
|
+
alignItems: 'center',
|
|
1709
|
+
justifyContent: 'center',
|
|
1710
|
+
gap: 'calc(var(--base) * 0.5)'
|
|
1711
|
+
},
|
|
1712
|
+
children: [
|
|
1713
|
+
/*#__PURE__*/ _jsx("span", {
|
|
1714
|
+
style: {
|
|
1715
|
+
fontSize: '18px'
|
|
1716
|
+
},
|
|
1717
|
+
children: method.icon
|
|
1718
|
+
}),
|
|
1719
|
+
method.label
|
|
1720
|
+
]
|
|
1721
|
+
}, method.key))
|
|
1323
1722
|
]
|
|
1324
1723
|
}),
|
|
1724
|
+
primaryMethod === null && secondaryMethods.length === 0 && /*#__PURE__*/ _jsx("p", {
|
|
1725
|
+
style: {
|
|
1726
|
+
marginTop: 'var(--base)',
|
|
1727
|
+
textAlign: 'center',
|
|
1728
|
+
fontSize: 'var(--font-size-small)',
|
|
1729
|
+
color: 'var(--theme-text)',
|
|
1730
|
+
opacity: 0.7
|
|
1731
|
+
},
|
|
1732
|
+
children: "No sign-in methods are currently enabled."
|
|
1733
|
+
}),
|
|
1325
1734
|
signUpAvailable && /*#__PURE__*/ _jsxs("div", {
|
|
1326
1735
|
style: {
|
|
1327
1736
|
marginTop: 'calc(var(--base) * 1.5)',
|
|
@@ -18,6 +18,10 @@ import { LoginView } from './LoginView.js';
|
|
|
18
18
|
defaultSignUpRole: loginConfig.defaultSignUpRole,
|
|
19
19
|
enableForgotPassword: loginConfig.enableForgotPassword,
|
|
20
20
|
resetPasswordUrl: loginConfig.resetPasswordUrl,
|
|
21
|
+
enablePassword: loginConfig.enablePassword,
|
|
22
|
+
enableMagicLink: loginConfig.enableMagicLink,
|
|
23
|
+
enableEmailOtp: loginConfig.enableEmailOtp,
|
|
24
|
+
magicLinkCallbackURL: loginConfig.magicLinkCallbackURL,
|
|
21
25
|
title: loginConfig.title
|
|
22
26
|
});
|
|
23
27
|
}
|
package/dist/exports/client.js
CHANGED
|
@@ -53,6 +53,10 @@ export { twoFactorClient } from 'better-auth/client/plugins';
|
|
|
53
53
|
* })
|
|
54
54
|
* ```
|
|
55
55
|
*/ export function createPayloadAuthClient(options) {
|
|
56
|
+
// `payloadAuthPlugins` is intentionally widened to `BetterAuthClientPlugin[]` for
|
|
57
|
+
// declaration-emit portability. That widening makes the inferred return type drop
|
|
58
|
+
// some base-client methods (e.g. `refreshToken`, present at runtime) relative to the
|
|
59
|
+
// zero-arg `ReturnType<typeof createAuthClient>`, so we cast back to the stable type.
|
|
56
60
|
return createAuthClient({
|
|
57
61
|
baseURL: options?.baseURL ?? (typeof window !== 'undefined' ? window.location.origin : ''),
|
|
58
62
|
plugins: [
|
package/dist/plugin/index.d.ts
CHANGED
|
@@ -69,6 +69,36 @@ export type BetterAuthPluginAdminOptions = {
|
|
|
69
69
|
* instead of showing the inline password reset form.
|
|
70
70
|
*/
|
|
71
71
|
resetPasswordUrl?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Enable email + password sign-in.
|
|
74
|
+
* - true: Always show the password field
|
|
75
|
+
* - false: Hide the password field (passwordless-only)
|
|
76
|
+
* - 'auto': Auto-detect via the /sign-in/email endpoint
|
|
77
|
+
* Default: 'auto' - LoginView hides the password field when the email/password
|
|
78
|
+
* strategy is disabled in Better Auth.
|
|
79
|
+
*/
|
|
80
|
+
enablePassword?: boolean | 'auto';
|
|
81
|
+
/**
|
|
82
|
+
* Enable magic-link sign-in ("email me a link").
|
|
83
|
+
* - true: Always show the magic-link option
|
|
84
|
+
* - false: Never show it
|
|
85
|
+
* - 'auto': Auto-detect via the /sign-in/magic-link endpoint
|
|
86
|
+
* Default: 'auto' - requires the Better Auth magicLink() plugin.
|
|
87
|
+
*/
|
|
88
|
+
enableMagicLink?: boolean | 'auto';
|
|
89
|
+
/**
|
|
90
|
+
* Enable email-OTP sign-in ("email me a code").
|
|
91
|
+
* - true: Always show the email-OTP option
|
|
92
|
+
* - false: Never show it
|
|
93
|
+
* - 'auto': Auto-detect via the /email-otp/send-verification-otp endpoint
|
|
94
|
+
* Default: 'auto' - requires the Better Auth emailOTP() plugin.
|
|
95
|
+
*/
|
|
96
|
+
enableEmailOtp?: boolean | 'auto';
|
|
97
|
+
/**
|
|
98
|
+
* Where the emailed magic link returns after verification.
|
|
99
|
+
* Default: afterLoginPath
|
|
100
|
+
*/
|
|
101
|
+
magicLinkCallbackURL?: string;
|
|
72
102
|
};
|
|
73
103
|
/** Path to custom logout button component (import map format) */
|
|
74
104
|
logoutButtonComponent?: string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for deciding which sign-in methods the LoginView should display.
|
|
3
|
+
* Kept DOM-free so they can be unit-tested under the project's node-env vitest setup.
|
|
4
|
+
*/
|
|
5
|
+
/** A method-enable setting: explicit boolean, or 'auto' to defer to an endpoint probe. */
|
|
6
|
+
export type MethodSetting = boolean | 'auto';
|
|
7
|
+
/**
|
|
8
|
+
* Resolve a `boolean | 'auto'` setting against the result of an endpoint probe.
|
|
9
|
+
*
|
|
10
|
+
* - `true` -> always available
|
|
11
|
+
* - `false` -> never available
|
|
12
|
+
* - `'auto'` -> available iff the probe succeeded (`probeOk === true`); a `null`
|
|
13
|
+
* probe (not yet completed) resolves to `false`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveAvailability(setting: MethodSetting, probeOk: boolean | null): boolean;
|
|
16
|
+
/** The sign-in methods that can own the primary submit button. */
|
|
17
|
+
export type PrimaryMethod = 'password' | 'magicLink' | 'emailOtp';
|
|
18
|
+
/**
|
|
19
|
+
* Choose which available method owns the primary submit button.
|
|
20
|
+
* Precedence: password -> magicLink -> emailOtp. Returns null if none are available.
|
|
21
|
+
*/
|
|
22
|
+
export declare function pickPrimaryMethod(available: {
|
|
23
|
+
password: boolean;
|
|
24
|
+
magicLink: boolean;
|
|
25
|
+
emailOtp: boolean;
|
|
26
|
+
}): PrimaryMethod | null;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for deciding which sign-in methods the LoginView should display.
|
|
3
|
+
* Kept DOM-free so they can be unit-tested under the project's node-env vitest setup.
|
|
4
|
+
*/ /** A method-enable setting: explicit boolean, or 'auto' to defer to an endpoint probe. */ /**
|
|
5
|
+
* Resolve a `boolean | 'auto'` setting against the result of an endpoint probe.
|
|
6
|
+
*
|
|
7
|
+
* - `true` -> always available
|
|
8
|
+
* - `false` -> never available
|
|
9
|
+
* - `'auto'` -> available iff the probe succeeded (`probeOk === true`); a `null`
|
|
10
|
+
* probe (not yet completed) resolves to `false`.
|
|
11
|
+
*/ export function resolveAvailability(setting, probeOk) {
|
|
12
|
+
if (setting === true) return true;
|
|
13
|
+
if (setting === false) return false;
|
|
14
|
+
return probeOk === true;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Choose which available method owns the primary submit button.
|
|
18
|
+
* Precedence: password -> magicLink -> emailOtp. Returns null if none are available.
|
|
19
|
+
*/ export function pickPrimaryMethod(available) {
|
|
20
|
+
if (available.password) return 'password';
|
|
21
|
+
if (available.magicLink) return 'magicLink';
|
|
22
|
+
if (available.emailOtp) return 'emailOtp';
|
|
23
|
+
return null;
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delmaredigital/payload-better-auth",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"description": "Better Auth adapter and plugins for Payload CMS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -113,23 +113,24 @@
|
|
|
113
113
|
}
|
|
114
114
|
},
|
|
115
115
|
"devDependencies": {
|
|
116
|
-
"@better-auth/api-key": "^1.6.
|
|
117
|
-
"@better-auth/oauth-provider": "^1.6.
|
|
118
|
-
"@better-auth/passkey": "^1.6.
|
|
119
|
-
"@payloadcms/next": "^3.
|
|
120
|
-
"@payloadcms/ui": "^3.
|
|
116
|
+
"@better-auth/api-key": "^1.6.18",
|
|
117
|
+
"@better-auth/oauth-provider": "^1.6.18",
|
|
118
|
+
"@better-auth/passkey": "^1.6.18",
|
|
119
|
+
"@payloadcms/next": "^3.85.0",
|
|
120
|
+
"@payloadcms/ui": "^3.85.0",
|
|
121
121
|
"@swc/cli": "^0.6.0",
|
|
122
122
|
"@swc/core": "^1.15.30",
|
|
123
123
|
"@types/node": "^24.12.2",
|
|
124
124
|
"@types/react": "^19.2.14",
|
|
125
|
-
"@vitest/coverage-v8": "^
|
|
126
|
-
"better-auth": "^1.6.
|
|
125
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
126
|
+
"better-auth": "^1.6.18",
|
|
127
127
|
"next": "^16.2.5",
|
|
128
|
-
"payload": "^3.
|
|
128
|
+
"payload": "^3.85.0",
|
|
129
129
|
"react": "^19.2.5",
|
|
130
130
|
"tsx": "^4.21.0",
|
|
131
131
|
"typescript": "^5.9.3",
|
|
132
|
-
"
|
|
132
|
+
"vite": "^7.3.5",
|
|
133
|
+
"vitest": "^4.1.8"
|
|
133
134
|
},
|
|
134
135
|
"keywords": [
|
|
135
136
|
"payload",
|