@delmaredigital/payload-better-auth 0.7.5 → 0.7.7
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.js +14 -106
- package/dist/components/LoginViewWrapper.d.ts +9 -2
- package/dist/components/LoginViewWrapper.js +33 -8
- package/dist/exports/client.js +4 -0
- package/dist/utils/loginMethods.d.ts +35 -0
- package/dist/utils/loginMethods.js +22 -0
- package/package.json +5 -5
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
|
|
|
@@ -25,7 +25,7 @@ import { useConfig } from '@payloadcms/ui';
|
|
|
25
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 }) {
|
|
26
26
|
const router = useRouter();
|
|
27
27
|
// Payload Config
|
|
28
|
-
const { config: { routes: { admin: adminRoute
|
|
28
|
+
const { config: { routes: { admin: adminRoute } } } = useConfig();
|
|
29
29
|
// View state
|
|
30
30
|
const [viewMode, setViewMode] = useState('login');
|
|
31
31
|
// Form fields
|
|
@@ -40,16 +40,18 @@ export function LoginView({ authClient: providedClient, logo, title = 'Login', a
|
|
|
40
40
|
const [passkeyLoading, setPasskeyLoading] = useState(false);
|
|
41
41
|
const [checkingSession, setCheckingSession] = useState(true);
|
|
42
42
|
const [accessDenied, setAccessDenied] = useState(false);
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
43
|
+
// Which methods to show. LoginViewWrapper resolves these server-side from the
|
|
44
|
+
// Better Auth instance's options and passes concrete booleans. For standalone
|
|
45
|
+
// <LoginView> use, an unresolved 'auto' falls back to safe defaults: password
|
|
46
|
+
// shown, optional methods hidden. We no longer probe endpoints — Better Auth
|
|
47
|
+
// answers every OPTIONS request with 200 (CORS preflight), so probing could
|
|
48
|
+
// never tell whether a method was actually enabled.
|
|
49
|
+
const passwordAvailable = resolveAvailability(enablePassword, true);
|
|
50
|
+
const passkeyAvailable = resolveAvailability(enablePasskey, false);
|
|
51
|
+
const signUpAvailable = resolveAvailability(enableSignUp, false);
|
|
52
|
+
const forgotPasswordAvailable = resolveAvailability(enableForgotPassword, false);
|
|
53
|
+
const magicLinkAvailable = resolveAvailability(enableMagicLink, false);
|
|
54
|
+
const emailOtpAvailable = resolveAvailability(enableEmailOtp, false);
|
|
53
55
|
// Email-OTP code entry state
|
|
54
56
|
const [otp, setOtp] = useState('');
|
|
55
57
|
const [otpLoading, setOtpLoading] = useState(false);
|
|
@@ -101,97 +103,6 @@ export function LoginView({ authClient: providedClient, logo, title = 'Login', a
|
|
|
101
103
|
requireAllRoles,
|
|
102
104
|
router
|
|
103
105
|
]);
|
|
104
|
-
// Auto-detect passkey availability if set to 'auto'
|
|
105
|
-
useEffect(()=>{
|
|
106
|
-
if (enablePasskey === 'auto') {
|
|
107
|
-
// Check if passkey endpoint exists (GET request)
|
|
108
|
-
// Better Auth passkey routes are at /passkey/* (singular)
|
|
109
|
-
fetch(`${apiRoute}/auth/passkey/generate-authenticate-options`, {
|
|
110
|
-
method: 'GET',
|
|
111
|
-
credentials: 'include'
|
|
112
|
-
}).then((res)=>{
|
|
113
|
-
// If we get a response (even 400/401 for not authenticated), passkey is available
|
|
114
|
-
// 404 means passkey plugin is not installed
|
|
115
|
-
setPasskeyAvailable(res.status !== 404);
|
|
116
|
-
}).catch(()=>{
|
|
117
|
-
setPasskeyAvailable(false);
|
|
118
|
-
});
|
|
119
|
-
} else {
|
|
120
|
-
setPasskeyAvailable(enablePasskey === true);
|
|
121
|
-
}
|
|
122
|
-
}, [
|
|
123
|
-
enablePasskey
|
|
124
|
-
]);
|
|
125
|
-
// Auto-detect sign up availability if set to 'auto'
|
|
126
|
-
useEffect(()=>{
|
|
127
|
-
if (enableSignUp === 'auto') {
|
|
128
|
-
// Check if sign-up endpoint exists
|
|
129
|
-
fetch(`${apiRoute}/auth/sign-up/email`, {
|
|
130
|
-
method: 'OPTIONS',
|
|
131
|
-
credentials: 'include'
|
|
132
|
-
}).then((res)=>{
|
|
133
|
-
// 404 means sign-up is not available
|
|
134
|
-
setSignUpAvailable(res.status !== 404);
|
|
135
|
-
}).catch(()=>{
|
|
136
|
-
// If OPTIONS fails, try a HEAD or just assume it's available since it's a core endpoint
|
|
137
|
-
setSignUpAvailable(true);
|
|
138
|
-
});
|
|
139
|
-
} else {
|
|
140
|
-
setSignUpAvailable(enableSignUp === true);
|
|
141
|
-
}
|
|
142
|
-
}, [
|
|
143
|
-
enableSignUp
|
|
144
|
-
]);
|
|
145
|
-
// Auto-detect forgot password availability if set to 'auto'
|
|
146
|
-
useEffect(()=>{
|
|
147
|
-
if (enableForgotPassword === 'auto') {
|
|
148
|
-
// Check if request-password-reset endpoint exists
|
|
149
|
-
fetch(`${apiRoute}/auth/request-password-reset`, {
|
|
150
|
-
method: 'OPTIONS',
|
|
151
|
-
credentials: 'include'
|
|
152
|
-
}).then((res)=>{
|
|
153
|
-
// 404 means request-password-reset is not available
|
|
154
|
-
setForgotPasswordAvailable(res.status !== 404);
|
|
155
|
-
}).catch(()=>{
|
|
156
|
-
// If OPTIONS fails, assume it's available since it's a core endpoint
|
|
157
|
-
setForgotPasswordAvailable(true);
|
|
158
|
-
});
|
|
159
|
-
} else {
|
|
160
|
-
setForgotPasswordAvailable(enableForgotPassword === true);
|
|
161
|
-
}
|
|
162
|
-
}, [
|
|
163
|
-
enableForgotPassword
|
|
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
106
|
/**
|
|
196
107
|
* Shared post-authentication tail: re-fetch the session for complete user data
|
|
197
108
|
* (e.g. roles applied by hooks), enforce the role gate, and redirect on success.
|
|
@@ -1430,10 +1341,7 @@ export function LoginView({ authClient: providedClient, logo, title = 'Login', a
|
|
|
1430
1341
|
})
|
|
1431
1342
|
});
|
|
1432
1343
|
}
|
|
1433
|
-
//
|
|
1434
|
-
const passwordAvailable = resolveAvailability(enablePassword, passwordProbe);
|
|
1435
|
-
const magicLinkAvailable = resolveAvailability(enableMagicLink, magicLinkProbe);
|
|
1436
|
-
const emailOtpAvailable = resolveAvailability(enableEmailOtp, emailOtpProbe);
|
|
1344
|
+
// Which method owns the primary submit button (availability resolved above)
|
|
1437
1345
|
const primaryMethod = pickPrimaryMethod({
|
|
1438
1346
|
password: passwordAvailable,
|
|
1439
1347
|
magicLink: magicLinkAvailable,
|
|
@@ -2,8 +2,15 @@ import type { AdminViewProps } from 'payload';
|
|
|
2
2
|
type LoginViewWrapperProps = AdminViewProps;
|
|
3
3
|
/**
|
|
4
4
|
* Server component wrapper for LoginView.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* Reads login configuration from `payload.config.custom.betterAuth.login` and
|
|
7
|
+
* resolves each `'auto'` option against the Better Auth instance's resolved
|
|
8
|
+
* `options` (server-side), passing concrete booleans to the client LoginView.
|
|
9
|
+
*
|
|
10
|
+
* This replaces the old client-side `OPTIONS` endpoint probing: Better Auth
|
|
11
|
+
* answers every `OPTIONS` request with 200 (CORS preflight), so probing could
|
|
12
|
+
* never determine whether a method was actually enabled. The server `options`
|
|
13
|
+
* are authoritative.
|
|
7
14
|
*/
|
|
8
15
|
export declare function LoginViewWrapper({ initPageResult }: LoginViewWrapperProps): Promise<import("react").JSX.Element>;
|
|
9
16
|
export default LoginViewWrapper;
|
|
@@ -1,26 +1,51 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { LoginView } from './LoginView.js';
|
|
3
|
+
import { detectEnabledMethods, resolveAvailability } from '../utils/loginMethods.js';
|
|
4
|
+
/**
|
|
5
|
+
* Fallback used when the Better Auth instance isn't available on `payload` yet.
|
|
6
|
+
* Show password (so admins are never locked out) and hide the optional methods
|
|
7
|
+
* (so we never render a button for a plugin that may be absent).
|
|
8
|
+
*/ const FALLBACK_DETECTED = {
|
|
9
|
+
password: true,
|
|
10
|
+
signup: false,
|
|
11
|
+
forgotPassword: false,
|
|
12
|
+
passkey: false,
|
|
13
|
+
magicLink: false,
|
|
14
|
+
emailOtp: false
|
|
15
|
+
};
|
|
3
16
|
/**
|
|
4
17
|
* Server component wrapper for LoginView.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
18
|
+
*
|
|
19
|
+
* Reads login configuration from `payload.config.custom.betterAuth.login` and
|
|
20
|
+
* resolves each `'auto'` option against the Better Auth instance's resolved
|
|
21
|
+
* `options` (server-side), passing concrete booleans to the client LoginView.
|
|
22
|
+
*
|
|
23
|
+
* This replaces the old client-side `OPTIONS` endpoint probing: Better Auth
|
|
24
|
+
* answers every `OPTIONS` request with 200 (CORS preflight), so probing could
|
|
25
|
+
* never determine whether a method was actually enabled. The server `options`
|
|
26
|
+
* are authoritative.
|
|
7
27
|
*/ export async function LoginViewWrapper({ initPageResult }) {
|
|
8
28
|
const { req } = initPageResult;
|
|
9
29
|
const { payload } = req;
|
|
10
30
|
// Read login config from payload.config.custom.betterAuth.login
|
|
11
31
|
const loginConfig = payload.config.custom?.betterAuth?.login ?? {};
|
|
32
|
+
// Detect which methods Better Auth actually has enabled, from its resolved options.
|
|
33
|
+
const authOptions = payload.betterAuth?.options;
|
|
34
|
+
const detected = authOptions ? detectEnabledMethods(authOptions) : FALLBACK_DETECTED;
|
|
35
|
+
// Resolve each option: explicit boolean wins; 'auto' (or unset) uses detection.
|
|
36
|
+
const resolve = (setting, detectedValue)=>resolveAvailability(setting ?? 'auto', detectedValue);
|
|
12
37
|
return /*#__PURE__*/ _jsx(LoginView, {
|
|
13
38
|
afterLoginPath: loginConfig.afterLoginPath,
|
|
14
39
|
requiredRole: loginConfig.requiredRole,
|
|
15
40
|
requireAllRoles: loginConfig.requireAllRoles,
|
|
16
|
-
|
|
17
|
-
enableSignUp: loginConfig.enableSignUp,
|
|
41
|
+
enablePassword: resolve(loginConfig.enablePassword, detected.password),
|
|
42
|
+
enableSignUp: resolve(loginConfig.enableSignUp, detected.signup),
|
|
18
43
|
defaultSignUpRole: loginConfig.defaultSignUpRole,
|
|
19
|
-
enableForgotPassword: loginConfig.enableForgotPassword,
|
|
44
|
+
enableForgotPassword: resolve(loginConfig.enableForgotPassword, detected.forgotPassword),
|
|
45
|
+
enablePasskey: resolve(loginConfig.enablePasskey, detected.passkey),
|
|
46
|
+
enableMagicLink: resolve(loginConfig.enableMagicLink, detected.magicLink),
|
|
47
|
+
enableEmailOtp: resolve(loginConfig.enableEmailOtp, detected.emailOtp),
|
|
20
48
|
resetPasswordUrl: loginConfig.resetPasswordUrl,
|
|
21
|
-
enablePassword: loginConfig.enablePassword,
|
|
22
|
-
enableMagicLink: loginConfig.enableMagicLink,
|
|
23
|
-
enableEmailOtp: loginConfig.enableEmailOtp,
|
|
24
49
|
magicLinkCallbackURL: loginConfig.magicLinkCallbackURL,
|
|
25
50
|
title: loginConfig.title
|
|
26
51
|
});
|
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: [
|
|
@@ -24,3 +24,38 @@ export declare function pickPrimaryMethod(available: {
|
|
|
24
24
|
magicLink: boolean;
|
|
25
25
|
emailOtp: boolean;
|
|
26
26
|
}): PrimaryMethod | null;
|
|
27
|
+
/** Which sign-in methods a Better Auth instance actually has enabled. */
|
|
28
|
+
export interface DetectedMethods {
|
|
29
|
+
password: boolean;
|
|
30
|
+
signup: boolean;
|
|
31
|
+
forgotPassword: boolean;
|
|
32
|
+
passkey: boolean;
|
|
33
|
+
magicLink: boolean;
|
|
34
|
+
emailOtp: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Minimal structural shape of the Better Auth resolved options we read.
|
|
38
|
+
* Declared locally (not imported from better-auth) so this stays dependency-free
|
|
39
|
+
* and unit-testable.
|
|
40
|
+
*/
|
|
41
|
+
export interface AuthOptionsLike {
|
|
42
|
+
emailAndPassword?: {
|
|
43
|
+
enabled?: boolean;
|
|
44
|
+
disableSignUp?: boolean;
|
|
45
|
+
sendResetPassword?: unknown;
|
|
46
|
+
};
|
|
47
|
+
plugins?: Array<{
|
|
48
|
+
id?: string;
|
|
49
|
+
} | null | undefined>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Determine which sign-in methods are enabled from a Better Auth instance's
|
|
53
|
+
* resolved `options`. This is the authoritative, server-side replacement for the
|
|
54
|
+
* old client-side endpoint probing: Better Auth answers every `OPTIONS` request
|
|
55
|
+
* with 200 (CORS preflight), so probing `OPTIONS /sign-in/*` could never tell
|
|
56
|
+
* whether a method was actually enabled.
|
|
57
|
+
*
|
|
58
|
+
* `forgotPassword` requires a configured `sendResetPassword` callback, since the
|
|
59
|
+
* reset flow can't email a link without it.
|
|
60
|
+
*/
|
|
61
|
+
export declare function detectEnabledMethods(options: AuthOptionsLike | null | undefined): DetectedMethods;
|
|
@@ -22,3 +22,25 @@
|
|
|
22
22
|
if (available.emailOtp) return 'emailOtp';
|
|
23
23
|
return null;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Determine which sign-in methods are enabled from a Better Auth instance's
|
|
27
|
+
* resolved `options`. This is the authoritative, server-side replacement for the
|
|
28
|
+
* old client-side endpoint probing: Better Auth answers every `OPTIONS` request
|
|
29
|
+
* with 200 (CORS preflight), so probing `OPTIONS /sign-in/*` could never tell
|
|
30
|
+
* whether a method was actually enabled.
|
|
31
|
+
*
|
|
32
|
+
* `forgotPassword` requires a configured `sendResetPassword` callback, since the
|
|
33
|
+
* reset flow can't email a link without it.
|
|
34
|
+
*/ export function detectEnabledMethods(options) {
|
|
35
|
+
const ep = options?.emailAndPassword;
|
|
36
|
+
const password = !!ep?.enabled;
|
|
37
|
+
const pluginIds = new Set((options?.plugins ?? []).map((p)=>p?.id).filter((id)=>typeof id === 'string'));
|
|
38
|
+
return {
|
|
39
|
+
password,
|
|
40
|
+
signup: password && !ep?.disableSignUp,
|
|
41
|
+
forgotPassword: password && !!ep?.sendResetPassword,
|
|
42
|
+
passkey: pluginIds.has('passkey'),
|
|
43
|
+
magicLink: pluginIds.has('magic-link'),
|
|
44
|
+
emailOtp: pluginIds.has('email-otp')
|
|
45
|
+
};
|
|
46
|
+
}
|
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.7",
|
|
4
4
|
"description": "Better Auth adapter and plugins for Payload CMS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -113,9 +113,9 @@
|
|
|
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.
|
|
116
|
+
"@better-auth/api-key": "^1.6.18",
|
|
117
|
+
"@better-auth/oauth-provider": "^1.6.18",
|
|
118
|
+
"@better-auth/passkey": "^1.6.18",
|
|
119
119
|
"@payloadcms/next": "^3.85.0",
|
|
120
120
|
"@payloadcms/ui": "^3.85.0",
|
|
121
121
|
"@swc/cli": "^0.6.0",
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
"@types/node": "^24.12.2",
|
|
124
124
|
"@types/react": "^19.2.14",
|
|
125
125
|
"@vitest/coverage-v8": "^4.1.8",
|
|
126
|
-
"better-auth": "^1.6.
|
|
126
|
+
"better-auth": "^1.6.18",
|
|
127
127
|
"next": "^16.2.5",
|
|
128
128
|
"payload": "^3.85.0",
|
|
129
129
|
"react": "^19.2.5",
|