@mdxui/payload 6.0.1

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.
@@ -0,0 +1,241 @@
1
+ 'use client'
2
+
3
+ import { createContext, useContext, useEffect, useState, useCallback, useRef, ReactNode } from 'react'
4
+
5
+ interface AuthState {
6
+ isAuthenticated: boolean
7
+ isLoading: boolean
8
+ isRefreshing: boolean
9
+ user: { email: string; id: string } | null
10
+ error: string | null
11
+ }
12
+
13
+ interface AuthContextValue extends AuthState {
14
+ refreshAuth: () => Promise<void>
15
+ }
16
+
17
+ const AuthContext = createContext<AuthContextValue | null>(null)
18
+
19
+ export function useAuth() {
20
+ const context = useContext(AuthContext)
21
+ if (!context) {
22
+ throw new Error('useAuth must be used within AuthProvider')
23
+ }
24
+ return context
25
+ }
26
+
27
+ interface AuthProviderProps {
28
+ children: ReactNode
29
+ /** How often to check auth status (ms). Default: 60000 (1 minute) */
30
+ checkInterval?: number
31
+ }
32
+
33
+ /**
34
+ * AuthProvider that monitors authentication status and handles token refresh.
35
+ *
36
+ * When the token expires, it silently refreshes using a hidden iframe
37
+ * so the user isn't interrupted.
38
+ */
39
+ export function AuthProvider({ children, checkInterval = 60000 }: AuthProviderProps) {
40
+ const [state, setState] = useState<AuthState>({
41
+ isAuthenticated: false,
42
+ isLoading: true,
43
+ isRefreshing: false,
44
+ user: null,
45
+ error: null,
46
+ })
47
+ const iframeRef = useRef<HTMLIFrameElement | null>(null)
48
+ const refreshAttempts = useRef(0)
49
+
50
+ // Silent token refresh using hidden iframe
51
+ const silentRefresh = useCallback(() => {
52
+ return new Promise<boolean>((resolve) => {
53
+ console.log('[AuthProvider] Starting silent token refresh...')
54
+ setState(prev => ({ ...prev, isRefreshing: true }))
55
+
56
+ // Create hidden iframe if it doesn't exist
57
+ if (!iframeRef.current) {
58
+ const iframe = document.createElement('iframe')
59
+ iframe.style.display = 'none'
60
+ iframe.id = 'auth-refresh-iframe'
61
+ document.body.appendChild(iframe)
62
+ iframeRef.current = iframe
63
+ }
64
+
65
+ const iframe = iframeRef.current
66
+ let resolved = false
67
+
68
+ const cleanup = () => {
69
+ iframe.removeEventListener('load', onLoad)
70
+ window.removeEventListener('message', onMessage)
71
+ clearTimeout(timeout)
72
+ }
73
+
74
+ // Listen for postMessage from the refresh-complete page
75
+ const onMessage = (event: MessageEvent) => {
76
+ if (event.data?.type === 'auth-refresh-complete') {
77
+ if (resolved) return
78
+ resolved = true
79
+ console.log('[AuthProvider] Silent refresh complete (via postMessage)')
80
+ setState(prev => ({ ...prev, isRefreshing: false }))
81
+ cleanup()
82
+ resolve(true)
83
+ }
84
+ }
85
+
86
+ // Fallback: detect via iframe load event
87
+ const onLoad = () => {
88
+ // Wait a bit for postMessage, but resolve anyway after delay
89
+ setTimeout(() => {
90
+ if (resolved) return
91
+ resolved = true
92
+ console.log('[AuthProvider] Silent refresh complete (via load event)')
93
+ setState(prev => ({ ...prev, isRefreshing: false }))
94
+ cleanup()
95
+ resolve(true)
96
+ }, 200)
97
+ }
98
+
99
+ // Set up timeout
100
+ const timeout = setTimeout(() => {
101
+ if (resolved) return
102
+ resolved = true
103
+ console.log('[AuthProvider] Silent refresh timed out')
104
+ setState(prev => ({ ...prev, isRefreshing: false }))
105
+ cleanup()
106
+ resolve(false)
107
+ }, 10000) // 10 second timeout
108
+
109
+ window.addEventListener('message', onMessage)
110
+ iframe.addEventListener('load', onLoad)
111
+
112
+ // Navigate iframe to auto-login (which will set the cookie and redirect)
113
+ iframe.src = '/api/auth/auto-login?redirect=/api/auth/refresh-complete&silent=1'
114
+ })
115
+ }, [])
116
+
117
+ const checkAuth = useCallback(async () => {
118
+ // IMPORTANT: Never try to redirect or refresh when on login page
119
+ // Let AutoLogin component handle authentication there
120
+ const isLoginPage = window.location.pathname === '/login'
121
+
122
+ try {
123
+ const response = await fetch('/api/admins/me', {
124
+ credentials: 'include',
125
+ })
126
+
127
+ if (!response.ok) {
128
+ throw new Error('Auth check failed')
129
+ }
130
+
131
+ const data = await response.json()
132
+
133
+ if (data.user) {
134
+ refreshAttempts.current = 0 // Reset on success
135
+ setState(prev => ({
136
+ ...prev,
137
+ isAuthenticated: true,
138
+ isLoading: false,
139
+ user: { email: data.user.email, id: data.user.id },
140
+ error: null,
141
+ }))
142
+ } else {
143
+ // Token expired or invalid
144
+ if (isLoginPage) {
145
+ // On login page - just update state, let AutoLogin handle redirect
146
+ setState(prev => ({
147
+ ...prev,
148
+ isAuthenticated: false,
149
+ isLoading: false,
150
+ user: null,
151
+ error: 'Not authenticated',
152
+ }))
153
+ return
154
+ }
155
+
156
+ // Not on login page - try silent refresh (up to 3 times)
157
+ if (refreshAttempts.current < 3) {
158
+ refreshAttempts.current++
159
+ console.log(`[AuthProvider] Token expired, attempting silent refresh (${refreshAttempts.current}/3)...`)
160
+ const success = await silentRefresh()
161
+ if (success) {
162
+ // Re-check auth after refresh
163
+ setTimeout(() => checkAuth(), 500)
164
+ return
165
+ }
166
+ }
167
+
168
+ // Silent refresh failed - redirect to login
169
+ // Capture current path BEFORE any redirect
170
+ const currentPath = window.location.pathname + window.location.search
171
+ // Don't include /login in redirect to avoid loops
172
+ const redirectPath = currentPath.startsWith('/login') ? '/' : currentPath
173
+ console.log('[AuthProvider] Silent refresh failed, redirecting to login...')
174
+ window.location.href = `/api/auth/auto-login?redirect=${encodeURIComponent(redirectPath)}`
175
+ }
176
+ } catch (error) {
177
+ console.error('[AuthProvider] Auth check error:', error)
178
+ if (isLoginPage) {
179
+ setState(prev => ({
180
+ ...prev,
181
+ isAuthenticated: false,
182
+ isLoading: false,
183
+ user: null,
184
+ error: String(error),
185
+ }))
186
+ return
187
+ }
188
+ // Try silent refresh on error too
189
+ if (refreshAttempts.current < 3) {
190
+ refreshAttempts.current++
191
+ const success = await silentRefresh()
192
+ if (success) {
193
+ setTimeout(() => checkAuth(), 500)
194
+ return
195
+ }
196
+ }
197
+ const currentPath = window.location.pathname + window.location.search
198
+ const redirectPath = currentPath.startsWith('/login') ? '/' : currentPath
199
+ window.location.href = `/api/auth/auto-login?redirect=${encodeURIComponent(redirectPath)}`
200
+ }
201
+ }, [silentRefresh])
202
+
203
+ const refreshAuth = useCallback(async () => {
204
+ setState(prev => ({ ...prev, isLoading: true }))
205
+ await checkAuth()
206
+ }, [checkAuth])
207
+
208
+ // Initial auth check
209
+ useEffect(() => {
210
+ checkAuth()
211
+ }, [checkAuth])
212
+
213
+ // Periodic auth check to detect token expiration
214
+ useEffect(() => {
215
+ if (!state.isAuthenticated) return
216
+
217
+ const interval = setInterval(() => {
218
+ checkAuth()
219
+ }, checkInterval)
220
+
221
+ return () => clearInterval(interval)
222
+ }, [state.isAuthenticated, checkInterval, checkAuth])
223
+
224
+ // Also check on window focus (user might have been away)
225
+ useEffect(() => {
226
+ const handleFocus = () => {
227
+ if (state.isAuthenticated) {
228
+ checkAuth()
229
+ }
230
+ }
231
+
232
+ window.addEventListener('focus', handleFocus)
233
+ return () => window.removeEventListener('focus', handleFocus)
234
+ }, [state.isAuthenticated, checkAuth])
235
+
236
+ return (
237
+ <AuthContext.Provider value={{ ...state, refreshAuth }}>
238
+ {children}
239
+ </AuthContext.Provider>
240
+ )
241
+ }
@@ -0,0 +1,70 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState, useRef } from 'react'
4
+
5
+ /**
6
+ * Custom login component that auto-redirects to oauth.do login
7
+ * Replaces Payload's default login form
8
+ */
9
+ export default function AutoLogin() {
10
+ const [status, setStatus] = useState('Checking authentication...')
11
+ const redirectedRef = useRef(false)
12
+
13
+ useEffect(() => {
14
+ // Prevent double-redirect from React strict mode or multiple renders
15
+ if (redirectedRef.current) return
16
+
17
+ // Check if we already have an oauth token cookie
18
+ const hasCookie = document.cookie.includes('oauth-token=')
19
+
20
+ // Check URL for loop detection
21
+ const url = new URL(window.location.href)
22
+ const loopCount = parseInt(url.searchParams.get('_loop') || '0')
23
+
24
+ if (loopCount > 5) {
25
+ setStatus('Authentication failed. Please run "oauth.do login" in terminal and refresh.')
26
+ return
27
+ }
28
+
29
+ if (hasCookie) {
30
+ // Already have cookie - verify it's valid before redirecting
31
+ setStatus('Verifying authentication...')
32
+ redirectedRef.current = true
33
+
34
+ fetch('/api/admins/me', { credentials: 'include' })
35
+ .then(res => res.json())
36
+ .then(data => {
37
+ if (data.user) {
38
+ setStatus('Authenticated. Loading dashboard...')
39
+ window.location.href = '/'
40
+ } else {
41
+ // Cookie exists but token is invalid - get a fresh one
42
+ setStatus('Refreshing token...')
43
+ window.location.href = `/api/auth/auto-login?redirect=/&_loop=${loopCount + 1}`
44
+ }
45
+ })
46
+ .catch(() => {
47
+ // Error checking auth - try to refresh token
48
+ window.location.href = `/api/auth/auto-login?redirect=/&_loop=${loopCount + 1}`
49
+ })
50
+ return
51
+ }
52
+
53
+ // No cookie, redirect to auto-login with loop counter
54
+ redirectedRef.current = true
55
+ setStatus('Redirecting to login...')
56
+ window.location.href = `/api/auth/auto-login?redirect=/&_loop=${loopCount + 1}`
57
+ }, [])
58
+
59
+ return (
60
+ <div style={{
61
+ display: 'flex',
62
+ justifyContent: 'center',
63
+ alignItems: 'center',
64
+ height: '100vh',
65
+ fontFamily: 'system-ui, sans-serif'
66
+ }}>
67
+ <p>{status}</p>
68
+ </div>
69
+ )
70
+ }
@@ -0,0 +1,4 @@
1
+ // Auth components for Payload CMS admin
2
+ export { AdminAuthWrapper } from './AdminAuthWrapper'
3
+ export { AuthProvider, useAuth } from './AuthProvider'
4
+ export { default as AutoLogin } from './AutoLogin'
@@ -0,0 +1,77 @@
1
+ 'use client'
2
+
3
+ import { ApiKeys as WorkOSApiKeys } from '@mdxui/auth/widgets'
4
+ import { WorkOSProvider } from './WorkOSProvider'
5
+ import { useWidgetToken } from './useWidgetToken'
6
+
7
+ interface ApiKeysProps {
8
+ /**
9
+ * Organization ID for the API keys (required)
10
+ */
11
+ organizationId: string
12
+ }
13
+
14
+ /**
15
+ * API Keys Widget
16
+ *
17
+ * Displays and manages API keys for an organization:
18
+ * - Create new API keys with specific permissions
19
+ * - View existing API keys
20
+ * - Revoke API keys
21
+ *
22
+ * Requires the `widgets:api-keys:manage` permission.
23
+ * Themed to match Payload CMS admin styling.
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * <ApiKeys organizationId="org_123" />
28
+ * ```
29
+ */
30
+ export function ApiKeys({ organizationId }: ApiKeysProps) {
31
+ const { token, loading, error } = useWidgetToken('api-keys', organizationId)
32
+
33
+ if (loading) {
34
+ return (
35
+ <div className="workos-widget-container">
36
+ <div className="workos-widget-loading">
37
+ <p>Loading API keys...</p>
38
+ </div>
39
+ </div>
40
+ )
41
+ }
42
+
43
+ if (error) {
44
+ return (
45
+ <div className="workos-widget-container">
46
+ <div className="workos-widget-error">
47
+ <h3>Unable to load API keys</h3>
48
+ <p>{error}</p>
49
+ <p className="workos-widget-hint">
50
+ Make sure you have the required permissions and WorkOS is configured correctly.
51
+ </p>
52
+ </div>
53
+ </div>
54
+ )
55
+ }
56
+
57
+ if (!token) {
58
+ return (
59
+ <div className="workos-widget-container">
60
+ <div className="workos-widget-error">
61
+ <h3>Authentication required</h3>
62
+ <p>Please log in to manage API keys.</p>
63
+ </div>
64
+ </div>
65
+ )
66
+ }
67
+
68
+ return (
69
+ <WorkOSProvider>
70
+ <div className="workos-widget-container">
71
+ <WorkOSApiKeys authToken={token} />
72
+ </div>
73
+ </WorkOSProvider>
74
+ )
75
+ }
76
+
77
+ export default ApiKeys
@@ -0,0 +1,77 @@
1
+ 'use client'
2
+
3
+ import { UserProfile as WorkOSUserProfile } from '@mdxui/auth/widgets'
4
+ import { WorkOSProvider } from './WorkOSProvider'
5
+ import { useWidgetToken } from './useWidgetToken'
6
+
7
+ interface UserProfileProps {
8
+ /**
9
+ * Optional organization ID context
10
+ */
11
+ organizationId?: string
12
+ }
13
+
14
+ /**
15
+ * User Profile Widget
16
+ *
17
+ * Displays user profile information including:
18
+ * - Profile picture
19
+ * - Name and email
20
+ * - Connected accounts (OAuth providers)
21
+ * - Profile editing capabilities
22
+ *
23
+ * Themed to match Payload CMS admin styling.
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * <UserProfile />
28
+ * ```
29
+ */
30
+ export function UserProfile({ organizationId }: UserProfileProps) {
31
+ const { token, loading, error } = useWidgetToken('user-profile', organizationId)
32
+
33
+ if (loading) {
34
+ return (
35
+ <div className="workos-widget-container">
36
+ <div className="workos-widget-loading">
37
+ <p>Loading profile...</p>
38
+ </div>
39
+ </div>
40
+ )
41
+ }
42
+
43
+ if (error) {
44
+ return (
45
+ <div className="workos-widget-container">
46
+ <div className="workos-widget-error">
47
+ <h3>Unable to load profile</h3>
48
+ <p>{error}</p>
49
+ <p className="workos-widget-hint">
50
+ Make sure you are authenticated and WorkOS is configured correctly.
51
+ </p>
52
+ </div>
53
+ </div>
54
+ )
55
+ }
56
+
57
+ if (!token) {
58
+ return (
59
+ <div className="workos-widget-container">
60
+ <div className="workos-widget-error">
61
+ <h3>Authentication required</h3>
62
+ <p>Please log in to view your profile.</p>
63
+ </div>
64
+ </div>
65
+ )
66
+ }
67
+
68
+ return (
69
+ <WorkOSProvider>
70
+ <div className="workos-widget-container">
71
+ <WorkOSUserProfile authToken={token} />
72
+ </div>
73
+ </WorkOSProvider>
74
+ )
75
+ }
76
+
77
+ export default UserProfile
@@ -0,0 +1,77 @@
1
+ 'use client'
2
+
3
+ import { UserSecurity as WorkOSUserSecurity } from '@mdxui/auth/widgets'
4
+ import { WorkOSProvider } from './WorkOSProvider'
5
+ import { useWidgetToken } from './useWidgetToken'
6
+
7
+ interface UserSecurityProps {
8
+ /**
9
+ * Optional organization ID context
10
+ */
11
+ organizationId?: string
12
+ }
13
+
14
+ /**
15
+ * User Security Widget
16
+ *
17
+ * Allows users to manage their security settings:
18
+ * - Password management
19
+ * - Multi-factor authentication (MFA)
20
+ * - Connected authentication methods
21
+ * - Security keys
22
+ *
23
+ * Themed to match Payload CMS admin styling.
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * <UserSecurity />
28
+ * ```
29
+ */
30
+ export function UserSecurity({ organizationId }: UserSecurityProps) {
31
+ const { token, loading, error } = useWidgetToken('user-security', organizationId)
32
+
33
+ if (loading) {
34
+ return (
35
+ <div className="workos-widget-container">
36
+ <div className="workos-widget-loading">
37
+ <p>Loading security settings...</p>
38
+ </div>
39
+ </div>
40
+ )
41
+ }
42
+
43
+ if (error) {
44
+ return (
45
+ <div className="workos-widget-container">
46
+ <div className="workos-widget-error">
47
+ <h3>Unable to load security settings</h3>
48
+ <p>{error}</p>
49
+ <p className="workos-widget-hint">
50
+ Make sure you are authenticated and WorkOS is configured correctly.
51
+ </p>
52
+ </div>
53
+ </div>
54
+ )
55
+ }
56
+
57
+ if (!token) {
58
+ return (
59
+ <div className="workos-widget-container">
60
+ <div className="workos-widget-error">
61
+ <h3>Authentication required</h3>
62
+ <p>Please log in to manage your security settings.</p>
63
+ </div>
64
+ </div>
65
+ )
66
+ }
67
+
68
+ return (
69
+ <WorkOSProvider>
70
+ <div className="workos-widget-container">
71
+ <WorkOSUserSecurity authToken={token} />
72
+ </div>
73
+ </WorkOSProvider>
74
+ )
75
+ }
76
+
77
+ export default UserSecurity
@@ -0,0 +1,70 @@
1
+ 'use client'
2
+
3
+ import { WidgetsProvider } from '@mdxui/auth/providers'
4
+ import { Theme } from '@radix-ui/themes'
5
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
6
+ import { type ReactNode, useEffect } from 'react'
7
+ import { injectThemeStyles, payloadAuthKitTheme } from './theme'
8
+
9
+ // Import required CSS
10
+ import '@radix-ui/themes/styles.css'
11
+
12
+ /**
13
+ * QueryClient instance for TanStack Query
14
+ * Used by WorkOS widgets for data fetching and caching
15
+ */
16
+ const queryClient = new QueryClient({
17
+ defaultOptions: {
18
+ queries: {
19
+ staleTime: 5 * 60 * 1000, // 5 minutes
20
+ retry: 1,
21
+ },
22
+ },
23
+ })
24
+
25
+ interface WorkOSProviderProps {
26
+ children: ReactNode
27
+ }
28
+
29
+ /**
30
+ * WorkOS Provider Component
31
+ *
32
+ * Wraps children with all necessary providers for AuthKit widgets:
33
+ * - TanStack Query for data fetching
34
+ * - Radix Themes for UI styling
35
+ * - WorkOsWidgets context
36
+ *
37
+ * Also injects custom theme styles to match Payload CMS.
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * <WorkOSProvider>
42
+ * <UserProfile />
43
+ * </WorkOSProvider>
44
+ * ```
45
+ */
46
+ export function WorkOSProvider({ children }: WorkOSProviderProps) {
47
+ // Inject custom theme styles on mount
48
+ useEffect(() => {
49
+ injectThemeStyles()
50
+ }, [])
51
+
52
+ return (
53
+ <QueryClientProvider client={queryClient}>
54
+ <Theme
55
+ appearance={payloadAuthKitTheme.appearance}
56
+ accentColor={payloadAuthKitTheme.accentColor}
57
+ radius={payloadAuthKitTheme.radius}
58
+ grayColor={payloadAuthKitTheme.grayColor}
59
+ scaling={payloadAuthKitTheme.scaling}
60
+ className="workos-widgets"
61
+ >
62
+ <WidgetsProvider>
63
+ {children}
64
+ </WidgetsProvider>
65
+ </Theme>
66
+ </QueryClientProvider>
67
+ )
68
+ }
69
+
70
+ export default WorkOSProvider
@@ -0,0 +1,34 @@
1
+ /**
2
+ * AuthKit Widget Components
3
+ *
4
+ * WorkOS AuthKit widgets themed and configured for Payload CMS admin.
5
+ * These components provide user management functionality directly in the admin UI.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import { UserProfile, UserSecurity, WorkOSProvider } from '@mdxui/payload/authkit'
10
+ *
11
+ * // Standalone widget
12
+ * <UserProfile />
13
+ *
14
+ * // With provider for multiple widgets
15
+ * <WorkOSProvider>
16
+ * <UserProfile />
17
+ * <UserSecurity />
18
+ * </WorkOSProvider>
19
+ * ```
20
+ */
21
+
22
+ // Provider
23
+ export { WorkOSProvider } from './WorkOSProvider'
24
+
25
+ // Widgets
26
+ export { ApiKeys } from './ApiKeys'
27
+ export { UserProfile } from './UserProfile'
28
+ export { UserSecurity } from './UserSecurity'
29
+
30
+ // Hooks
31
+ export { useWidgetToken } from './useWidgetToken'
32
+
33
+ // Theme
34
+ export { payloadAuthKitTheme, injectThemeStyles, customThemeStyles } from './theme'