@delmaredigital/payload-better-auth 0.6.8 → 0.6.10

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.
@@ -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(()=>({}));
@@ -638,96 +638,9 @@ let apiKeyPermissionsConfig = undefined;
638
638
  }
639
639
  }
640
640
  } catch {
641
- // JWT verification failed — try opaque token lookup
642
- // Refreshed OAuth tokens are opaque (not JWTs) and stored in the DB
643
- try {
644
- const adapter = auth.options?.adapter;
645
- if (adapter?.findOne) {
646
- // Hash the token the same way Better Auth stores it (SHA-256 base64url)
647
- const { createHash } = await import('crypto');
648
- const hashedToken = createHash('sha256').update(token).digest('base64url');
649
- const accessTokenRecord = await adapter.findOne({
650
- model: 'oauthAccessToken',
651
- where: [
652
- {
653
- field: 'token',
654
- value: hashedToken
655
- }
656
- ]
657
- });
658
- if (accessTokenRecord && accessTokenRecord.userId) {
659
- // Check expiry
660
- const expiresAt = accessTokenRecord.expiresAt instanceof Date ? accessTokenRecord.expiresAt : new Date(accessTokenRecord.expiresAt);
661
- if (expiresAt < new Date()) {
662
- // Token expired
663
- } else {
664
- const userId = accessTokenRecord.userId;
665
- const users = await payload.find({
666
- collection: usersCollection,
667
- where: {
668
- id: {
669
- equals: userId
670
- }
671
- },
672
- limit: 1,
673
- depth: 0
674
- });
675
- if (users.docs.length > 0) {
676
- const scopes = Array.isArray(accessTokenRecord.scopes) ? accessTokenRecord.scopes : [];
677
- const referenceId = accessTokenRecord.referenceId;
678
- // Look up org role if referenceId (org ID) is present
679
- let orgRole;
680
- if (referenceId) {
681
- try {
682
- const memberships = await payload.find({
683
- collection: membersCollection,
684
- where: {
685
- and: [
686
- {
687
- user: {
688
- equals: userId
689
- }
690
- },
691
- {
692
- organization: {
693
- equals: referenceId
694
- }
695
- }
696
- ]
697
- },
698
- limit: 1,
699
- depth: 0
700
- });
701
- if (memberships.docs.length > 0) {
702
- orgRole = memberships.docs[0].role;
703
- }
704
- } catch {
705
- // Members collection might not exist
706
- }
707
- }
708
- const userDoc = users.docs[0];
709
- return {
710
- user: {
711
- ...userDoc,
712
- id: userDoc.id,
713
- oauthScopes: scopes,
714
- collection: usersCollection,
715
- _strategy: 'better-auth',
716
- ...referenceId ? {
717
- activeOrganizationId: referenceId
718
- } : {},
719
- ...orgRole ? {
720
- organizationRole: orgRole
721
- } : {}
722
- }
723
- };
724
- }
725
- }
726
- }
727
- }
728
- } catch {
729
- // Opaque token lookup also failed — continue to return null
730
- }
641
+ // JWT verification failed — token is not a valid OAuth JWT
642
+ // Per Better Auth docs: "only accept JWT-formatted access tokens for your API"
643
+ // Opaque tokens should be handled via the /oauth2/introspect endpoint if needed
731
644
  }
732
645
  }
733
646
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delmaredigital/payload-better-auth",
3
- "version": "0.6.8",
3
+ "version": "0.6.10",
4
4
  "description": "Better Auth adapter and plugins for Payload CMS",
5
5
  "type": "module",
6
6
  "license": "MIT",