@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.
- package/.turbo/turbo-typecheck.log +5 -0
- package/CHANGELOG.md +136 -0
- package/package.json +58 -0
- package/src/app-preview/AppPreview.tsx +304 -0
- package/src/app-preview/index.ts +16 -0
- package/src/auth/AdminAuthWrapper.tsx +20 -0
- package/src/auth/AuthProvider.tsx +241 -0
- package/src/auth/AutoLogin.tsx +70 -0
- package/src/auth/index.ts +4 -0
- package/src/authkit/ApiKeys.tsx +77 -0
- package/src/authkit/UserProfile.tsx +77 -0
- package/src/authkit/UserSecurity.tsx +77 -0
- package/src/authkit/WorkOSProvider.tsx +70 -0
- package/src/authkit/index.ts +34 -0
- package/src/authkit/theme.ts +102 -0
- package/src/authkit/useWidgetToken.ts +67 -0
- package/src/dev-tools/Browser.tsx +364 -0
- package/src/dev-tools/DevTools.tsx +106 -0
- package/src/dev-tools/Terminal.tsx +216 -0
- package/src/dev-tools/index.ts +4 -0
- package/src/index.ts +39 -0
- package/src/mdx-preview/MDXPreview.tsx +183 -0
- package/src/mdx-preview/MDXProvider.tsx +62 -0
- package/src/mdx-preview/PayloadMDXField.tsx +120 -0
- package/src/mdx-preview/index.ts +28 -0
- package/src/mdx-preview/useMDXCompiler.ts +95 -0
- package/src/site-preview/PayloadSiteField.tsx +167 -0
- package/src/site-preview/SitePreview.tsx +194 -0
- package/src/site-preview/index.ts +31 -0
- package/tsconfig.json +5 -0
|
@@ -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,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'
|