@djangocfg/layouts 2.0.6 → 2.0.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 +65 -6
- package/package.json +14 -13
- package/src/auth/hooks/index.ts +1 -0
- package/src/auth/hooks/useGithubAuth.ts +183 -0
- 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/layouts/PaymentsLayout/components/PaymentDetailsDialog.tsx +3 -22
- package/src/layouts/SupportLayout/components/MessageList.tsx +1 -1
- package/src/layouts/SupportLayout/components/TicketList.tsx +1 -1
- package/src/snippets/Analytics/events.ts +5 -0
- package/src/snippets/Chat/components/MessageList.tsx +1 -1
- package/src/snippets/Chat/components/SessionList.tsx +1 -1
- package/src/snippets/McpChat/components/AIChatWidget.tsx +268 -0
- package/src/snippets/McpChat/components/ChatMessages.tsx +151 -0
- package/src/snippets/McpChat/components/ChatPanel.tsx +126 -0
- package/src/snippets/McpChat/components/ChatSidebar.tsx +119 -0
- package/src/snippets/McpChat/components/ChatWidget.tsx +134 -0
- package/src/snippets/McpChat/components/MessageBubble.tsx +125 -0
- package/src/snippets/McpChat/components/MessageInput.tsx +139 -0
- package/src/snippets/McpChat/components/index.ts +22 -0
- package/src/snippets/McpChat/config.ts +35 -0
- package/src/snippets/McpChat/context/AIChatContext.tsx +245 -0
- package/src/snippets/McpChat/context/ChatContext.tsx +350 -0
- package/src/snippets/McpChat/context/index.ts +7 -0
- package/src/snippets/McpChat/hooks/index.ts +5 -0
- package/src/snippets/McpChat/hooks/useAIChat.ts +487 -0
- package/src/snippets/McpChat/hooks/useChatLayout.ts +329 -0
- package/src/snippets/McpChat/index.ts +76 -0
- package/src/snippets/McpChat/types.ts +141 -0
- package/src/snippets/index.ts +32 -0
- package/src/utils/index.ts +0 -1
- package/src/utils/og-image.ts +0 -169
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.8",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -92,28 +92,29 @@
|
|
|
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.38",
|
|
96
|
+
"@djangocfg/centrifugo": "^1.4.38",
|
|
97
|
+
"@djangocfg/ui": "^1.4.38",
|
|
98
98
|
"@hookform/resolvers": "^5.2.0",
|
|
99
99
|
"consola": "^3.4.2",
|
|
100
|
-
"lucide-react": "^0.
|
|
101
|
-
"next": "
|
|
100
|
+
"lucide-react": "^0.545.0",
|
|
101
|
+
"next": ">=15.0.0",
|
|
102
102
|
"p-retry": "^7.0.0",
|
|
103
|
-
"react": "^19.
|
|
104
|
-
"react-dom": "^19.
|
|
103
|
+
"react": "^19.2.0",
|
|
104
|
+
"react-dom": "^19.2.0",
|
|
105
105
|
"react-hook-form": "7.65.0",
|
|
106
|
-
"sonner": "2.0.
|
|
106
|
+
"sonner": "2.0.7",
|
|
107
107
|
"swr": "^2.3.0",
|
|
108
|
-
"tailwindcss": "^4.
|
|
108
|
+
"tailwindcss": "^4.1.14",
|
|
109
109
|
"tailwindcss-animate": "^1.0.7",
|
|
110
|
-
"zod": "^4.
|
|
110
|
+
"zod": "^4.1.13"
|
|
111
111
|
},
|
|
112
112
|
"dependencies": {
|
|
113
|
-
"react-ga4": "^2.1.0"
|
|
113
|
+
"react-ga4": "^2.1.0",
|
|
114
|
+
"uuid": "^11.1.0"
|
|
114
115
|
},
|
|
115
116
|
"devDependencies": {
|
|
116
|
-
"@djangocfg/typescript-config": "^1.4.
|
|
117
|
+
"@djangocfg/typescript-config": "^1.4.38",
|
|
117
118
|
"@types/node": "^24.7.2",
|
|
118
119
|
"@types/react": "19.2.2",
|
|
119
120
|
"@types/react-dom": "19.2.1",
|
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,
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
|
|
6
|
+
import { api } from '@djangocfg/api';
|
|
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 = useRouter();
|
|
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
|
+
const finalRedirectUrl = redirectUrl || '/dashboard';
|
|
159
|
+
router.push(finalRedirectUrl);
|
|
160
|
+
|
|
161
|
+
} catch (err) {
|
|
162
|
+
const errorMessage = err instanceof Error ? err.message : 'GitHub authentication failed';
|
|
163
|
+
authLogger.error('GitHub OAuth callback error:', err);
|
|
164
|
+
setError(errorMessage);
|
|
165
|
+
onError?.(errorMessage);
|
|
166
|
+
|
|
167
|
+
// Track OAuth error
|
|
168
|
+
Analytics.event(AnalyticsEvent.AUTH_OAUTH_FAIL, {
|
|
169
|
+
category: AnalyticsCategory.AUTH,
|
|
170
|
+
label: 'github',
|
|
171
|
+
});
|
|
172
|
+
} finally {
|
|
173
|
+
setIsLoading(false);
|
|
174
|
+
}
|
|
175
|
+
}, [onSuccess, onError, redirectUrl, router]);
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
isLoading,
|
|
179
|
+
error,
|
|
180
|
+
startGithubAuth,
|
|
181
|
+
handleGithubCallback,
|
|
182
|
+
};
|
|
183
|
+
};
|
|
@@ -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>
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
import { useSearchParams } from 'next/navigation';
|
|
5
|
+
import { Loader2, AlertCircle, CheckCircle } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@djangocfg/ui/components';
|
|
8
|
+
|
|
9
|
+
import { useGithubAuth } from '../../auth/hooks';
|
|
10
|
+
|
|
11
|
+
export interface OAuthCallbackProps {
|
|
12
|
+
onSuccess?: (user: any, isNewUser: boolean, provider: string) => void;
|
|
13
|
+
onError?: (error: string) => void;
|
|
14
|
+
redirectUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type CallbackStatus = 'processing' | 'success' | 'error';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* OAuth Callback Handler Component
|
|
21
|
+
*
|
|
22
|
+
* Processes OAuth callback from providers (GitHub, etc.).
|
|
23
|
+
* Reads code, state, and provider from URL params and exchanges for tokens.
|
|
24
|
+
*
|
|
25
|
+
* Usage:
|
|
26
|
+
* Place this component on your /auth page to handle OAuth callbacks.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* // app/auth/page.tsx
|
|
31
|
+
* import { OAuthCallback, AuthLayout } from '@djangocfg/layouts';
|
|
32
|
+
*
|
|
33
|
+
* export default function AuthPage() {
|
|
34
|
+
* return (
|
|
35
|
+
* <>
|
|
36
|
+
* <OAuthCallback
|
|
37
|
+
* onSuccess={(user) => console.log('OAuth success:', user)}
|
|
38
|
+
* onError={(error) => console.error('OAuth error:', error)}
|
|
39
|
+
* />
|
|
40
|
+
* <AuthLayout enableGithubAuth>
|
|
41
|
+
* {/* Your auth content *\/}
|
|
42
|
+
* </AuthLayout>
|
|
43
|
+
* </>
|
|
44
|
+
* );
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export const OAuthCallback: React.FC<OAuthCallbackProps> = ({
|
|
49
|
+
onSuccess,
|
|
50
|
+
onError,
|
|
51
|
+
redirectUrl,
|
|
52
|
+
}) => {
|
|
53
|
+
const searchParams = useSearchParams();
|
|
54
|
+
const [status, setStatus] = useState<CallbackStatus | null>(null);
|
|
55
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
56
|
+
|
|
57
|
+
const provider = searchParams.get('provider');
|
|
58
|
+
const code = searchParams.get('code');
|
|
59
|
+
const state = searchParams.get('state');
|
|
60
|
+
const error = searchParams.get('error');
|
|
61
|
+
const errorDescription = searchParams.get('error_description');
|
|
62
|
+
|
|
63
|
+
const {
|
|
64
|
+
handleGithubCallback,
|
|
65
|
+
isLoading,
|
|
66
|
+
error: githubError,
|
|
67
|
+
} = useGithubAuth({
|
|
68
|
+
onSuccess: (user, isNewUser) => {
|
|
69
|
+
setStatus('success');
|
|
70
|
+
onSuccess?.(user, isNewUser, 'github');
|
|
71
|
+
},
|
|
72
|
+
onError: (err) => {
|
|
73
|
+
setStatus('error');
|
|
74
|
+
setErrorMessage(err);
|
|
75
|
+
onError?.(err);
|
|
76
|
+
},
|
|
77
|
+
redirectUrl,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Process OAuth callback on mount
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
// Check if this is an OAuth callback
|
|
83
|
+
if (!provider || !code || !state) {
|
|
84
|
+
// Not an OAuth callback, don't show anything
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check for error from provider
|
|
89
|
+
if (error) {
|
|
90
|
+
setStatus('error');
|
|
91
|
+
setErrorMessage(errorDescription || error);
|
|
92
|
+
onError?.(errorDescription || error);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Process based on provider
|
|
97
|
+
const processCallback = async () => {
|
|
98
|
+
setStatus('processing');
|
|
99
|
+
|
|
100
|
+
if (provider === 'github') {
|
|
101
|
+
await handleGithubCallback(code, state);
|
|
102
|
+
} else {
|
|
103
|
+
setStatus('error');
|
|
104
|
+
setErrorMessage(`Unsupported OAuth provider: ${provider}`);
|
|
105
|
+
onError?.(`Unsupported OAuth provider: ${provider}`);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
processCallback();
|
|
110
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
111
|
+
}, [provider, code, state, error]);
|
|
112
|
+
|
|
113
|
+
// Don't render if not an OAuth callback
|
|
114
|
+
if (!provider || (!code && !error)) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm z-50 flex items-center justify-center">
|
|
120
|
+
<Card className="w-full max-w-md mx-4 shadow-lg">
|
|
121
|
+
<CardHeader className="text-center">
|
|
122
|
+
{status === 'processing' && (
|
|
123
|
+
<>
|
|
124
|
+
<div className="mx-auto w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mb-4">
|
|
125
|
+
<Loader2 className="w-6 h-6 text-primary animate-spin" />
|
|
126
|
+
</div>
|
|
127
|
+
<CardTitle>Signing you in...</CardTitle>
|
|
128
|
+
<CardDescription>
|
|
129
|
+
Please wait while we complete your {provider} authentication.
|
|
130
|
+
</CardDescription>
|
|
131
|
+
</>
|
|
132
|
+
)}
|
|
133
|
+
|
|
134
|
+
{status === 'success' && (
|
|
135
|
+
<>
|
|
136
|
+
<div className="mx-auto w-12 h-12 bg-green-100 dark:bg-green-900/20 rounded-full flex items-center justify-center mb-4">
|
|
137
|
+
<CheckCircle className="w-6 h-6 text-green-600 dark:text-green-400" />
|
|
138
|
+
</div>
|
|
139
|
+
<CardTitle>Success!</CardTitle>
|
|
140
|
+
<CardDescription>
|
|
141
|
+
You have been signed in successfully. Redirecting...
|
|
142
|
+
</CardDescription>
|
|
143
|
+
</>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{status === 'error' && (
|
|
147
|
+
<>
|
|
148
|
+
<div className="mx-auto w-12 h-12 bg-destructive/10 rounded-full flex items-center justify-center mb-4">
|
|
149
|
+
<AlertCircle className="w-6 h-6 text-destructive" />
|
|
150
|
+
</div>
|
|
151
|
+
<CardTitle>Authentication Failed</CardTitle>
|
|
152
|
+
<CardDescription className="text-destructive">
|
|
153
|
+
{errorMessage || githubError || 'An error occurred during authentication.'}
|
|
154
|
+
</CardDescription>
|
|
155
|
+
</>
|
|
156
|
+
)}
|
|
157
|
+
</CardHeader>
|
|
158
|
+
|
|
159
|
+
{status === 'error' && (
|
|
160
|
+
<CardContent className="text-center">
|
|
161
|
+
<a
|
|
162
|
+
href="/auth"
|
|
163
|
+
className="text-primary hover:underline text-sm"
|
|
164
|
+
>
|
|
165
|
+
Try again
|
|
166
|
+
</a>
|
|
167
|
+
</CardContent>
|
|
168
|
+
)}
|
|
169
|
+
</Card>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Github, Loader2 } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
import { Button } from '@djangocfg/ui/components';
|
|
7
|
+
|
|
8
|
+
import { useGithubAuth } from '../../auth/hooks';
|
|
9
|
+
import { useAuthContext } from './AuthContext';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* OAuth Providers Component
|
|
13
|
+
*
|
|
14
|
+
* Shows OAuth login buttons (GitHub, etc.) when enabled.
|
|
15
|
+
* Handles the OAuth flow initiation.
|
|
16
|
+
*/
|
|
17
|
+
export const OAuthProviders: React.FC = () => {
|
|
18
|
+
const { enableGithubAuth, sourceUrl, error: contextError, setError } = useAuthContext();
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
isLoading: isGithubLoading,
|
|
22
|
+
error: githubError,
|
|
23
|
+
startGithubAuth,
|
|
24
|
+
} = useGithubAuth({
|
|
25
|
+
sourceUrl,
|
|
26
|
+
onError: (error) => {
|
|
27
|
+
setError(error);
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Don't render if no OAuth providers are enabled
|
|
32
|
+
if (!enableGithubAuth) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const error = githubError || contextError;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="space-y-4">
|
|
40
|
+
{/* Divider */}
|
|
41
|
+
<div className="relative">
|
|
42
|
+
<div className="absolute inset-0 flex items-center">
|
|
43
|
+
<div className="w-full border-t border-border" />
|
|
44
|
+
</div>
|
|
45
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
46
|
+
<span className="bg-card px-2 text-muted-foreground">
|
|
47
|
+
Or continue with
|
|
48
|
+
</span>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{/* OAuth Buttons */}
|
|
53
|
+
<div className="grid gap-3">
|
|
54
|
+
{enableGithubAuth && (
|
|
55
|
+
<Button
|
|
56
|
+
type="button"
|
|
57
|
+
variant="outline"
|
|
58
|
+
className="w-full h-11 text-base font-medium"
|
|
59
|
+
onClick={startGithubAuth}
|
|
60
|
+
disabled={isGithubLoading}
|
|
61
|
+
>
|
|
62
|
+
{isGithubLoading ? (
|
|
63
|
+
<div className="flex items-center gap-2">
|
|
64
|
+
<Loader2 className="w-5 h-5 animate-spin" />
|
|
65
|
+
Connecting...
|
|
66
|
+
</div>
|
|
67
|
+
) : (
|
|
68
|
+
<div className="flex items-center gap-2">
|
|
69
|
+
<Github className="w-5 h-5" />
|
|
70
|
+
Continue with GitHub
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
</Button>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
{/* Error Message */}
|
|
78
|
+
{error && (
|
|
79
|
+
<div className="text-sm text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20">
|
|
80
|
+
{error}
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
};
|