@delmaredigital/payload-better-auth 0.6.9 → 0.7.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 CHANGED
@@ -12,7 +12,13 @@ Better Auth adapter and plugins for Payload CMS. Enables seamless integration be
12
12
  <a href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fdelmaredigital%2Fdd-starter&project-name=my-payload-site&build-command=pnpm%20run%20ci&env=PAYLOAD_SECRET,BETTER_AUTH_SECRET&stores=%5B%7B%22type%22%3A%22integration%22%2C%22protocol%22%3A%22storage%22%2C%22productSlug%22%3A%22neon%22%2C%22integrationSlug%22%3A%22neon%22%7D%2C%7B%22type%22%3A%22blob%22%7D%5D"><img src="https://vercel.com/button" alt="Deploy with Vercel" height="32"></a>
13
13
  </p>
14
14
 
15
- > **Upgrading to 0.5?** This release requires Better Auth 1.5 and includes breaking changes to client plugins, API key imports, and auth instance types. See the [CHANGELOG](./CHANGELOG.md#054---2026-03-02) for migration instructions.
15
+ > ⚠️ **Upgrading to 0.7?** This release requires **Better Auth 1.6** and includes several breaking changes:
16
+ >
17
+ > - **Schema migration required** for projects using the `twoFactor` plugin — Better Auth 1.6.2 added a `verified` column to the `twoFactor` table.
18
+ > - **`oidcProvider` → `@better-auth/oauth-provider`** — generated OAuth types now reflect the `oauth-provider` schema. Consumers using `oidcProvider()` at runtime will keep working, but `OauthApplication` / `PluginId` / `ModelKey` type exports have changed shape. Migrating to `@better-auth/oauth-provider` is recommended.
19
+ > - **Client helper type widening** — `createPayloadAuthClient()` and `payloadAuthPlugins` are typed more conservatively to keep `.d.ts` portable. For typed plugin methods (e.g. `client.twoFactor.verifyTotp`), list plugins explicitly in `createAuthClient({ plugins: [...] })` (see [Client-Side Auth](#4-client-side-auth) below).
20
+ >
21
+ > See the [CHANGELOG](./CHANGELOG.md#070---2026-04-21) for full migration instructions.
16
22
 
17
23
  ---
18
24
 
@@ -30,7 +36,7 @@ For AI-assisted exploration: [DeepWiki](https://deepwiki.com/delmaredigital/payl
30
36
  pnpm add @delmaredigital/payload-better-auth better-auth
31
37
  ```
32
38
 
33
- **Requirements:** `payload` >= 3.69.0 · `better-auth` >= 1.5.0 · `next` >= 15.4.8 · `react` >= 19.2.1
39
+ **Requirements:** `payload` >= 3.69.0 · `better-auth` >= 1.6.0 · `next` >= 15.4.8 · `react` >= 19.2.1
34
40
 
35
41
  ## Quick Start
36
42
 
@@ -138,13 +144,18 @@ export default buildConfig({
138
144
  // src/lib/auth/client.ts
139
145
  'use client'
140
146
 
141
- import { createPayloadAuthClient } from '@delmaredigital/payload-better-auth/client'
147
+ import { createAuthClient, twoFactorClient } from '@delmaredigital/payload-better-auth/client'
148
+ import { passkeyClient } from '@better-auth/passkey/client'
142
149
 
143
- export const authClient = createPayloadAuthClient()
150
+ export const authClient = createAuthClient({
151
+ plugins: [twoFactorClient(), passkeyClient()],
152
+ })
144
153
 
145
154
  export const { useSession, signIn, signUp, signOut, twoFactor, passkey } = authClient
146
155
  ```
147
156
 
157
+ > Listing plugins inline (rather than using `createPayloadAuthClient()` or spreading `payloadAuthPlugins`) ensures `twoFactor` and other plugin methods are typed on the returned client.
158
+
148
159
  ### 5. Server-Side Session
149
160
 
150
161
  ```ts
@@ -1,10 +1,10 @@
1
1
  export type BeforeLoginProps = {
2
- /** URL to redirect to for login. Default: '/admin/login' */
2
+ /** URL to redirect to for login. Defaults to `${routes.admin}/login`. */
3
3
  loginUrl?: string;
4
4
  };
5
5
  /**
6
6
  * BeforeLogin component that redirects to the custom login page.
7
7
  * Injected into Payload's beforeLogin slot to intercept default login.
8
8
  */
9
- export declare function BeforeLogin({ loginUrl }: BeforeLoginProps): import("react").JSX.Element;
9
+ export declare function BeforeLogin({ loginUrl }?: BeforeLoginProps): import("react").JSX.Element;
10
10
  export default BeforeLogin;
@@ -2,16 +2,19 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useEffect } from 'react';
4
4
  import { useRouter } from 'next/navigation.js';
5
+ import { useConfig } from '@payloadcms/ui';
5
6
  /**
6
7
  * BeforeLogin component that redirects to the custom login page.
7
8
  * Injected into Payload's beforeLogin slot to intercept default login.
8
- */ export function BeforeLogin({ loginUrl = '/admin/login' }) {
9
+ */ export function BeforeLogin({ loginUrl } = {}) {
9
10
  const router = useRouter();
11
+ const { config: { routes: { admin: adminRoute } } } = useConfig();
12
+ const target = loginUrl ?? `${adminRoute}/login`;
10
13
  useEffect(()=>{
11
- router.replace(loginUrl);
14
+ router.replace(target);
12
15
  }, [
13
16
  router,
14
- loginUrl
17
+ target
15
18
  ]);
16
19
  // Show loading state while redirecting
17
20
  return /*#__PURE__*/ _jsx("div", {
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation.js';
5
5
  import { createAuthClient } from 'better-auth/react';
6
6
  import { twoFactorClient } from 'better-auth/client/plugins';
7
7
  import { hasAnyRole, hasAllRoles } from '../utils/access.js';
8
+ import { useConfig } from '@payloadcms/ui';
8
9
  /**
9
10
  * Check if user has the required role(s)
10
11
  */ function checkUserRoles(user, requiredRole, requireAllRoles) {
@@ -22,6 +23,8 @@ import { hasAnyRole, hasAllRoles } from '../utils/access.js';
22
23
  }
23
24
  export function LoginView({ authClient: providedClient, logo, title = 'Login', afterLoginPath = '/admin', requiredRole = 'admin', requireAllRoles = false, enablePasskey = 'auto', enableSignUp = 'auto', defaultSignUpRole = 'user', enableForgotPassword = 'auto', resetPasswordUrl }) {
24
25
  const router = useRouter();
26
+ // Payload Config
27
+ const { config: { routes: { admin: adminRoute, api: apiRoute } } } = useConfig();
25
28
  // View state
26
29
  const [viewMode, setViewMode] = useState('login');
27
30
  // Form fields
@@ -91,7 +94,7 @@ export function LoginView({ authClient: providedClient, logo, title = 'Login', a
91
94
  if (enablePasskey === 'auto') {
92
95
  // Check if passkey endpoint exists (GET request)
93
96
  // Better Auth passkey routes are at /passkey/* (singular)
94
- fetch('/api/auth/passkey/generate-authenticate-options', {
97
+ fetch(`${apiRoute}/auth/passkey/generate-authenticate-options`, {
95
98
  method: 'GET',
96
99
  credentials: 'include'
97
100
  }).then((res)=>{
@@ -111,7 +114,7 @@ export function LoginView({ authClient: providedClient, logo, title = 'Login', a
111
114
  useEffect(()=>{
112
115
  if (enableSignUp === 'auto') {
113
116
  // Check if sign-up endpoint exists
114
- fetch('/api/auth/sign-up/email', {
117
+ fetch(`${apiRoute}/auth/sign-up/email`, {
115
118
  method: 'OPTIONS',
116
119
  credentials: 'include'
117
120
  }).then((res)=>{
@@ -131,7 +134,7 @@ export function LoginView({ authClient: providedClient, logo, title = 'Login', a
131
134
  useEffect(()=>{
132
135
  if (enableForgotPassword === 'auto') {
133
136
  // Check if request-password-reset endpoint exists
134
- fetch('/api/auth/request-password-reset', {
137
+ fetch(`${apiRoute}/auth/request-password-reset`, {
135
138
  method: 'OPTIONS',
136
139
  credentials: 'include'
137
140
  }).then((res)=>{
@@ -254,7 +257,7 @@ export function LoginView({ authClient: providedClient, logo, title = 'Login', a
254
257
  const client = await getClient();
255
258
  const result = await client.requestPasswordReset({
256
259
  email,
257
- redirectTo: resetPasswordUrl ?? `${window.location.origin}/admin/reset-password`
260
+ redirectTo: resetPasswordUrl ?? `${window.location.origin}${adminRoute}/reset-password`
258
261
  });
259
262
  if (result.error) {
260
263
  setError(result.error.message ?? 'Failed to send reset email');
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useState } from 'react';
4
4
  import { useRouter } from 'next/navigation.js';
5
+ import { useConfig } from '@payloadcms/ui';
5
6
  /**
6
7
  * Logout button component styled to match Payload's admin nav.
7
8
  * Uses Payload's CSS classes and variables for native theme integration.
@@ -10,6 +11,8 @@ import { useRouter } from 'next/navigation.js';
10
11
  * clean state when switching between users.
11
12
  */ export function LogoutButton() {
12
13
  const router = useRouter();
14
+ // Payload Config
15
+ const { config: { routes: { admin: adminRoute, api: apiRoute } } } = useConfig();
13
16
  const [isLoading, setIsLoading] = useState(false);
14
17
  async function handleLogout() {
15
18
  if (isLoading) return;
@@ -19,7 +22,7 @@ import { useRouter } from 'next/navigation.js';
19
22
  // - Better Auth: clears BA session cookie
20
23
  // - Payload: clears JWT cookie (payload-token) so useAuth() resets
21
24
  await Promise.allSettled([
22
- fetch('/api/auth/sign-out', {
25
+ fetch(`${apiRoute}/auth/sign-out`, {
23
26
  method: 'POST',
24
27
  credentials: 'include',
25
28
  headers: {
@@ -27,12 +30,12 @@ import { useRouter } from 'next/navigation.js';
27
30
  },
28
31
  body: JSON.stringify({})
29
32
  }),
30
- fetch('/api/users/logout', {
33
+ fetch(`${apiRoute}/users/logout`, {
31
34
  method: 'POST',
32
35
  credentials: 'include'
33
36
  })
34
37
  ]);
35
- router.push('/admin/login');
38
+ router.push(`${adminRoute}/login`);
36
39
  } catch (error) {
37
40
  console.error('[better-auth] Logout error:', error);
38
41
  setIsLoading(false);
@@ -1,10 +1,13 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useConfig } from '@payloadcms/ui';
3
4
  import { useState } from 'react';
4
5
  /**
5
6
  * Forgot password page component for requesting a password reset email.
6
7
  * Uses Better Auth's forgetPassword endpoint.
7
8
  */ export function ForgotPasswordView({ logo, title = 'Forgot Password', loginPath = '/admin/login', successMessage = 'If an account exists with this email, you will receive a password reset link.' }) {
9
+ // Payload Config
10
+ const { config: { routes: { admin: adminRoute, api: apiRoute } } } = useConfig();
8
11
  const [email, setEmail] = useState('');
9
12
  const [error, setError] = useState(null);
10
13
  const [success, setSuccess] = useState(false);
@@ -14,7 +17,7 @@ import { useState } from 'react';
14
17
  setLoading(true);
15
18
  setError(null);
16
19
  try {
17
- const response = await fetch('/api/auth/forget-password', {
20
+ const response = await fetch(`${apiRoute}/auth/forget-password`, {
18
21
  method: 'POST',
19
22
  headers: {
20
23
  'Content-Type': 'application/json'
@@ -22,7 +25,7 @@ import { useState } from 'react';
22
25
  credentials: 'include',
23
26
  body: JSON.stringify({
24
27
  email,
25
- redirectTo: `${window.location.origin}/admin/reset-password`
28
+ redirectTo: `${window.location.origin}${adminRoute}/reset-password`
26
29
  })
27
30
  });
28
31
  if (response.ok) {
@@ -3,7 +3,7 @@ export type ResetPasswordViewProps = {
3
3
  logo?: React.ReactNode;
4
4
  /** Page title. Default: 'Reset Password' */
5
5
  title?: string;
6
- /** Path to redirect after successful reset. Default: '/admin/login' */
6
+ /** Path to redirect after successful reset. Defaults to `${routes.admin}/login`. */
7
7
  afterResetPath?: string;
8
8
  /** Minimum password length. Default: 8 */
9
9
  minPasswordLength?: number;
@@ -2,13 +2,16 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useEffect } from 'react';
4
4
  import { useRouter, useSearchParams } from 'next/navigation.js';
5
+ import { useConfig } from '@payloadcms/ui';
5
6
  /**
6
7
  * Reset password page component for setting a new password.
7
8
  * Expects a token in the URL query parameter.
8
9
  * Uses Better Auth's resetPassword endpoint.
9
- */ export function ResetPasswordView({ logo, title = 'Reset Password', afterResetPath = '/admin/login', minPasswordLength = 8 }) {
10
+ */ export function ResetPasswordView({ logo, title = 'Reset Password', afterResetPath, minPasswordLength = 8 }) {
10
11
  const router = useRouter();
11
12
  const searchParams = useSearchParams();
13
+ const { config: { routes: { admin: adminRoute, api: apiRoute } } } = useConfig();
14
+ const resolvedAfterResetPath = afterResetPath ?? `${adminRoute}/login`;
12
15
  const [password, setPassword] = useState('');
13
16
  const [confirmPassword, setConfirmPassword] = useState('');
14
17
  const [error, setError] = useState(null);
@@ -42,7 +45,7 @@ import { useRouter, useSearchParams } from 'next/navigation.js';
42
45
  }
43
46
  setLoading(true);
44
47
  try {
45
- const response = await fetch('/api/auth/reset-password', {
48
+ const response = await fetch(`${apiRoute}/auth/reset-password`, {
46
49
  method: 'POST',
47
50
  headers: {
48
51
  'Content-Type': 'application/json'
@@ -111,7 +114,7 @@ import { useRouter, useSearchParams } from 'next/navigation.js';
111
114
  children: "Your password has been successfully reset. You can now log in with your new password."
112
115
  }),
113
116
  /*#__PURE__*/ _jsx("button", {
114
- onClick: ()=>router.push(afterResetPath),
117
+ onClick: ()=>router.push(resolvedAfterResetPath),
115
118
  style: {
116
119
  padding: 'calc(var(--base) * 0.75) calc(var(--base) * 1.5)',
117
120
  background: 'var(--theme-elevation-800)',
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { NavGroup } from '@payloadcms/ui';
3
+ import { NavGroup, useConfig } from '@payloadcms/ui';
4
4
  /**
5
5
  * Navigation links for security management features.
6
6
  * Rendered in admin sidebar via afterNavLinks injection.
@@ -8,14 +8,16 @@ import { NavGroup } from '@payloadcms/ui';
8
8
  *
9
9
  * Currently only renders API Keys link — 2FA and Passkeys
10
10
  * are now embedded as ui fields on the user document.
11
- */ export function SecurityNavLinks({ basePath = '/admin/security', showApiKeys = true } = {}) {
11
+ */ export function SecurityNavLinks({ basePath, showApiKeys = true } = {}) {
12
+ const { config: { routes: { admin: adminRoute } } } = useConfig();
12
13
  if (!showApiKeys) {
13
14
  return null;
14
15
  }
16
+ const resolvedBasePath = basePath ?? `${adminRoute}/security`;
15
17
  return /*#__PURE__*/ _jsx(NavGroup, {
16
18
  label: "Security",
17
19
  children: /*#__PURE__*/ _jsx("a", {
18
- href: `${basePath}/api-keys`,
20
+ href: `${resolvedBasePath}/api-keys`,
19
21
  className: "nav__link",
20
22
  children: /*#__PURE__*/ _jsx("span", {
21
23
  className: "nav__link-label",
@@ -3,7 +3,7 @@ export type TwoFactorSetupViewProps = {
3
3
  logo?: React.ReactNode;
4
4
  /** Page title. Default: 'Set Up Two-Factor Authentication' */
5
5
  title?: string;
6
- /** Path to redirect after successful setup. Default: '/admin' */
6
+ /** Path to redirect after successful setup. Defaults to `routes.admin`. */
7
7
  afterSetupPath?: string;
8
8
  /** Callback after successful setup */
9
9
  onSetupComplete?: () => void;
@@ -1,11 +1,14 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useEffect } from 'react';
4
+ import { useConfig } from '@payloadcms/ui';
4
5
  /**
5
6
  * Two-factor authentication setup component.
6
7
  * Displays QR code for TOTP apps and allows verification.
7
8
  * Uses Better Auth's twoFactor plugin endpoints.
8
- */ export function TwoFactorSetupView({ logo, title = 'Set Up Two-Factor Authentication', afterSetupPath = '/admin', onSetupComplete }) {
9
+ */ export function TwoFactorSetupView({ logo, title = 'Set Up Two-Factor Authentication', afterSetupPath, onSetupComplete }) {
10
+ const { config: { routes: { admin: adminRoute, api: apiRoute } } } = useConfig();
11
+ const resolvedAfterSetupPath = afterSetupPath ?? adminRoute;
9
12
  const [step, setStep] = useState('loading');
10
13
  const [totpUri, setTotpUri] = useState(null);
11
14
  const [secret, setSecret] = useState(null);
@@ -16,7 +19,7 @@ import { useState, useEffect } from 'react';
16
19
  useEffect(()=>{
17
20
  async function enableTwoFactor() {
18
21
  try {
19
- const response = await fetch('/api/auth/two-factor/enable', {
22
+ const response = await fetch(`${apiRoute}/auth/two-factor/enable`, {
20
23
  method: 'POST',
21
24
  headers: {
22
25
  'Content-Type': 'application/json'
@@ -41,13 +44,15 @@ import { useState, useEffect } from 'react';
41
44
  }
42
45
  }
43
46
  enableTwoFactor();
44
- }, []);
47
+ }, [
48
+ apiRoute
49
+ ]);
45
50
  async function handleVerify(e) {
46
51
  e.preventDefault();
47
52
  setLoading(true);
48
53
  setError(null);
49
54
  try {
50
- const response = await fetch('/api/auth/two-factor/verify-totp', {
55
+ const response = await fetch(`${apiRoute}/auth/two-factor/verify-totp`, {
51
56
  method: 'POST',
52
57
  headers: {
53
58
  'Content-Type': 'application/json'
@@ -144,7 +149,7 @@ import { useState, useEffect } from 'react';
144
149
  children: "Your account is now protected with two-factor authentication."
145
150
  }),
146
151
  /*#__PURE__*/ _jsx("a", {
147
- href: afterSetupPath,
152
+ href: resolvedAfterSetupPath,
148
153
  style: {
149
154
  display: 'inline-block',
150
155
  padding: 'calc(var(--base) * 0.75) calc(var(--base) * 1.5)',
@@ -3,7 +3,7 @@ export type TwoFactorVerifyViewProps = {
3
3
  logo?: React.ReactNode;
4
4
  /** Page title. Default: 'Two-Factor Authentication' */
5
5
  title?: string;
6
- /** Path to redirect after successful verification. Default: '/admin' */
6
+ /** Path to redirect after successful verification. Defaults to `routes.admin`. */
7
7
  afterVerifyPath?: string;
8
8
  /** Callback after successful verification */
9
9
  onVerifyComplete?: () => void;
@@ -2,12 +2,15 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState } from 'react';
4
4
  import { useRouter } from 'next/navigation.js';
5
+ import { useConfig } from '@payloadcms/ui';
5
6
  /**
6
7
  * Two-factor authentication verification component.
7
8
  * Used during login flow when 2FA is enabled on the account.
8
9
  * Uses Better Auth's twoFactor plugin endpoints.
9
- */ export function TwoFactorVerifyView({ logo, title = 'Two-Factor Authentication', afterVerifyPath = '/admin', onVerifyComplete }) {
10
+ */ export function TwoFactorVerifyView({ logo, title = 'Two-Factor Authentication', afterVerifyPath, onVerifyComplete }) {
10
11
  const router = useRouter();
12
+ const { config: { routes: { admin: adminRoute, api: apiRoute } } } = useConfig();
13
+ const resolvedAfterVerifyPath = afterVerifyPath ?? adminRoute;
11
14
  const [code, setCode] = useState('');
12
15
  const [error, setError] = useState(null);
13
16
  const [loading, setLoading] = useState(false);
@@ -17,7 +20,7 @@ import { useRouter } from 'next/navigation.js';
17
20
  setLoading(true);
18
21
  setError(null);
19
22
  try {
20
- const endpoint = useBackupCode ? '/api/auth/two-factor/verify-backup-code' : '/api/auth/two-factor/verify-totp';
23
+ const endpoint = useBackupCode ? `${apiRoute}/auth/two-factor/verify-backup-code` : `${apiRoute}/auth/two-factor/verify-totp`;
21
24
  const response = await fetch(endpoint, {
22
25
  method: 'POST',
23
26
  headers: {
@@ -30,7 +33,7 @@ import { useRouter } from 'next/navigation.js';
30
33
  });
31
34
  if (response.ok) {
32
35
  onVerifyComplete?.();
33
- router.push(afterVerifyPath);
36
+ router.push(resolvedAfterVerifyPath);
34
37
  router.refresh();
35
38
  } else {
36
39
  const data = await response.json().catch(()=>({}));