@delmaredigital/payload-better-auth 0.3.7 → 0.3.8
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 +12 -1
- package/dist/adapter/collections.d.ts.map +1 -1
- package/dist/adapter/collections.js +126 -88
- package/dist/adapter/collections.js.map +1 -1
- package/dist/adapter/index.js +197 -150
- package/dist/adapter/index.js.map +1 -1
- package/dist/components/BeforeLogin.d.ts +1 -1
- package/dist/components/BeforeLogin.d.ts.map +1 -1
- package/dist/components/BeforeLogin.js +15 -7
- package/dist/components/BeforeLogin.js.map +1 -1
- package/dist/components/LoginView.d.ts +2 -2
- package/dist/components/LoginView.d.ts.map +1 -1
- package/dist/components/LoginView.js +660 -218
- package/dist/components/LoginView.js.map +1 -1
- package/dist/components/LoginViewWrapper.d.ts +1 -1
- package/dist/components/LoginViewWrapper.d.ts.map +1 -1
- package/dist/components/LoginViewWrapper.js +14 -4
- package/dist/components/LoginViewWrapper.js.map +1 -1
- package/dist/components/LogoutButton.d.ts +1 -1
- package/dist/components/LogoutButton.d.ts.map +1 -1
- package/dist/components/LogoutButton.js +19 -11
- package/dist/components/LogoutButton.js.map +1 -1
- package/dist/components/PasskeyRegisterButton.d.ts +2 -2
- package/dist/components/PasskeyRegisterButton.d.ts.map +1 -1
- package/dist/components/PasskeyRegisterButton.js +20 -16
- package/dist/components/PasskeyRegisterButton.js.map +1 -1
- package/dist/components/PasskeySignInButton.d.ts +2 -2
- package/dist/components/PasskeySignInButton.d.ts.map +1 -1
- package/dist/components/PasskeySignInButton.js +14 -12
- package/dist/components/PasskeySignInButton.js.map +1 -1
- package/dist/components/auth/ForgotPasswordView.d.ts +1 -1
- package/dist/components/auth/ForgotPasswordView.d.ts.map +1 -1
- package/dist/components/auth/ForgotPasswordView.js +133 -43
- package/dist/components/auth/ForgotPasswordView.js.map +1 -1
- package/dist/components/auth/ResetPasswordView.d.ts +1 -1
- package/dist/components/auth/ResetPasswordView.d.ts.map +1 -1
- package/dist/components/auth/ResetPasswordView.js +154 -50
- package/dist/components/auth/ResetPasswordView.js.map +1 -1
- package/dist/components/auth/index.js +2 -2
- package/dist/components/auth/index.js.map +1 -1
- package/dist/components/management/ApiKeysManagementClient.d.ts +2 -2
- package/dist/components/management/ApiKeysManagementClient.d.ts.map +1 -1
- package/dist/components/management/ApiKeysManagementClient.js +539 -222
- package/dist/components/management/ApiKeysManagementClient.js.map +1 -1
- package/dist/components/management/PasskeysManagementClient.d.ts +2 -2
- package/dist/components/management/PasskeysManagementClient.d.ts.map +1 -1
- package/dist/components/management/PasskeysManagementClient.js +215 -92
- package/dist/components/management/PasskeysManagementClient.js.map +1 -1
- package/dist/components/management/SecurityNavLinks.d.ts +1 -1
- package/dist/components/management/SecurityNavLinks.d.ts.map +1 -1
- package/dist/components/management/SecurityNavLinks.js +51 -24
- package/dist/components/management/SecurityNavLinks.js.map +1 -1
- package/dist/components/management/TwoFactorManagementClient.d.ts +2 -2
- package/dist/components/management/TwoFactorManagementClient.d.ts.map +1 -1
- package/dist/components/management/TwoFactorManagementClient.js +270 -111
- package/dist/components/management/TwoFactorManagementClient.js.map +1 -1
- package/dist/components/management/index.js +2 -2
- package/dist/components/management/index.js.map +1 -1
- package/dist/components/management/views/ApiKeysView.d.ts +1 -1
- package/dist/components/management/views/ApiKeysView.d.ts.map +1 -1
- package/dist/components/management/views/ApiKeysView.js +19 -4
- package/dist/components/management/views/ApiKeysView.js.map +1 -1
- package/dist/components/management/views/PasskeysView.d.ts +1 -1
- package/dist/components/management/views/PasskeysView.d.ts.map +1 -1
- package/dist/components/management/views/PasskeysView.js +16 -4
- package/dist/components/management/views/PasskeysView.js.map +1 -1
- package/dist/components/management/views/TwoFactorView.d.ts +1 -1
- package/dist/components/management/views/TwoFactorView.d.ts.map +1 -1
- package/dist/components/management/views/TwoFactorView.js +16 -4
- package/dist/components/management/views/TwoFactorView.js.map +1 -1
- package/dist/components/management/views/index.js +2 -2
- package/dist/components/management/views/index.js.map +1 -1
- package/dist/components/twoFactor/TwoFactorSetupView.d.ts +1 -1
- package/dist/components/twoFactor/TwoFactorSetupView.d.ts.map +1 -1
- package/dist/components/twoFactor/TwoFactorSetupView.js +240 -87
- package/dist/components/twoFactor/TwoFactorSetupView.js.map +1 -1
- package/dist/components/twoFactor/TwoFactorVerifyView.d.ts +1 -1
- package/dist/components/twoFactor/TwoFactorVerifyView.d.ts.map +1 -1
- package/dist/components/twoFactor/TwoFactorVerifyView.js +108 -45
- package/dist/components/twoFactor/TwoFactorVerifyView.js.map +1 -1
- package/dist/components/twoFactor/index.js +2 -2
- package/dist/components/twoFactor/index.js.map +1 -1
- package/dist/exports/client.js +9 -10
- package/dist/exports/client.js.map +1 -1
- package/dist/exports/components.js +2 -2
- package/dist/exports/components.js.map +1 -1
- package/dist/exports/management.js +3 -3
- package/dist/exports/management.js.map +1 -1
- package/dist/exports/rsc.js +2 -2
- package/dist/exports/rsc.js.map +1 -1
- package/dist/generated-types.js +4 -2
- package/dist/generated-types.js.map +1 -1
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/dist/plugin/index.js +198 -162
- package/dist/plugin/index.js.map +1 -1
- package/dist/scripts/generate-types.js +66 -50
- package/dist/scripts/generate-types.js.map +1 -1
- package/dist/types/apiKey.js +7 -2
- package/dist/types/apiKey.js.map +1 -1
- package/dist/types/betterAuth.js +23 -2
- package/dist/types/betterAuth.js.map +1 -1
- package/dist/utils/access.js +78 -81
- package/dist/utils/access.js.map +1 -1
- package/dist/utils/apiKeyAccess.js +65 -72
- package/dist/utils/apiKeyAccess.js.map +1 -1
- package/dist/utils/betterAuthDefaults.js +8 -8
- package/dist/utils/betterAuthDefaults.js.map +1 -1
- package/dist/utils/detectAuthConfig.js +8 -11
- package/dist/utils/detectAuthConfig.js.map +1 -1
- package/dist/utils/detectEnabledPlugins.js +6 -7
- package/dist/utils/detectEnabledPlugins.js.map +1 -1
- package/dist/utils/firstUserAdmin.js +18 -20
- package/dist/utils/firstUserAdmin.js.map +1 -1
- package/dist/utils/generateScopes.js +40 -41
- package/dist/utils/generateScopes.js.map +1 -1
- package/dist/utils/session.js +8 -9
- package/dist/utils/session.js.map +1 -1
- package/package.json +97 -26
- package/src/adapter/collections.ts +621 -0
- package/src/adapter/index.ts +712 -0
- package/src/components/BeforeLogin.tsx +39 -0
- package/src/components/LoginView.tsx +1516 -0
- package/src/components/LoginViewWrapper.tsx +35 -0
- package/src/components/LogoutButton.tsx +58 -0
- package/src/components/PasskeyRegisterButton.tsx +105 -0
- package/src/components/PasskeySignInButton.tsx +96 -0
- package/src/components/auth/ForgotPasswordView.tsx +274 -0
- package/src/components/auth/ResetPasswordView.tsx +331 -0
- package/src/components/auth/index.ts +8 -0
- package/src/components/management/ApiKeysManagementClient.tsx +988 -0
- package/src/components/management/PasskeysManagementClient.tsx +409 -0
- package/src/components/management/SecurityNavLinks.tsx +117 -0
- package/src/components/management/TwoFactorManagementClient.tsx +560 -0
- package/src/components/management/index.ts +20 -0
- package/src/components/management/views/ApiKeysView.tsx +57 -0
- package/src/components/management/views/PasskeysView.tsx +42 -0
- package/src/components/management/views/TwoFactorView.tsx +42 -0
- package/src/components/management/views/index.ts +10 -0
- package/src/components/twoFactor/TwoFactorSetupView.tsx +515 -0
- package/src/components/twoFactor/TwoFactorVerifyView.tsx +238 -0
- package/src/components/twoFactor/index.ts +8 -0
- package/src/exports/client.ts +77 -0
- package/src/exports/components.ts +30 -0
- package/src/exports/management.ts +25 -0
- package/src/exports/rsc.ts +11 -0
- package/src/generated-types.ts +269 -0
- package/src/index.ts +135 -0
- package/src/plugin/index.ts +834 -0
- package/src/scripts/generate-types.ts +269 -0
- package/src/types/apiKey.ts +63 -0
- package/src/types/betterAuth.ts +253 -0
- package/src/utils/access.ts +410 -0
- package/src/utils/apiKeyAccess.ts +443 -0
- package/src/utils/betterAuthDefaults.ts +102 -0
- package/src/utils/detectAuthConfig.ts +47 -0
- package/src/utils/detectEnabledPlugins.ts +69 -0
- package/src/utils/firstUserAdmin.ts +164 -0
- package/src/utils/generateScopes.ts +150 -0
- package/src/utils/session.ts +91 -0
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payload Plugins for Better Auth
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
Plugin,
|
|
9
|
+
AuthStrategy,
|
|
10
|
+
Payload,
|
|
11
|
+
BasePayload,
|
|
12
|
+
Endpoint,
|
|
13
|
+
PayloadHandler,
|
|
14
|
+
Config,
|
|
15
|
+
} from 'payload'
|
|
16
|
+
import type { betterAuth, BetterAuthOptions } from 'better-auth'
|
|
17
|
+
import { detectAuthConfig } from '../utils/detectAuthConfig.js'
|
|
18
|
+
import {
|
|
19
|
+
detectEnabledPlugins,
|
|
20
|
+
type EnabledPluginsResult,
|
|
21
|
+
} from '../utils/detectEnabledPlugins.js'
|
|
22
|
+
import type { ApiKeyScopesConfig } from '../types/apiKey.js'
|
|
23
|
+
import {
|
|
24
|
+
buildAvailableScopes,
|
|
25
|
+
scopesToPermissions,
|
|
26
|
+
} from '../utils/generateScopes.js'
|
|
27
|
+
|
|
28
|
+
export type Auth = ReturnType<typeof betterAuth>
|
|
29
|
+
// PayloadWithAuth from types
|
|
30
|
+
import type { PayloadWithAuth } from '../types/betterAuth.js'
|
|
31
|
+
export type { PayloadWithAuth } from '../types/betterAuth.js'
|
|
32
|
+
|
|
33
|
+
export type CreateAuthFunction = (payload: BasePayload) => Auth
|
|
34
|
+
|
|
35
|
+
export type BetterAuthPluginAdminOptions = {
|
|
36
|
+
/** Disable auto-injection of logout button */
|
|
37
|
+
disableLogoutButton?: boolean
|
|
38
|
+
/** Disable auto-injection of BeforeLogin redirect */
|
|
39
|
+
disableBeforeLogin?: boolean
|
|
40
|
+
/** Disable auto-injection of login view */
|
|
41
|
+
disableLoginView?: boolean
|
|
42
|
+
/** Login page customization */
|
|
43
|
+
login?: {
|
|
44
|
+
/** Custom title for login page */
|
|
45
|
+
title?: string
|
|
46
|
+
/** Path to redirect after successful login. Default: '/admin' */
|
|
47
|
+
afterLoginPath?: string
|
|
48
|
+
/**
|
|
49
|
+
* Required role(s) for admin access.
|
|
50
|
+
* - string: Single role required (default: 'admin')
|
|
51
|
+
* - string[]: Multiple roles (behavior depends on requireAllRoles)
|
|
52
|
+
* - null: Disable role checking
|
|
53
|
+
*/
|
|
54
|
+
requiredRole?: string | string[] | null
|
|
55
|
+
/**
|
|
56
|
+
* When requiredRole is an array, require ALL roles (true) or ANY role (false).
|
|
57
|
+
* Default: false (any matching role grants access)
|
|
58
|
+
*/
|
|
59
|
+
requireAllRoles?: boolean
|
|
60
|
+
/**
|
|
61
|
+
* Enable passkey (WebAuthn) sign-in option.
|
|
62
|
+
* - true: Always show passkey button
|
|
63
|
+
* - false: Never show passkey button
|
|
64
|
+
* - 'auto': Auto-detect if passkey plugin is available (default for LoginView)
|
|
65
|
+
* Default: false (for backwards compatibility)
|
|
66
|
+
*/
|
|
67
|
+
enablePasskey?: boolean | 'auto'
|
|
68
|
+
/**
|
|
69
|
+
* Enable user registration (sign up) option.
|
|
70
|
+
* - true: Always show "Create account" link
|
|
71
|
+
* - false: Never show registration option
|
|
72
|
+
* - 'auto': Auto-detect if sign-up endpoint is available
|
|
73
|
+
* Default: 'auto' - LoginView automatically detects if Better Auth has signup enabled
|
|
74
|
+
*/
|
|
75
|
+
enableSignUp?: boolean | 'auto'
|
|
76
|
+
/**
|
|
77
|
+
* Default role to assign to new users during registration.
|
|
78
|
+
* Only used when enableSignUp is enabled.
|
|
79
|
+
* Default: 'user'
|
|
80
|
+
*/
|
|
81
|
+
defaultSignUpRole?: string
|
|
82
|
+
/**
|
|
83
|
+
* Enable forgot password option.
|
|
84
|
+
* - true: Always show "Forgot password?" link
|
|
85
|
+
* - false: Never show forgot password option
|
|
86
|
+
* - 'auto': Auto-detect if password reset endpoint is available
|
|
87
|
+
* Default: 'auto' - LoginView automatically detects if Better Auth has password reset enabled
|
|
88
|
+
*/
|
|
89
|
+
enableForgotPassword?: boolean | 'auto'
|
|
90
|
+
/**
|
|
91
|
+
* Custom URL for password reset page. If provided, users will be redirected here
|
|
92
|
+
* instead of showing the inline password reset form.
|
|
93
|
+
*/
|
|
94
|
+
resetPasswordUrl?: string
|
|
95
|
+
}
|
|
96
|
+
/** Path to custom logout button component (import map format) */
|
|
97
|
+
logoutButtonComponent?: string
|
|
98
|
+
/** Path to custom BeforeLogin component (import map format) */
|
|
99
|
+
beforeLoginComponent?: string
|
|
100
|
+
/** Path to custom login view component (import map format) */
|
|
101
|
+
loginViewComponent?: string
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Enable management UI for security features (2FA, API keys).
|
|
105
|
+
* Management views are auto-injected based on which Better Auth plugins are enabled.
|
|
106
|
+
* @default true
|
|
107
|
+
*/
|
|
108
|
+
enableManagementUI?: boolean
|
|
109
|
+
/**
|
|
110
|
+
* Better Auth options - used to detect which plugins are enabled.
|
|
111
|
+
* Required for management UI to auto-detect enabled features.
|
|
112
|
+
*/
|
|
113
|
+
betterAuthOptions?: Partial<BetterAuthOptions>
|
|
114
|
+
/** Custom paths for management views */
|
|
115
|
+
managementPaths?: {
|
|
116
|
+
/** Two-factor management view path. Default: '/security/two-factor' */
|
|
117
|
+
twoFactor?: string
|
|
118
|
+
/** API keys management view path. Default: '/security/api-keys' */
|
|
119
|
+
apiKeys?: string
|
|
120
|
+
/** Passkeys management view path. Default: '/security/passkeys' */
|
|
121
|
+
passkeys?: string
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* API key scopes configuration.
|
|
125
|
+
* Controls which permission scopes are available when creating API keys.
|
|
126
|
+
* When not provided, scopes are auto-generated from Payload collections.
|
|
127
|
+
*/
|
|
128
|
+
apiKey?: ApiKeyScopesConfig
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export type BetterAuthPluginOptions = {
|
|
132
|
+
/**
|
|
133
|
+
* Function that creates the Better Auth instance.
|
|
134
|
+
* Called during Payload's onInit lifecycle.
|
|
135
|
+
*/
|
|
136
|
+
createAuth: CreateAuthFunction
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Base path for auth API endpoints (registered via Payload endpoints).
|
|
140
|
+
* @default '/auth'
|
|
141
|
+
*/
|
|
142
|
+
authBasePath?: string
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Auto-register auth API endpoints via Payload's endpoint system.
|
|
146
|
+
* Set to false if you need custom route-level handling (rare).
|
|
147
|
+
* Note: All Better Auth customization (hooks, plugins, callbacks)
|
|
148
|
+
* is done in createAuth - the route handler is just a passthrough.
|
|
149
|
+
* @default true
|
|
150
|
+
*/
|
|
151
|
+
autoRegisterEndpoints?: boolean
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Auto-inject admin components when disableLocalStrategy is detected.
|
|
155
|
+
* @default true
|
|
156
|
+
*/
|
|
157
|
+
autoInjectAdminComponents?: boolean
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Admin UI customization options.
|
|
161
|
+
*/
|
|
162
|
+
admin?: BetterAuthPluginAdminOptions
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Track auth instance for HMR
|
|
166
|
+
let authInstance: Auth | null = null
|
|
167
|
+
|
|
168
|
+
// Store API key scopes config for access by management views
|
|
169
|
+
let apiKeyScopesConfig: ApiKeyScopesConfig | undefined = undefined
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get the configured API key scopes config.
|
|
173
|
+
* Used by the ApiKeysView to build available scopes.
|
|
174
|
+
*/
|
|
175
|
+
export function getApiKeyScopesConfig(): ApiKeyScopesConfig | undefined {
|
|
176
|
+
return apiKeyScopesConfig
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Type for auth api methods we need
|
|
180
|
+
type AuthApi = {
|
|
181
|
+
getSession: (options: { headers: Headers }) => Promise<{
|
|
182
|
+
user?: { id: string } | null
|
|
183
|
+
session?: Record<string, unknown> | null
|
|
184
|
+
} | null>
|
|
185
|
+
createApiKey?: (options: {
|
|
186
|
+
body: {
|
|
187
|
+
name?: string
|
|
188
|
+
expiresIn?: number
|
|
189
|
+
userId: string
|
|
190
|
+
prefix?: string
|
|
191
|
+
metadata?: Record<string, unknown>
|
|
192
|
+
permissions?: Record<string, string[]>
|
|
193
|
+
}
|
|
194
|
+
}) => Promise<{
|
|
195
|
+
key: string
|
|
196
|
+
id: string
|
|
197
|
+
name: string | null
|
|
198
|
+
userId: string
|
|
199
|
+
expiresAt: Date | null
|
|
200
|
+
createdAt: Date
|
|
201
|
+
metadata: Record<string, unknown> | null
|
|
202
|
+
}>
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Handle API key creation with scopes server-side.
|
|
207
|
+
* Converts scopes to permissions and calls Better Auth's server API.
|
|
208
|
+
*/
|
|
209
|
+
async function handleApiKeyCreateWithScopes(
|
|
210
|
+
authApi: AuthApi,
|
|
211
|
+
payload: Payload,
|
|
212
|
+
headers: Headers,
|
|
213
|
+
body: Record<string, unknown>
|
|
214
|
+
): Promise<Response> {
|
|
215
|
+
try {
|
|
216
|
+
// Get the current session to find the user
|
|
217
|
+
const session = await authApi.getSession({ headers })
|
|
218
|
+
if (!session?.user?.id) {
|
|
219
|
+
return new Response(
|
|
220
|
+
JSON.stringify({ error: 'Unauthorized' }),
|
|
221
|
+
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Extract scopes from the request body
|
|
226
|
+
const scopes = (body.scopes as string[] | undefined) ?? []
|
|
227
|
+
|
|
228
|
+
// Build permissions from scopes if any are provided
|
|
229
|
+
let permissions: Record<string, string[]> | undefined
|
|
230
|
+
if (scopes.length > 0) {
|
|
231
|
+
const scopesConfig = getApiKeyScopesConfig()
|
|
232
|
+
const availableScopes = buildAvailableScopes(
|
|
233
|
+
payload.config.collections,
|
|
234
|
+
scopesConfig
|
|
235
|
+
)
|
|
236
|
+
permissions = scopesToPermissions(scopes, availableScopes)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Build the API key creation options
|
|
240
|
+
const createOptions = {
|
|
241
|
+
body: {
|
|
242
|
+
name: body.name as string | undefined,
|
|
243
|
+
userId: session.user.id,
|
|
244
|
+
expiresIn: body.expiresIn as number | undefined,
|
|
245
|
+
prefix: body.prefix as string | undefined,
|
|
246
|
+
permissions: permissions && Object.keys(permissions).length > 0 ? permissions : undefined,
|
|
247
|
+
metadata: scopes.length > 0
|
|
248
|
+
? { ...(body.metadata as Record<string, unknown> | undefined), scopes }
|
|
249
|
+
: (body.metadata as Record<string, unknown> | undefined),
|
|
250
|
+
},
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Call Better Auth's server-side API
|
|
254
|
+
if (typeof authApi.createApiKey !== 'function') {
|
|
255
|
+
return new Response(
|
|
256
|
+
JSON.stringify({ error: 'API key plugin not enabled' }),
|
|
257
|
+
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
const result = await authApi.createApiKey(createOptions)
|
|
263
|
+
return new Response(JSON.stringify(result), {
|
|
264
|
+
status: 200,
|
|
265
|
+
headers: { 'Content-Type': 'application/json' },
|
|
266
|
+
})
|
|
267
|
+
} catch (createError) {
|
|
268
|
+
// Check if error is due to metadata being disabled
|
|
269
|
+
const errorMessage = createError instanceof Error ? createError.message : String(createError)
|
|
270
|
+
const isMetadataDisabled = errorMessage.toLowerCase().includes('metadata') &&
|
|
271
|
+
errorMessage.toLowerCase().includes('disabled')
|
|
272
|
+
|
|
273
|
+
if (isMetadataDisabled && createOptions.body.metadata) {
|
|
274
|
+
// Retry without metadata - key will still work, just won't show scopes in UI
|
|
275
|
+
console.warn('[better-auth] Metadata disabled, creating API key without scope metadata. Enable metadata with apiKeyWithDefaults() for better UX.')
|
|
276
|
+
const optionsWithoutMetadata = {
|
|
277
|
+
body: {
|
|
278
|
+
...createOptions.body,
|
|
279
|
+
metadata: undefined,
|
|
280
|
+
},
|
|
281
|
+
}
|
|
282
|
+
const result = await authApi.createApiKey(optionsWithoutMetadata)
|
|
283
|
+
return new Response(JSON.stringify(result), {
|
|
284
|
+
status: 200,
|
|
285
|
+
headers: { 'Content-Type': 'application/json' },
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Re-throw other errors
|
|
290
|
+
throw createError
|
|
291
|
+
}
|
|
292
|
+
} catch (error) {
|
|
293
|
+
console.error('[better-auth] API key creation error:', error)
|
|
294
|
+
const message = error instanceof Error ? error.message : 'Failed to create API key'
|
|
295
|
+
return new Response(
|
|
296
|
+
JSON.stringify({ error: message }),
|
|
297
|
+
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Creates the auth endpoint handler that proxies requests to Better Auth.
|
|
304
|
+
*/
|
|
305
|
+
function createAuthEndpointHandler(): PayloadHandler {
|
|
306
|
+
return async (req) => {
|
|
307
|
+
const payloadWithAuth = req.payload as PayloadWithAuth
|
|
308
|
+
const auth = payloadWithAuth.betterAuth
|
|
309
|
+
|
|
310
|
+
if (!auth) {
|
|
311
|
+
return new Response(
|
|
312
|
+
JSON.stringify({ error: 'Better Auth not initialized' }),
|
|
313
|
+
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
// Construct the full URL for Better Auth
|
|
319
|
+
// PayloadRequest provides these properties
|
|
320
|
+
const protocol = req.headers.get('x-forwarded-proto') || 'http'
|
|
321
|
+
const host = req.headers.get('host') || 'localhost'
|
|
322
|
+
const pathname = (req as unknown as { pathname?: string }).pathname || ''
|
|
323
|
+
const search =
|
|
324
|
+
(req as unknown as { search?: string }).search ||
|
|
325
|
+
(req as unknown as { url?: string }).url?.split('?')[1] ||
|
|
326
|
+
''
|
|
327
|
+
|
|
328
|
+
const url = new URL(pathname, `${protocol}://${host}`)
|
|
329
|
+
if (search) {
|
|
330
|
+
url.search = search.startsWith('?') ? search : `?${search}`
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Get request body for non-GET methods
|
|
334
|
+
let body: string | undefined
|
|
335
|
+
let parsedBody: Record<string, unknown> | undefined
|
|
336
|
+
if (req.method && !['GET', 'HEAD'].includes(req.method)) {
|
|
337
|
+
try {
|
|
338
|
+
// Try to get body from request
|
|
339
|
+
if (typeof (req as unknown as { text?: () => Promise<string> }).text === 'function') {
|
|
340
|
+
body = await (req as unknown as { text: () => Promise<string> }).text()
|
|
341
|
+
if (body) {
|
|
342
|
+
try {
|
|
343
|
+
parsedBody = JSON.parse(body)
|
|
344
|
+
} catch {
|
|
345
|
+
// Not JSON, that's okay
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} else if ((req as unknown as { data?: unknown }).data) {
|
|
349
|
+
parsedBody = (req as unknown as { data: Record<string, unknown> }).data
|
|
350
|
+
body = JSON.stringify(parsedBody)
|
|
351
|
+
}
|
|
352
|
+
} catch {
|
|
353
|
+
// Body might already be consumed, try data property
|
|
354
|
+
if ((req as unknown as { data?: unknown }).data) {
|
|
355
|
+
parsedBody = (req as unknown as { data: Record<string, unknown> }).data
|
|
356
|
+
body = JSON.stringify(parsedBody)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Intercept API key creation requests with scopes
|
|
362
|
+
// Better Auth's API key create endpoint is POST /api-key/create
|
|
363
|
+
const isApiKeyCreate =
|
|
364
|
+
req.method === 'POST' &&
|
|
365
|
+
pathname.endsWith('/api-key/create') &&
|
|
366
|
+
parsedBody?.scopes &&
|
|
367
|
+
Array.isArray(parsedBody.scopes)
|
|
368
|
+
|
|
369
|
+
if (isApiKeyCreate && parsedBody) {
|
|
370
|
+
return handleApiKeyCreateWithScopes(
|
|
371
|
+
auth.api as unknown as AuthApi,
|
|
372
|
+
req.payload,
|
|
373
|
+
req.headers,
|
|
374
|
+
parsedBody
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Create a new Request for Better Auth
|
|
379
|
+
const request = new Request(url.toString(), {
|
|
380
|
+
method: req.method || 'GET',
|
|
381
|
+
headers: req.headers,
|
|
382
|
+
body,
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
const response = await auth.handler(request)
|
|
386
|
+
|
|
387
|
+
return response
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.error('[better-auth] Endpoint handler error:', error)
|
|
390
|
+
return new Response(
|
|
391
|
+
JSON.stringify({ error: 'Internal server error' }),
|
|
392
|
+
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Generates Payload endpoints for Better Auth.
|
|
400
|
+
*/
|
|
401
|
+
function generateAuthEndpoints(basePath: string): Endpoint[] {
|
|
402
|
+
const handler = createAuthEndpointHandler()
|
|
403
|
+
const methods = ['get', 'post', 'patch', 'put', 'delete'] as const
|
|
404
|
+
|
|
405
|
+
return methods.map((method) => ({
|
|
406
|
+
path: `${basePath}/:path*`,
|
|
407
|
+
method,
|
|
408
|
+
handler,
|
|
409
|
+
}))
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Injects admin components into the Payload config when disableLocalStrategy is detected.
|
|
414
|
+
*/
|
|
415
|
+
function injectAdminComponents(
|
|
416
|
+
config: Config,
|
|
417
|
+
options: BetterAuthPluginOptions
|
|
418
|
+
): Config {
|
|
419
|
+
const authDetection = detectAuthConfig(config)
|
|
420
|
+
|
|
421
|
+
// Skip if not using disableLocalStrategy or auto-injection is disabled
|
|
422
|
+
if (
|
|
423
|
+
!authDetection.hasDisableLocalStrategy ||
|
|
424
|
+
options.autoInjectAdminComponents === false
|
|
425
|
+
) {
|
|
426
|
+
return config
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const adminOptions = options.admin ?? {}
|
|
430
|
+
const existingComponents = config.admin?.components ?? {}
|
|
431
|
+
|
|
432
|
+
// Build logout button config
|
|
433
|
+
const logoutButton = adminOptions.disableLogoutButton
|
|
434
|
+
? (existingComponents.logout as { Button?: string })?.Button
|
|
435
|
+
: adminOptions.logoutButtonComponent ??
|
|
436
|
+
'@delmaredigital/payload-better-auth/components#LogoutButton'
|
|
437
|
+
|
|
438
|
+
// Build beforeLogin config
|
|
439
|
+
const existingBeforeLogin = existingComponents.beforeLogin ?? []
|
|
440
|
+
const beforeLogin = adminOptions.disableBeforeLogin
|
|
441
|
+
? existingBeforeLogin
|
|
442
|
+
: [
|
|
443
|
+
...(Array.isArray(existingBeforeLogin)
|
|
444
|
+
? existingBeforeLogin
|
|
445
|
+
: [existingBeforeLogin]),
|
|
446
|
+
adminOptions.beforeLoginComponent ??
|
|
447
|
+
'@delmaredigital/payload-better-auth/components#BeforeLogin',
|
|
448
|
+
]
|
|
449
|
+
|
|
450
|
+
// Build login view config
|
|
451
|
+
const existingViews =
|
|
452
|
+
(existingComponents.views as Record<string, unknown> | undefined) ?? {}
|
|
453
|
+
const newLoginView = adminOptions.disableLoginView
|
|
454
|
+
? undefined
|
|
455
|
+
: {
|
|
456
|
+
Component:
|
|
457
|
+
adminOptions.loginViewComponent ??
|
|
458
|
+
'@delmaredigital/payload-better-auth/rsc#LoginViewWrapper',
|
|
459
|
+
path: '/login' as const,
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const views = {
|
|
463
|
+
...existingViews,
|
|
464
|
+
...(newLoginView ? { login: newLoginView } : {}),
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Store login config in config.custom for the RSC wrapper to read
|
|
468
|
+
const loginConfig = adminOptions.login ?? {}
|
|
469
|
+
|
|
470
|
+
// Note: enabledPlugins will be added by injectManagementComponents
|
|
471
|
+
return {
|
|
472
|
+
...config,
|
|
473
|
+
custom: {
|
|
474
|
+
...config.custom,
|
|
475
|
+
betterAuth: {
|
|
476
|
+
...(config.custom?.betterAuth as Record<string, unknown> | undefined),
|
|
477
|
+
login: loginConfig,
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
admin: {
|
|
481
|
+
...config.admin,
|
|
482
|
+
components: {
|
|
483
|
+
...existingComponents,
|
|
484
|
+
logout: logoutButton
|
|
485
|
+
? {
|
|
486
|
+
...(typeof existingComponents.logout === 'object'
|
|
487
|
+
? existingComponents.logout
|
|
488
|
+
: {}),
|
|
489
|
+
Button: logoutButton,
|
|
490
|
+
}
|
|
491
|
+
: existingComponents.logout,
|
|
492
|
+
beforeLogin,
|
|
493
|
+
views,
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
} as Config
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Injects management UI components into the Payload config based on enabled plugins.
|
|
501
|
+
*/
|
|
502
|
+
function injectManagementComponents(
|
|
503
|
+
config: Config,
|
|
504
|
+
options: BetterAuthPluginOptions
|
|
505
|
+
): Config {
|
|
506
|
+
const adminOptions = options.admin ?? {}
|
|
507
|
+
|
|
508
|
+
// Skip if management UI is disabled
|
|
509
|
+
if (adminOptions.enableManagementUI === false) {
|
|
510
|
+
return config
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Detect which plugins are enabled
|
|
514
|
+
const enabledPlugins = detectEnabledPlugins(adminOptions.betterAuthOptions)
|
|
515
|
+
|
|
516
|
+
// Get custom paths or use defaults
|
|
517
|
+
const paths = {
|
|
518
|
+
twoFactor: adminOptions.managementPaths?.twoFactor ?? '/security/two-factor',
|
|
519
|
+
apiKeys: adminOptions.managementPaths?.apiKeys ?? '/security/api-keys',
|
|
520
|
+
passkeys: adminOptions.managementPaths?.passkeys ?? '/security/passkeys',
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const existingComponents = config.admin?.components ?? {}
|
|
524
|
+
const existingViews =
|
|
525
|
+
(existingComponents.views as Record<string, unknown> | undefined) ?? {}
|
|
526
|
+
const existingAfterNavLinks = existingComponents.afterNavLinks ?? []
|
|
527
|
+
|
|
528
|
+
// Build management views based on enabled plugins
|
|
529
|
+
// Note: Sessions and passkeys use Payload's default collection views
|
|
530
|
+
const managementViews: Record<string, { Component: string; path: string }> = {}
|
|
531
|
+
|
|
532
|
+
// Two-factor (if enabled)
|
|
533
|
+
if (enabledPlugins.hasTwoFactor) {
|
|
534
|
+
managementViews.securityTwoFactor = {
|
|
535
|
+
Component: '@delmaredigital/payload-better-auth/rsc#TwoFactorView',
|
|
536
|
+
path: paths.twoFactor,
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// API keys (if enabled)
|
|
541
|
+
if (enabledPlugins.hasApiKey) {
|
|
542
|
+
managementViews.securityApiKeys = {
|
|
543
|
+
Component: '@delmaredigital/payload-better-auth/rsc#ApiKeysView',
|
|
544
|
+
path: paths.apiKeys,
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Passkeys (if enabled)
|
|
549
|
+
if (enabledPlugins.hasPasskey) {
|
|
550
|
+
managementViews.securityPasskeys = {
|
|
551
|
+
Component: '@delmaredigital/payload-better-auth/rsc#PasskeysView',
|
|
552
|
+
path: paths.passkeys,
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Only add nav links if at least one plugin is enabled
|
|
557
|
+
const hasAnyPlugin = enabledPlugins.hasTwoFactor || enabledPlugins.hasApiKey || enabledPlugins.hasPasskey
|
|
558
|
+
|
|
559
|
+
// Add SecurityNavLinks to afterNavLinks with clientProps for enabled plugins
|
|
560
|
+
const afterNavLinks = hasAnyPlugin
|
|
561
|
+
? [
|
|
562
|
+
...(Array.isArray(existingAfterNavLinks)
|
|
563
|
+
? existingAfterNavLinks
|
|
564
|
+
: [existingAfterNavLinks]),
|
|
565
|
+
{
|
|
566
|
+
path: '@delmaredigital/payload-better-auth/components/management#SecurityNavLinks',
|
|
567
|
+
clientProps: {
|
|
568
|
+
showTwoFactor: enabledPlugins.hasTwoFactor,
|
|
569
|
+
showApiKeys: enabledPlugins.hasApiKey,
|
|
570
|
+
showPasskeys: enabledPlugins.hasPasskey,
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
]
|
|
574
|
+
: existingAfterNavLinks
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
...config,
|
|
578
|
+
admin: {
|
|
579
|
+
...config.admin,
|
|
580
|
+
components: {
|
|
581
|
+
...existingComponents,
|
|
582
|
+
views: {
|
|
583
|
+
...existingViews,
|
|
584
|
+
...managementViews,
|
|
585
|
+
},
|
|
586
|
+
afterNavLinks,
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
} as Config
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Payload plugin that initializes Better Auth.
|
|
594
|
+
*
|
|
595
|
+
* Better Auth is created in onInit (after Payload is ready) to avoid
|
|
596
|
+
* circular dependency issues. The auth instance is then attached to
|
|
597
|
+
* payload.betterAuth for access throughout the app.
|
|
598
|
+
*
|
|
599
|
+
* Features:
|
|
600
|
+
* - Auto-registers auth API endpoints (configurable)
|
|
601
|
+
* - Auto-injects admin components when disableLocalStrategy is detected
|
|
602
|
+
* - Auto-injects management UI for security features based on enabled plugins
|
|
603
|
+
* - Handles HMR gracefully
|
|
604
|
+
*
|
|
605
|
+
* @example
|
|
606
|
+
* ```ts
|
|
607
|
+
* import { createBetterAuthPlugin } from '@delmaredigital/payload-better-auth/plugin'
|
|
608
|
+
*
|
|
609
|
+
* export default buildConfig({
|
|
610
|
+
* plugins: [
|
|
611
|
+
* createBetterAuthPlugin({
|
|
612
|
+
* createAuth: (payload) => betterAuth({
|
|
613
|
+
* database: payloadAdapter({ payloadClient: payload, ... }),
|
|
614
|
+
* // ... other options
|
|
615
|
+
* }),
|
|
616
|
+
* }),
|
|
617
|
+
* ],
|
|
618
|
+
* })
|
|
619
|
+
* ```
|
|
620
|
+
*/
|
|
621
|
+
export function createBetterAuthPlugin(
|
|
622
|
+
options: BetterAuthPluginOptions
|
|
623
|
+
): Plugin {
|
|
624
|
+
const {
|
|
625
|
+
createAuth,
|
|
626
|
+
authBasePath = '/auth',
|
|
627
|
+
autoRegisterEndpoints = true,
|
|
628
|
+
autoInjectAdminComponents = true,
|
|
629
|
+
} = options
|
|
630
|
+
|
|
631
|
+
// Store API key scopes config for access by management views
|
|
632
|
+
apiKeyScopesConfig = options.admin?.apiKey
|
|
633
|
+
|
|
634
|
+
return (incomingConfig) => {
|
|
635
|
+
// Inject admin components if enabled
|
|
636
|
+
let config =
|
|
637
|
+
autoInjectAdminComponents
|
|
638
|
+
? injectAdminComponents(incomingConfig, options)
|
|
639
|
+
: incomingConfig
|
|
640
|
+
|
|
641
|
+
// Inject management UI components
|
|
642
|
+
config = injectManagementComponents(config, options)
|
|
643
|
+
|
|
644
|
+
// Generate auth endpoints if enabled
|
|
645
|
+
const authEndpoints = autoRegisterEndpoints
|
|
646
|
+
? generateAuthEndpoints(authBasePath)
|
|
647
|
+
: []
|
|
648
|
+
|
|
649
|
+
// Merge endpoints
|
|
650
|
+
const existingEndpoints = config.endpoints ?? []
|
|
651
|
+
|
|
652
|
+
// Get existing onInit
|
|
653
|
+
const existingOnInit = config.onInit
|
|
654
|
+
|
|
655
|
+
return {
|
|
656
|
+
...config,
|
|
657
|
+
endpoints: [...existingEndpoints, ...authEndpoints],
|
|
658
|
+
onInit: async (payload) => {
|
|
659
|
+
if (existingOnInit) {
|
|
660
|
+
await existingOnInit(payload)
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Check if already attached (HMR scenario)
|
|
664
|
+
if ('betterAuth' in payload) {
|
|
665
|
+
return
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Reuse or create auth instance
|
|
669
|
+
if (!authInstance) {
|
|
670
|
+
try {
|
|
671
|
+
authInstance = createAuth(payload)
|
|
672
|
+
} catch (error) {
|
|
673
|
+
console.error('[better-auth] Failed to create auth:', error)
|
|
674
|
+
throw error
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Attach to payload for global access
|
|
679
|
+
Object.defineProperty(payload, 'betterAuth', {
|
|
680
|
+
value: authInstance,
|
|
681
|
+
writable: false,
|
|
682
|
+
enumerable: false,
|
|
683
|
+
configurable: false,
|
|
684
|
+
})
|
|
685
|
+
},
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
export type BetterAuthStrategyOptions = {
|
|
691
|
+
/**
|
|
692
|
+
* The collection slug for users
|
|
693
|
+
* @default 'users'
|
|
694
|
+
*/
|
|
695
|
+
usersCollection?: string
|
|
696
|
+
/**
|
|
697
|
+
* The collection slug for organization members (used for organization role lookup)
|
|
698
|
+
* @default 'members'
|
|
699
|
+
*/
|
|
700
|
+
membersCollection?: string
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Payload auth strategy that uses Better Auth for authentication.
|
|
705
|
+
*
|
|
706
|
+
* Use this in your Users collection to authenticate via Better Auth sessions.
|
|
707
|
+
*
|
|
708
|
+
* Session fields (like `activeOrganizationId` from the organization plugin) are
|
|
709
|
+
* automatically merged onto `req.user`, making them available in access control functions.
|
|
710
|
+
*
|
|
711
|
+
* If an active organization is set, the user's role in that organization is also
|
|
712
|
+
* fetched and available as `req.user.organizationRole`.
|
|
713
|
+
*
|
|
714
|
+
* @example
|
|
715
|
+
* ```ts
|
|
716
|
+
* import { betterAuthStrategy } from '@delmaredigital/payload-better-auth/plugin'
|
|
717
|
+
*
|
|
718
|
+
* export const Users: CollectionConfig = {
|
|
719
|
+
* slug: 'users',
|
|
720
|
+
* auth: {
|
|
721
|
+
* disableLocalStrategy: true,
|
|
722
|
+
* strategies: [betterAuthStrategy()],
|
|
723
|
+
* },
|
|
724
|
+
* // ...
|
|
725
|
+
* }
|
|
726
|
+
* ```
|
|
727
|
+
*
|
|
728
|
+
* @example Access control with organization data
|
|
729
|
+
* ```ts
|
|
730
|
+
* // In your access control:
|
|
731
|
+
* export const orgReadAccess: Access = ({ req }) => {
|
|
732
|
+
* if (!req.user?.activeOrganizationId) return false
|
|
733
|
+
* return {
|
|
734
|
+
* organization: { equals: req.user.activeOrganizationId }
|
|
735
|
+
* }
|
|
736
|
+
* }
|
|
737
|
+
* ```
|
|
738
|
+
*/
|
|
739
|
+
export function betterAuthStrategy(
|
|
740
|
+
options: BetterAuthStrategyOptions = {}
|
|
741
|
+
): AuthStrategy {
|
|
742
|
+
const { usersCollection = 'users', membersCollection = 'members' } = options
|
|
743
|
+
|
|
744
|
+
return {
|
|
745
|
+
name: 'better-auth',
|
|
746
|
+
authenticate: async ({
|
|
747
|
+
payload,
|
|
748
|
+
headers,
|
|
749
|
+
}: {
|
|
750
|
+
payload: Payload
|
|
751
|
+
headers: Headers
|
|
752
|
+
}) => {
|
|
753
|
+
try {
|
|
754
|
+
const payloadWithAuth = payload as PayloadWithAuth
|
|
755
|
+
const auth = payloadWithAuth.betterAuth
|
|
756
|
+
|
|
757
|
+
if (!auth) {
|
|
758
|
+
console.error('Better Auth not initialized on payload instance')
|
|
759
|
+
return { user: null }
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const sessionData = await auth.api.getSession({ headers })
|
|
763
|
+
|
|
764
|
+
if (!sessionData?.user?.id) {
|
|
765
|
+
return { user: null }
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const users = await payload.find({
|
|
769
|
+
collection: usersCollection,
|
|
770
|
+
where: { id: { equals: sessionData.user.id } },
|
|
771
|
+
limit: 1,
|
|
772
|
+
depth: 0,
|
|
773
|
+
})
|
|
774
|
+
|
|
775
|
+
if (users.docs.length === 0) {
|
|
776
|
+
return { user: null }
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Extract session fields to merge onto user (e.g., activeOrganizationId from org plugin)
|
|
780
|
+
// Exclude fields that might conflict with user fields
|
|
781
|
+
const {
|
|
782
|
+
id: _sessionId,
|
|
783
|
+
userId: _userId,
|
|
784
|
+
expiresAt: _expiresAt,
|
|
785
|
+
token: _token,
|
|
786
|
+
...sessionFields
|
|
787
|
+
} = (sessionData.session as Record<string, unknown>) || {}
|
|
788
|
+
|
|
789
|
+
// If there's an active organization, fetch the user's role in that org
|
|
790
|
+
let organizationRole: string | undefined
|
|
791
|
+
if (sessionFields.activeOrganizationId) {
|
|
792
|
+
try {
|
|
793
|
+
const memberships = await payload.find({
|
|
794
|
+
collection: membersCollection,
|
|
795
|
+
where: {
|
|
796
|
+
and: [
|
|
797
|
+
{ user: { equals: sessionData.user.id } },
|
|
798
|
+
{ organization: { equals: sessionFields.activeOrganizationId } },
|
|
799
|
+
],
|
|
800
|
+
},
|
|
801
|
+
limit: 1,
|
|
802
|
+
depth: 0,
|
|
803
|
+
})
|
|
804
|
+
if (memberships.docs.length > 0) {
|
|
805
|
+
organizationRole = (memberships.docs[0] as { role?: string }).role
|
|
806
|
+
}
|
|
807
|
+
} catch {
|
|
808
|
+
// Members collection might not exist (org plugin not used), silently ignore
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
return {
|
|
813
|
+
user: {
|
|
814
|
+
...users.docs[0],
|
|
815
|
+
...sessionFields, // Merge session fields (activeOrganizationId, etc.)
|
|
816
|
+
...(organizationRole && { organizationRole }), // Add org role if found
|
|
817
|
+
collection: usersCollection,
|
|
818
|
+
_strategy: 'better-auth',
|
|
819
|
+
},
|
|
820
|
+
}
|
|
821
|
+
} catch (error) {
|
|
822
|
+
console.error('Better Auth strategy error:', error)
|
|
823
|
+
return { user: null }
|
|
824
|
+
}
|
|
825
|
+
},
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Reset the auth instance (useful for testing)
|
|
831
|
+
*/
|
|
832
|
+
export function resetAuthInstance(): void {
|
|
833
|
+
authInstance = null
|
|
834
|
+
}
|