@djangocfg/layouts 2.0.7 → 2.0.9
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 +65 -6
- package/package.json +5 -5
- package/src/auth/context/AuthContext.tsx +11 -6
- package/src/auth/hooks/index.ts +1 -0
- package/src/auth/hooks/useAuthGuard.ts +2 -2
- package/src/auth/hooks/useAutoAuth.ts +2 -2
- package/src/auth/hooks/useGithubAuth.ts +184 -0
- package/src/components/RedirectPage/RedirectPage.tsx +2 -2
- package/src/layouts/AuthLayout/AuthContext.tsx +2 -0
- package/src/layouts/AuthLayout/AuthLayout.tsx +22 -5
- package/src/layouts/AuthLayout/IdentifierForm.tsx +4 -0
- package/src/layouts/AuthLayout/OAuthCallback.tsx +172 -0
- package/src/layouts/AuthLayout/OAuthProviders.tsx +85 -0
- package/src/layouts/AuthLayout/index.ts +4 -0
- package/src/layouts/AuthLayout/types.ts +4 -0
- package/src/snippets/Analytics/events.ts +5 -0
- package/src/snippets/AuthDialog/AuthDialog.tsx +2 -2
- package/src/snippets/McpChat/components/AIChatWidget.tsx +3 -39
- package/src/snippets/McpChat/components/ChatMessages.tsx +2 -2
- package/src/snippets/McpChat/components/ChatPanel.tsx +84 -110
- package/src/snippets/McpChat/components/ChatSidebar.tsx +66 -60
- package/src/snippets/McpChat/components/ChatWidget.tsx +4 -37
- package/src/snippets/McpChat/components/MessageBubble.tsx +5 -5
- package/src/snippets/McpChat/components/index.ts +0 -2
- package/src/snippets/McpChat/config.ts +42 -0
- package/src/snippets/McpChat/context/ChatContext.tsx +5 -7
- package/src/snippets/McpChat/hooks/useChatLayout.ts +134 -23
- package/src/snippets/McpChat/index.ts +0 -1
- package/src/snippets/index.ts +0 -1
package/README.md
CHANGED
|
@@ -71,15 +71,21 @@ import { AppLayout } from '@djangocfg/layouts';
|
|
|
71
71
|
|
|
72
72
|
## Auth
|
|
73
73
|
|
|
74
|
-
Complete authentication system with
|
|
74
|
+
Complete authentication system with OTP and OAuth support.
|
|
75
75
|
|
|
76
76
|
```tsx
|
|
77
77
|
import { AuthProvider, useAuth } from '@djangocfg/layouts/auth';
|
|
78
|
-
import {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
import { AuthLayout, OAuthCallback } from '@djangocfg/layouts';
|
|
79
|
+
|
|
80
|
+
// Basic auth page with OTP and GitHub OAuth
|
|
81
|
+
<AuthLayout
|
|
82
|
+
enablePhoneAuth={false}
|
|
83
|
+
enableGithubAuth={true}
|
|
84
|
+
termsUrl="/legal/terms"
|
|
85
|
+
privacyUrl="/legal/privacy"
|
|
86
|
+
>
|
|
87
|
+
<h1>Welcome Back</h1>
|
|
88
|
+
</AuthLayout>
|
|
83
89
|
```
|
|
84
90
|
|
|
85
91
|
| Export | Description |
|
|
@@ -89,8 +95,59 @@ import { AuthDialog } from '@djangocfg/layouts/snippets';
|
|
|
89
95
|
| `useAuthGuard` | Route protection hook |
|
|
90
96
|
| `useAuthRedirect` | Redirect hook for auth flows |
|
|
91
97
|
| `useAutoAuth` | Auto-authentication hook |
|
|
98
|
+
| `useGithubAuth` | GitHub OAuth hook |
|
|
92
99
|
| `authMiddleware` | Next.js middleware |
|
|
93
100
|
|
|
101
|
+
### GitHub OAuth
|
|
102
|
+
|
|
103
|
+
Complete GitHub OAuth flow with automatic token handling:
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
// app/auth/page.tsx
|
|
107
|
+
import { AuthLayout } from '@djangocfg/layouts';
|
|
108
|
+
|
|
109
|
+
export default function AuthPage() {
|
|
110
|
+
return (
|
|
111
|
+
<AuthLayout
|
|
112
|
+
enableGithubAuth={true}
|
|
113
|
+
redirectUrl="/dashboard"
|
|
114
|
+
onOAuthSuccess={(user, isNewUser, provider) => console.log('Success!', user)}
|
|
115
|
+
onError={(error) => console.error(error)}
|
|
116
|
+
>
|
|
117
|
+
<h1>Sign In</h1>
|
|
118
|
+
</AuthLayout>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**OAuth Flow:**
|
|
124
|
+
1. User clicks "Continue with GitHub"
|
|
125
|
+
2. Redirects to GitHub authorization page
|
|
126
|
+
3. GitHub redirects to `/auth?provider=github&code=XXX&state=YYY`
|
|
127
|
+
4. `AuthLayout` automatically handles callback and exchanges code for JWT tokens
|
|
128
|
+
5. User is logged in and redirected to `redirectUrl`
|
|
129
|
+
|
|
130
|
+
> **Note:** OAuth callback handling is built into `AuthLayout` when `enableGithubAuth={true}`. No need to add `OAuthCallback` separately!
|
|
131
|
+
|
|
132
|
+
**Using the hook directly:**
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import { useGithubAuth } from '@djangocfg/layouts';
|
|
136
|
+
|
|
137
|
+
function CustomGithubButton() {
|
|
138
|
+
const { isLoading, startGithubAuth } = useGithubAuth({
|
|
139
|
+
onSuccess: (user) => console.log('Logged in!', user),
|
|
140
|
+
onError: (error) => console.error(error),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<button onClick={startGithubAuth} disabled={isLoading}>
|
|
145
|
+
{isLoading ? 'Connecting...' : 'Login with GitHub'}
|
|
146
|
+
</button>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
94
151
|
### Auth Context
|
|
95
152
|
|
|
96
153
|
```tsx
|
|
@@ -152,6 +209,7 @@ Analytics.setUser('user-123');
|
|
|
152
209
|
| Category | Events |
|
|
153
210
|
|----------|--------|
|
|
154
211
|
| **Auth** | `AUTH_OTP_REQUEST`, `AUTH_LOGIN_SUCCESS`, `AUTH_OTP_VERIFY_FAIL`, `AUTH_LOGOUT`, `AUTH_SESSION_EXPIRED`, `AUTH_TOKEN_REFRESH` |
|
|
212
|
+
| **OAuth** | `AUTH_OAUTH_START`, `AUTH_OAUTH_SUCCESS`, `AUTH_OAUTH_FAIL` |
|
|
155
213
|
| **Error** | `ERROR_BOUNDARY`, `ERROR_API`, `ERROR_VALIDATION`, `ERROR_NETWORK` |
|
|
156
214
|
| **Navigation** | `NAV_ADMIN_ENTER`, `NAV_DASHBOARD_ENTER`, `NAV_PAGE_VIEW` |
|
|
157
215
|
| **Engagement** | `THEME_CHANGE`, `SIDEBAR_TOGGLE`, `MOBILE_MENU_OPEN` |
|
|
@@ -162,6 +220,7 @@ Built-in tracking for:
|
|
|
162
220
|
- **Page views** - on every route change
|
|
163
221
|
- **User ID** - automatically set when user is authenticated
|
|
164
222
|
- **Auth events** - login, logout, OTP, session expiry
|
|
223
|
+
- **OAuth events** - GitHub OAuth start, success, failure
|
|
165
224
|
- **Errors** - React ErrorBoundary errors
|
|
166
225
|
|
|
167
226
|
## Snippets
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.9",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -92,9 +92,9 @@
|
|
|
92
92
|
"check": "tsc --noEmit"
|
|
93
93
|
},
|
|
94
94
|
"peerDependencies": {
|
|
95
|
-
"@djangocfg/api": "^1.4.
|
|
96
|
-
"@djangocfg/centrifugo": "^1.4.
|
|
97
|
-
"@djangocfg/ui": "^1.4.
|
|
95
|
+
"@djangocfg/api": "^1.4.39",
|
|
96
|
+
"@djangocfg/centrifugo": "^1.4.39",
|
|
97
|
+
"@djangocfg/ui": "^1.4.39",
|
|
98
98
|
"@hookform/resolvers": "^5.2.0",
|
|
99
99
|
"consola": "^3.4.2",
|
|
100
100
|
"lucide-react": "^0.545.0",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"uuid": "^11.1.0"
|
|
115
115
|
},
|
|
116
116
|
"devDependencies": {
|
|
117
|
-
"@djangocfg/typescript-config": "^1.4.
|
|
117
|
+
"@djangocfg/typescript-config": "^1.4.39",
|
|
118
118
|
"@types/node": "^24.7.2",
|
|
119
119
|
"@types/react": "19.2.2",
|
|
120
120
|
"@types/react-dom": "19.2.1",
|
|
@@ -8,7 +8,7 @@ import React, {
|
|
|
8
8
|
|
|
9
9
|
import { api, Enums } from '@djangocfg/api';
|
|
10
10
|
import { useAccountsContext, AccountsProvider } from './AccountsContext';
|
|
11
|
-
import { useLocalStorage, useQueryParams,
|
|
11
|
+
import { useLocalStorage, useQueryParams, useCfgRouter } from '@djangocfg/ui/hooks';
|
|
12
12
|
import { getCachedProfile, clearProfileCache } from '../hooks/useProfileCache';
|
|
13
13
|
|
|
14
14
|
import { authLogger } from '../../utils/logger';
|
|
@@ -50,7 +50,7 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
const [initialized, setInitialized] = useState(false);
|
|
53
|
-
const router =
|
|
53
|
+
const router = useCfgRouter();
|
|
54
54
|
const pathname = usePathname();
|
|
55
55
|
const queryParams = useQueryParams();
|
|
56
56
|
|
|
@@ -362,13 +362,14 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
362
362
|
}
|
|
363
363
|
|
|
364
364
|
// Handle redirect logic here
|
|
365
|
+
// Use hardPush for full page reload - ensures all React contexts reinitialize
|
|
365
366
|
const defaultCallback = config?.routes?.defaultCallback || defaultRoutes.defaultCallback;
|
|
366
367
|
|
|
367
368
|
if (redirectUrl && redirectUrl !== defaultCallback) {
|
|
368
369
|
clearRedirectUrl();
|
|
369
|
-
router.
|
|
370
|
+
router.hardPush(redirectUrl);
|
|
370
371
|
} else {
|
|
371
|
-
router.
|
|
372
|
+
router.hardPush(defaultCallback);
|
|
372
373
|
}
|
|
373
374
|
|
|
374
375
|
return {
|
|
@@ -460,7 +461,11 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
460
461
|
accounts.logout(); // Clear tokens and profile
|
|
461
462
|
setInitialized(true);
|
|
462
463
|
setIsLoading(false);
|
|
463
|
-
|
|
464
|
+
|
|
465
|
+
// Use hardReplace for full page reload + replace history
|
|
466
|
+
// This ensures contexts reinitialize AND back button won't return to protected page
|
|
467
|
+
const authCallbackUrl = config?.routes?.defaultAuthCallback || defaultRoutes.defaultAuthCallback;
|
|
468
|
+
router.hardReplace(authCallbackUrl);
|
|
464
469
|
};
|
|
465
470
|
|
|
466
471
|
// Use config.onConfirm if provided, otherwise use a simple confirm
|
|
@@ -482,7 +487,7 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
482
487
|
performLogout();
|
|
483
488
|
}
|
|
484
489
|
}
|
|
485
|
-
}, [accounts,
|
|
490
|
+
}, [accounts, config?.routes?.defaultAuthCallback, router]);
|
|
486
491
|
|
|
487
492
|
// Redirect URL methods
|
|
488
493
|
const getSavedRedirectUrl = useCallback((): string | null => {
|
package/src/auth/hooks/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { useSessionStorage } from './useSessionStorage';
|
|
|
6
6
|
export { useLocalStorage } from './useLocalStorage';
|
|
7
7
|
export { useAuthForm } from './useAuthForm';
|
|
8
8
|
export { useAutoAuth } from './useAutoAuth';
|
|
9
|
+
export { useGithubAuth, type UseGithubAuthOptions, type UseGithubAuthReturn } from './useGithubAuth';
|
|
9
10
|
export {
|
|
10
11
|
getCachedProfile,
|
|
11
12
|
setCachedProfile,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useEffect } from 'react';
|
|
4
4
|
|
|
5
5
|
import { useAuth } from '../context';
|
|
6
|
-
import {
|
|
6
|
+
import { useCfgRouter } from '@djangocfg/ui/hooks';
|
|
7
7
|
|
|
8
8
|
interface UseAuthGuardOptions {
|
|
9
9
|
redirectTo?: string;
|
|
@@ -13,7 +13,7 @@ interface UseAuthGuardOptions {
|
|
|
13
13
|
export const useAuthGuard = (options: UseAuthGuardOptions = {}) => {
|
|
14
14
|
const { redirectTo = '/auth', requireAuth = true } = options;
|
|
15
15
|
const { isAuthenticated, isLoading } = useAuth();
|
|
16
|
-
const router =
|
|
16
|
+
const router = useCfgRouter();
|
|
17
17
|
|
|
18
18
|
useEffect(() => {
|
|
19
19
|
if (!isLoading && requireAuth && !isAuthenticated) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect } from 'react';
|
|
4
4
|
import { usePathname } from 'next/navigation';
|
|
5
|
-
import { useQueryParams,
|
|
5
|
+
import { useQueryParams, useCfgRouter } from '@djangocfg/ui/hooks';
|
|
6
6
|
import { authLogger } from '../../utils/logger';
|
|
7
7
|
|
|
8
8
|
export interface UseAutoAuthOptions {
|
|
@@ -18,7 +18,7 @@ export const useAutoAuth = (options: UseAutoAuthOptions = {}) => {
|
|
|
18
18
|
const { onOTPDetected, cleanupUrl = true } = options;
|
|
19
19
|
const queryParams = useQueryParams();
|
|
20
20
|
const pathname = usePathname();
|
|
21
|
-
const router =
|
|
21
|
+
const router = useCfgRouter();
|
|
22
22
|
|
|
23
23
|
const isReady = !!pathname && !!queryParams.get('otp');
|
|
24
24
|
const hasOTP = !!(queryParams.get('otp'));
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import { api } from '@djangocfg/api';
|
|
6
|
+
import { useCfgRouter } from '@djangocfg/ui/hooks';
|
|
7
|
+
import { authLogger } from '../../utils/logger';
|
|
8
|
+
import { Analytics, AnalyticsEvent, AnalyticsCategory } from '../../snippets/Analytics';
|
|
9
|
+
|
|
10
|
+
export interface UseGithubAuthOptions {
|
|
11
|
+
sourceUrl?: string;
|
|
12
|
+
onSuccess?: (user: any, isNewUser: boolean) => void;
|
|
13
|
+
onError?: (error: string) => void;
|
|
14
|
+
redirectUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface UseGithubAuthReturn {
|
|
18
|
+
isLoading: boolean;
|
|
19
|
+
error: string | null;
|
|
20
|
+
startGithubAuth: () => Promise<void>;
|
|
21
|
+
handleGithubCallback: (code: string, state: string) => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Hook for GitHub OAuth authentication flow.
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* 1. Call startGithubAuth() to redirect user to GitHub
|
|
29
|
+
* 2. After GitHub redirects back, call handleGithubCallback(code, state)
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* const { isLoading, error, startGithubAuth } = useGithubAuth({
|
|
34
|
+
* onSuccess: (user) => router.push('/dashboard'),
|
|
35
|
+
* onError: (error) => console.error(error),
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* <Button onClick={startGithubAuth} disabled={isLoading}>
|
|
39
|
+
* Continue with GitHub
|
|
40
|
+
* </Button>
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export const useGithubAuth = (options: UseGithubAuthOptions = {}): UseGithubAuthReturn => {
|
|
44
|
+
const { sourceUrl, onSuccess, onError, redirectUrl } = options;
|
|
45
|
+
const router = useCfgRouter();
|
|
46
|
+
|
|
47
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
48
|
+
const [error, setError] = useState<string | null>(null);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Start GitHub OAuth flow - redirects user to GitHub authorization page.
|
|
52
|
+
*/
|
|
53
|
+
const startGithubAuth = useCallback(async () => {
|
|
54
|
+
setIsLoading(true);
|
|
55
|
+
setError(null);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
authLogger.info('Starting GitHub OAuth flow...');
|
|
59
|
+
|
|
60
|
+
// Track OAuth start
|
|
61
|
+
Analytics.event(AnalyticsEvent.AUTH_OAUTH_START, {
|
|
62
|
+
category: AnalyticsCategory.AUTH,
|
|
63
|
+
label: 'github',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Call API to get authorization URL
|
|
67
|
+
// The API will auto-generate redirect_uri from config if not provided
|
|
68
|
+
const response = await api.cfg_oauth.accountsOauthGithubAuthorizeCreate({
|
|
69
|
+
source_url: sourceUrl || (typeof window !== 'undefined' ? window.location.href : ''),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (!response.authorization_url) {
|
|
73
|
+
throw new Error('Failed to get authorization URL');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
authLogger.info('Redirecting to GitHub...', response.authorization_url);
|
|
77
|
+
|
|
78
|
+
// Store state in sessionStorage for verification on callback
|
|
79
|
+
if (typeof window !== 'undefined') {
|
|
80
|
+
sessionStorage.setItem('oauth_state', response.state);
|
|
81
|
+
sessionStorage.setItem('oauth_provider', 'github');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Redirect to GitHub
|
|
85
|
+
window.location.href = response.authorization_url;
|
|
86
|
+
|
|
87
|
+
} catch (err) {
|
|
88
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to start GitHub authentication';
|
|
89
|
+
authLogger.error('GitHub OAuth start error:', err);
|
|
90
|
+
setError(errorMessage);
|
|
91
|
+
onError?.(errorMessage);
|
|
92
|
+
|
|
93
|
+
// Track OAuth error
|
|
94
|
+
Analytics.event(AnalyticsEvent.AUTH_OAUTH_FAIL, {
|
|
95
|
+
category: AnalyticsCategory.AUTH,
|
|
96
|
+
label: 'github',
|
|
97
|
+
});
|
|
98
|
+
} finally {
|
|
99
|
+
setIsLoading(false);
|
|
100
|
+
}
|
|
101
|
+
}, [sourceUrl, onError]);
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Handle GitHub OAuth callback - exchanges code for JWT tokens.
|
|
105
|
+
*
|
|
106
|
+
* @param code - Authorization code from GitHub callback
|
|
107
|
+
* @param state - State token for CSRF verification
|
|
108
|
+
*/
|
|
109
|
+
const handleGithubCallback = useCallback(async (code: string, state: string) => {
|
|
110
|
+
setIsLoading(true);
|
|
111
|
+
setError(null);
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
authLogger.info('Processing GitHub OAuth callback...');
|
|
115
|
+
|
|
116
|
+
// Verify state matches what we stored
|
|
117
|
+
if (typeof window !== 'undefined') {
|
|
118
|
+
const storedState = sessionStorage.getItem('oauth_state');
|
|
119
|
+
if (storedState && storedState !== state) {
|
|
120
|
+
throw new Error('Invalid OAuth state - possible CSRF attack');
|
|
121
|
+
}
|
|
122
|
+
// Clear stored state
|
|
123
|
+
sessionStorage.removeItem('oauth_state');
|
|
124
|
+
sessionStorage.removeItem('oauth_provider');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Exchange code for tokens
|
|
128
|
+
// The API will auto-generate redirect_uri from config if not provided
|
|
129
|
+
const response = await api.cfg_oauth.accountsOauthGithubCallbackCreate({
|
|
130
|
+
code,
|
|
131
|
+
state,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (!response.access || !response.refresh) {
|
|
135
|
+
throw new Error('Invalid response from OAuth callback');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
authLogger.info('GitHub OAuth successful, user:', response.user);
|
|
139
|
+
|
|
140
|
+
// Save tokens using API client
|
|
141
|
+
api.setToken(response.access, response.refresh);
|
|
142
|
+
|
|
143
|
+
// Track successful OAuth
|
|
144
|
+
Analytics.event(AnalyticsEvent.AUTH_LOGIN_SUCCESS, {
|
|
145
|
+
category: AnalyticsCategory.AUTH,
|
|
146
|
+
label: 'github',
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Set user ID for future tracking
|
|
150
|
+
if (response.user?.id) {
|
|
151
|
+
Analytics.setUser(String(response.user.id));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Call success callback
|
|
155
|
+
onSuccess?.(response.user, response.is_new_user || false);
|
|
156
|
+
|
|
157
|
+
// Redirect to dashboard or specified URL
|
|
158
|
+
// Use hardPush for full page reload - ensures all React contexts reinitialize
|
|
159
|
+
const finalRedirectUrl = redirectUrl || '/dashboard';
|
|
160
|
+
router.hardPush(finalRedirectUrl);
|
|
161
|
+
|
|
162
|
+
} catch (err) {
|
|
163
|
+
const errorMessage = err instanceof Error ? err.message : 'GitHub authentication failed';
|
|
164
|
+
authLogger.error('GitHub OAuth callback error:', err);
|
|
165
|
+
setError(errorMessage);
|
|
166
|
+
onError?.(errorMessage);
|
|
167
|
+
|
|
168
|
+
// Track OAuth error
|
|
169
|
+
Analytics.event(AnalyticsEvent.AUTH_OAUTH_FAIL, {
|
|
170
|
+
category: AnalyticsCategory.AUTH,
|
|
171
|
+
label: 'github',
|
|
172
|
+
});
|
|
173
|
+
} finally {
|
|
174
|
+
setIsLoading(false);
|
|
175
|
+
}
|
|
176
|
+
}, [onSuccess, onError, redirectUrl, router]);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
isLoading,
|
|
180
|
+
error,
|
|
181
|
+
startGithubAuth,
|
|
182
|
+
handleGithubCallback,
|
|
183
|
+
};
|
|
184
|
+
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect } from 'react';
|
|
4
4
|
import { useAuth } from '../../auth';
|
|
5
|
-
import {
|
|
5
|
+
import { useCfgRouter } from '@djangocfg/ui/hooks';
|
|
6
6
|
import { Preloader } from '@djangocfg/ui/components';
|
|
7
7
|
|
|
8
8
|
export interface RedirectPageProps {
|
|
@@ -49,7 +49,7 @@ export function RedirectPage({
|
|
|
49
49
|
loadingText = 'Loading...',
|
|
50
50
|
}: RedirectPageProps) {
|
|
51
51
|
const { isAuthenticated } = useAuth();
|
|
52
|
-
const router =
|
|
52
|
+
const router = useCfgRouter();
|
|
53
53
|
|
|
54
54
|
useEffect(() => {
|
|
55
55
|
if (!isAuthenticated) {
|
|
@@ -15,6 +15,7 @@ export const AuthProvider: React.FC<AuthProps> = ({
|
|
|
15
15
|
termsUrl,
|
|
16
16
|
privacyUrl,
|
|
17
17
|
enablePhoneAuth = false, // Default to false for backward compatibility
|
|
18
|
+
enableGithubAuth = false, // Default to false for backward compatibility
|
|
18
19
|
onIdentifierSuccess,
|
|
19
20
|
onOTPSuccess,
|
|
20
21
|
onError,
|
|
@@ -39,6 +40,7 @@ export const AuthProvider: React.FC<AuthProps> = ({
|
|
|
39
40
|
termsUrl,
|
|
40
41
|
privacyUrl,
|
|
41
42
|
enablePhoneAuth,
|
|
43
|
+
enableGithubAuth,
|
|
42
44
|
};
|
|
43
45
|
|
|
44
46
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auth Layout
|
|
3
|
-
*
|
|
4
|
-
* Layout for authentication pages with OTP
|
|
3
|
+
*
|
|
4
|
+
* Layout for authentication pages with OTP (email/phone) and OAuth (GitHub) support.
|
|
5
5
|
* Supports two-step authentication flow: identifier input → OTP verification
|
|
6
|
-
*
|
|
6
|
+
* Also handles OAuth callbacks automatically when enableGithubAuth is true.
|
|
7
|
+
*
|
|
7
8
|
* @example
|
|
8
9
|
* ```tsx
|
|
9
10
|
* import { AuthLayout } from '@djangocfg/layouts';
|
|
10
|
-
*
|
|
11
|
+
*
|
|
11
12
|
* <AuthLayout
|
|
12
13
|
* sourceUrl="https://example.com"
|
|
13
14
|
* supportUrl="https://example.com/support"
|
|
14
15
|
* termsUrl="https://example.com/terms"
|
|
15
16
|
* privacyUrl="https://example.com/privacy"
|
|
16
|
-
* enablePhoneAuth={
|
|
17
|
+
* enablePhoneAuth={false}
|
|
18
|
+
* enableGithubAuth={true}
|
|
19
|
+
* redirectUrl="/dashboard"
|
|
17
20
|
* >
|
|
18
21
|
* {/* Optional custom content above forms *\/}
|
|
19
22
|
* </AuthLayout>
|
|
@@ -27,6 +30,7 @@ import React from 'react';
|
|
|
27
30
|
import { AuthProvider, useAuthContext } from './AuthContext';
|
|
28
31
|
import { IdentifierForm } from './IdentifierForm';
|
|
29
32
|
import { OTPForm } from './OTPForm';
|
|
33
|
+
import { OAuthCallback } from './OAuthCallback';
|
|
30
34
|
import { Suspense } from '../../components';
|
|
31
35
|
|
|
32
36
|
import type { AuthProps } from './types';
|
|
@@ -34,8 +38,21 @@ import type { AuthProps } from './types';
|
|
|
34
38
|
export type AuthLayoutProps = AuthProps;
|
|
35
39
|
|
|
36
40
|
export const AuthLayout: React.FC<AuthProps> = (props) => {
|
|
41
|
+
const { enableGithubAuth, redirectUrl = '/dashboard', onOAuthSuccess, onError } = props;
|
|
42
|
+
|
|
37
43
|
return (
|
|
38
44
|
<Suspense>
|
|
45
|
+
{/* Handle OAuth callback when GitHub auth is enabled */}
|
|
46
|
+
{enableGithubAuth && (
|
|
47
|
+
<Suspense fallback={null}>
|
|
48
|
+
<OAuthCallback
|
|
49
|
+
redirectUrl={redirectUrl}
|
|
50
|
+
onSuccess={onOAuthSuccess ? (user, isNewUser) => onOAuthSuccess(user, isNewUser, 'github') : undefined}
|
|
51
|
+
onError={onError}
|
|
52
|
+
/>
|
|
53
|
+
</Suspense>
|
|
54
|
+
)}
|
|
55
|
+
|
|
39
56
|
<AuthProvider {...props}>
|
|
40
57
|
<div
|
|
41
58
|
className={`min-h-screen flex flex-col items-center justify-center bg-background py-6 px-4 sm:py-12 sm:px-6 lg:px-8 ${props.className || ''}`}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
|
|
23
23
|
import { useAuthContext } from './AuthContext';
|
|
24
24
|
import { AuthHelp } from './AuthHelp';
|
|
25
|
+
import { OAuthProviders } from './OAuthProviders';
|
|
25
26
|
|
|
26
27
|
export const IdentifierForm: React.FC = () => {
|
|
27
28
|
const {
|
|
@@ -328,6 +329,9 @@ export const IdentifierForm: React.FC = () => {
|
|
|
328
329
|
</form>
|
|
329
330
|
)}
|
|
330
331
|
|
|
332
|
+
{/* OAuth Providers (GitHub, etc.) */}
|
|
333
|
+
<OAuthProviders />
|
|
334
|
+
|
|
331
335
|
{/* Help Section */}
|
|
332
336
|
<AuthHelp />
|
|
333
337
|
</CardContent>
|