@djangocfg/layouts 2.1.57 → 2.1.59
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 +11 -3
- package/package.json +6 -5
- package/src/layouts/AuthLayout/AuthLayout.tsx +55 -47
- package/src/layouts/AuthLayout/{AuthHelp.tsx → components/AuthHelp.tsx} +3 -3
- package/src/layouts/AuthLayout/components/AuthSuccess.tsx +101 -0
- package/src/layouts/AuthLayout/{IdentifierForm.tsx → components/IdentifierForm.tsx} +15 -29
- package/src/layouts/AuthLayout/{OTPForm.tsx → components/OTPForm.tsx} +29 -34
- package/src/layouts/AuthLayout/components/TwoFactorForm.tsx +140 -0
- package/src/layouts/AuthLayout/components/TwoFactorSetup.tsx +286 -0
- package/src/layouts/AuthLayout/components/index.ts +7 -0
- package/src/layouts/AuthLayout/{OAuthCallback.tsx → components/oauth/OAuthCallback.tsx} +16 -15
- package/src/layouts/AuthLayout/components/oauth/OAuthProviders.tsx +56 -0
- package/src/layouts/AuthLayout/components/oauth/index.ts +2 -0
- package/src/layouts/AuthLayout/{AuthContext.tsx → context.tsx} +15 -15
- package/src/layouts/AuthLayout/index.ts +20 -18
- package/src/layouts/AuthLayout/types.ts +22 -66
- package/src/layouts/AuthLayout/OAuthProviders.tsx +0 -76
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { CheckCircle, Copy, Eye, EyeOff, Loader2, ShieldCheck } from 'lucide-react';
|
|
4
|
+
import React, { useState } from 'react';
|
|
5
|
+
import { QRCodeSVG } from 'qrcode.react';
|
|
6
|
+
|
|
7
|
+
import { useTwoFactorSetup } from '@djangocfg/api/auth';
|
|
8
|
+
import {
|
|
9
|
+
Alert,
|
|
10
|
+
AlertDescription,
|
|
11
|
+
Button,
|
|
12
|
+
Card,
|
|
13
|
+
CardContent,
|
|
14
|
+
CardDescription,
|
|
15
|
+
CardFooter,
|
|
16
|
+
CardHeader,
|
|
17
|
+
CardTitle,
|
|
18
|
+
OTPInput,
|
|
19
|
+
} from '@djangocfg/ui-nextjs/components';
|
|
20
|
+
|
|
21
|
+
export interface TwoFactorSetupProps {
|
|
22
|
+
/** Callback when setup is complete */
|
|
23
|
+
onComplete?: (backupCodes: string[]) => void;
|
|
24
|
+
/** Callback to skip setup */
|
|
25
|
+
onSkip?: () => void;
|
|
26
|
+
/** Callback on error */
|
|
27
|
+
onError?: (error: string) => void;
|
|
28
|
+
/** Device name for the authenticator */
|
|
29
|
+
deviceName?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Two-Factor Authentication Setup Component
|
|
34
|
+
*
|
|
35
|
+
* Guides user through enabling 2FA:
|
|
36
|
+
* 1. Shows QR code to scan with authenticator app
|
|
37
|
+
* 2. User enters code to confirm
|
|
38
|
+
* 3. Shows backup codes to save
|
|
39
|
+
*/
|
|
40
|
+
export const TwoFactorSetup: React.FC<TwoFactorSetupProps> = ({
|
|
41
|
+
onComplete,
|
|
42
|
+
onSkip,
|
|
43
|
+
onError,
|
|
44
|
+
deviceName,
|
|
45
|
+
}) => {
|
|
46
|
+
const [confirmCode, setConfirmCode] = useState('');
|
|
47
|
+
const [showSecret, setShowSecret] = useState(false);
|
|
48
|
+
const [copiedSecret, setCopiedSecret] = useState(false);
|
|
49
|
+
const [copiedBackupCodes, setCopiedBackupCodes] = useState(false);
|
|
50
|
+
|
|
51
|
+
const {
|
|
52
|
+
isLoading,
|
|
53
|
+
error,
|
|
54
|
+
setupData,
|
|
55
|
+
backupCodes,
|
|
56
|
+
backupCodesWarning,
|
|
57
|
+
setupStep,
|
|
58
|
+
startSetup,
|
|
59
|
+
confirmSetup,
|
|
60
|
+
resetSetup,
|
|
61
|
+
} = useTwoFactorSetup({
|
|
62
|
+
onComplete,
|
|
63
|
+
onError,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Start setup on mount if not already started
|
|
67
|
+
React.useEffect(() => {
|
|
68
|
+
if (setupStep === 'idle') {
|
|
69
|
+
startSetup(deviceName);
|
|
70
|
+
}
|
|
71
|
+
}, [setupStep, startSetup, deviceName]);
|
|
72
|
+
|
|
73
|
+
const handleConfirm = async (e: React.FormEvent) => {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
await confirmSetup(confirmCode);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const copySecret = async () => {
|
|
79
|
+
if (setupData?.secret) {
|
|
80
|
+
await navigator.clipboard.writeText(setupData.secret);
|
|
81
|
+
setCopiedSecret(true);
|
|
82
|
+
setTimeout(() => setCopiedSecret(false), 2000);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const copyBackupCodes = async () => {
|
|
87
|
+
if (backupCodes) {
|
|
88
|
+
await navigator.clipboard.writeText(backupCodes.join('\n'));
|
|
89
|
+
setCopiedBackupCodes(true);
|
|
90
|
+
setTimeout(() => setCopiedBackupCodes(false), 2000);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Loading state
|
|
95
|
+
if (isLoading && !setupData) {
|
|
96
|
+
return (
|
|
97
|
+
<Card className="w-full max-w-md mx-auto">
|
|
98
|
+
<CardContent className="flex items-center justify-center py-12">
|
|
99
|
+
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
|
100
|
+
</CardContent>
|
|
101
|
+
</Card>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Complete - show backup codes
|
|
106
|
+
if (setupStep === 'complete' && backupCodes) {
|
|
107
|
+
return (
|
|
108
|
+
<Card className="w-full max-w-md mx-auto">
|
|
109
|
+
<CardHeader className="space-y-1 text-center">
|
|
110
|
+
<div className="mx-auto w-12 h-12 bg-green-100 dark:bg-green-900/20 rounded-full flex items-center justify-center mb-2">
|
|
111
|
+
<CheckCircle className="w-6 h-6 text-green-600 dark:text-green-400" />
|
|
112
|
+
</div>
|
|
113
|
+
<CardTitle className="text-2xl">2FA Enabled!</CardTitle>
|
|
114
|
+
<CardDescription>
|
|
115
|
+
Save these backup codes in a secure place
|
|
116
|
+
</CardDescription>
|
|
117
|
+
</CardHeader>
|
|
118
|
+
|
|
119
|
+
<CardContent className="space-y-4">
|
|
120
|
+
{backupCodesWarning && (
|
|
121
|
+
<Alert>
|
|
122
|
+
<AlertDescription>{backupCodesWarning}</AlertDescription>
|
|
123
|
+
</Alert>
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
<div className="bg-muted rounded-lg p-4">
|
|
127
|
+
<div className="grid grid-cols-2 gap-2 font-mono text-sm">
|
|
128
|
+
{backupCodes.map((code, index) => (
|
|
129
|
+
<div key={index} className="text-center py-1">
|
|
130
|
+
{code}
|
|
131
|
+
</div>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<p className="text-xs text-muted-foreground text-center">
|
|
137
|
+
Each code can only be used once. Store them securely.
|
|
138
|
+
</p>
|
|
139
|
+
</CardContent>
|
|
140
|
+
|
|
141
|
+
<CardFooter className="flex flex-col space-y-3">
|
|
142
|
+
<Button
|
|
143
|
+
type="button"
|
|
144
|
+
variant="outline"
|
|
145
|
+
className="w-full"
|
|
146
|
+
onClick={copyBackupCodes}
|
|
147
|
+
>
|
|
148
|
+
<Copy className="mr-2 h-4 w-4" />
|
|
149
|
+
{copiedBackupCodes ? 'Copied!' : 'Copy all codes'}
|
|
150
|
+
</Button>
|
|
151
|
+
|
|
152
|
+
<Button
|
|
153
|
+
type="button"
|
|
154
|
+
className="w-full"
|
|
155
|
+
onClick={() => onComplete?.(backupCodes)}
|
|
156
|
+
>
|
|
157
|
+
I've saved my backup codes
|
|
158
|
+
</Button>
|
|
159
|
+
</CardFooter>
|
|
160
|
+
</Card>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Scanning/Confirming - show QR code
|
|
165
|
+
return (
|
|
166
|
+
<Card className="w-full max-w-md mx-auto">
|
|
167
|
+
<CardHeader className="space-y-1 text-center">
|
|
168
|
+
<div className="mx-auto w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mb-2">
|
|
169
|
+
<ShieldCheck className="w-6 h-6 text-primary" />
|
|
170
|
+
</div>
|
|
171
|
+
<CardTitle className="text-2xl">Set Up 2FA</CardTitle>
|
|
172
|
+
<CardDescription>
|
|
173
|
+
Scan this QR code with your authenticator app
|
|
174
|
+
</CardDescription>
|
|
175
|
+
</CardHeader>
|
|
176
|
+
|
|
177
|
+
<form onSubmit={handleConfirm}>
|
|
178
|
+
<CardContent className="space-y-6">
|
|
179
|
+
{error && (
|
|
180
|
+
<Alert variant="destructive">
|
|
181
|
+
<AlertDescription>{error}</AlertDescription>
|
|
182
|
+
</Alert>
|
|
183
|
+
)}
|
|
184
|
+
|
|
185
|
+
{/* QR Code */}
|
|
186
|
+
{setupData && (
|
|
187
|
+
<div className="flex justify-center">
|
|
188
|
+
<div className="bg-white p-4 rounded-lg">
|
|
189
|
+
<QRCodeSVG
|
|
190
|
+
value={setupData.provisioningUri}
|
|
191
|
+
size={200}
|
|
192
|
+
level="M"
|
|
193
|
+
marginSize={0}
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
|
|
199
|
+
{/* Manual entry option */}
|
|
200
|
+
{setupData && (
|
|
201
|
+
<div className="space-y-2">
|
|
202
|
+
<Button
|
|
203
|
+
type="button"
|
|
204
|
+
variant="ghost"
|
|
205
|
+
size="sm"
|
|
206
|
+
className="w-full text-xs"
|
|
207
|
+
onClick={() => setShowSecret(!showSecret)}
|
|
208
|
+
>
|
|
209
|
+
{showSecret ? (
|
|
210
|
+
<EyeOff className="mr-2 h-3 w-3" />
|
|
211
|
+
) : (
|
|
212
|
+
<Eye className="mr-2 h-3 w-3" />
|
|
213
|
+
)}
|
|
214
|
+
{showSecret ? 'Hide' : 'Show'} manual entry code
|
|
215
|
+
</Button>
|
|
216
|
+
|
|
217
|
+
{showSecret && (
|
|
218
|
+
<div className="flex items-center gap-2 bg-muted rounded-lg p-3">
|
|
219
|
+
<code className="flex-1 text-xs font-mono break-all">
|
|
220
|
+
{setupData.secret}
|
|
221
|
+
</code>
|
|
222
|
+
<Button
|
|
223
|
+
type="button"
|
|
224
|
+
variant="ghost"
|
|
225
|
+
size="sm"
|
|
226
|
+
onClick={copySecret}
|
|
227
|
+
>
|
|
228
|
+
<Copy className="h-4 w-4" />
|
|
229
|
+
</Button>
|
|
230
|
+
</div>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
|
|
235
|
+
{/* Confirm code input */}
|
|
236
|
+
<div className="space-y-2">
|
|
237
|
+
<p className="text-sm text-center text-muted-foreground">
|
|
238
|
+
Enter the 6-digit code from your app to confirm
|
|
239
|
+
</p>
|
|
240
|
+
<div className="flex justify-center">
|
|
241
|
+
<OTPInput
|
|
242
|
+
length={6}
|
|
243
|
+
validationMode="numeric"
|
|
244
|
+
pasteBehavior="clean"
|
|
245
|
+
value={confirmCode}
|
|
246
|
+
onChange={setConfirmCode}
|
|
247
|
+
disabled={isLoading}
|
|
248
|
+
autoFocus={true}
|
|
249
|
+
size="lg"
|
|
250
|
+
/>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</CardContent>
|
|
254
|
+
|
|
255
|
+
<CardFooter className="flex flex-col space-y-3">
|
|
256
|
+
<Button
|
|
257
|
+
type="submit"
|
|
258
|
+
className="w-full"
|
|
259
|
+
disabled={isLoading || confirmCode.length !== 6}
|
|
260
|
+
>
|
|
261
|
+
{isLoading ? (
|
|
262
|
+
<>
|
|
263
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
264
|
+
Verifying...
|
|
265
|
+
</>
|
|
266
|
+
) : (
|
|
267
|
+
'Confirm & Enable 2FA'
|
|
268
|
+
)}
|
|
269
|
+
</Button>
|
|
270
|
+
|
|
271
|
+
{onSkip && (
|
|
272
|
+
<Button
|
|
273
|
+
type="button"
|
|
274
|
+
variant="ghost"
|
|
275
|
+
className="w-full"
|
|
276
|
+
onClick={onSkip}
|
|
277
|
+
disabled={isLoading}
|
|
278
|
+
>
|
|
279
|
+
Skip for now
|
|
280
|
+
</Button>
|
|
281
|
+
)}
|
|
282
|
+
</CardFooter>
|
|
283
|
+
</form>
|
|
284
|
+
</Card>
|
|
285
|
+
);
|
|
286
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { IdentifierForm } from './IdentifierForm';
|
|
2
|
+
export { OTPForm } from './OTPForm';
|
|
3
|
+
export { AuthHelp } from './AuthHelp';
|
|
4
|
+
export { OAuthProviders, OAuthCallback, type OAuthCallbackProps } from './oauth';
|
|
5
|
+
export { TwoFactorForm } from './TwoFactorForm';
|
|
6
|
+
export { TwoFactorSetup, type TwoFactorSetupProps } from './TwoFactorSetup';
|
|
7
|
+
export { AuthSuccess, type AuthSuccessProps } from './AuthSuccess';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { AlertCircle,
|
|
3
|
+
import { AlertCircle, Loader2 } from 'lucide-react';
|
|
4
4
|
import { useSearchParams } from 'next/navigation';
|
|
5
5
|
import React, { useEffect, useState } from 'react';
|
|
6
6
|
|
|
@@ -9,13 +9,15 @@ import {
|
|
|
9
9
|
Card, CardContent, CardDescription, CardHeader, CardTitle
|
|
10
10
|
} from '@djangocfg/ui-nextjs/components';
|
|
11
11
|
|
|
12
|
+
import { useAuthFormContext } from '../../context';
|
|
13
|
+
|
|
12
14
|
export interface OAuthCallbackProps {
|
|
13
15
|
onSuccess?: (user: any, isNewUser: boolean, provider: string) => void;
|
|
14
16
|
onError?: (error: string) => void;
|
|
15
17
|
redirectUrl?: string;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
type CallbackStatus = 'processing' | '
|
|
20
|
+
type CallbackStatus = 'processing' | 'error';
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* OAuth Callback Handler Component
|
|
@@ -52,6 +54,7 @@ export const OAuthCallback: React.FC<OAuthCallbackProps> = ({
|
|
|
52
54
|
redirectUrl,
|
|
53
55
|
}) => {
|
|
54
56
|
const searchParams = useSearchParams();
|
|
57
|
+
const { setStep, setTwoFactorSessionId, setShouldPrompt2FA } = useAuthFormContext();
|
|
55
58
|
const [status, setStatus] = useState<CallbackStatus | null>(null);
|
|
56
59
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
57
60
|
|
|
@@ -67,7 +70,8 @@ export const OAuthCallback: React.FC<OAuthCallbackProps> = ({
|
|
|
67
70
|
error: githubError,
|
|
68
71
|
} = useGithubAuth({
|
|
69
72
|
onSuccess: (user, isNewUser) => {
|
|
70
|
-
|
|
73
|
+
// Show success screen with logo instead of redirecting
|
|
74
|
+
setStep('success');
|
|
71
75
|
onSuccess?.(user, isNewUser, 'github');
|
|
72
76
|
},
|
|
73
77
|
onError: (err) => {
|
|
@@ -75,7 +79,14 @@ export const OAuthCallback: React.FC<OAuthCallbackProps> = ({
|
|
|
75
79
|
setErrorMessage(err);
|
|
76
80
|
onError?.(err);
|
|
77
81
|
},
|
|
82
|
+
onRequires2FA: (sessionId, shouldPrompt) => {
|
|
83
|
+
// Handle 2FA requirement
|
|
84
|
+
setTwoFactorSessionId(sessionId);
|
|
85
|
+
setShouldPrompt2FA(shouldPrompt);
|
|
86
|
+
setStep('2fa');
|
|
87
|
+
},
|
|
78
88
|
redirectUrl,
|
|
89
|
+
skipRedirect: true, // We handle navigation via success screen
|
|
79
90
|
});
|
|
80
91
|
|
|
81
92
|
// Process OAuth callback on mount
|
|
@@ -116,6 +127,8 @@ export const OAuthCallback: React.FC<OAuthCallbackProps> = ({
|
|
|
116
127
|
return null;
|
|
117
128
|
}
|
|
118
129
|
|
|
130
|
+
// Only show UI for processing or error states
|
|
131
|
+
// Success state is handled by AuthSuccess component via setStep('success')
|
|
119
132
|
return (
|
|
120
133
|
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm z-50 flex items-center justify-center">
|
|
121
134
|
<Card className="w-full max-w-md mx-4 shadow-lg">
|
|
@@ -132,18 +145,6 @@ export const OAuthCallback: React.FC<OAuthCallbackProps> = ({
|
|
|
132
145
|
</>
|
|
133
146
|
)}
|
|
134
147
|
|
|
135
|
-
{status === 'success' && (
|
|
136
|
-
<>
|
|
137
|
-
<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">
|
|
138
|
-
<CheckCircle className="w-6 h-6 text-green-600 dark:text-green-400" />
|
|
139
|
-
</div>
|
|
140
|
-
<CardTitle>Success!</CardTitle>
|
|
141
|
-
<CardDescription>
|
|
142
|
-
You have been signed in successfully. Redirecting...
|
|
143
|
-
</CardDescription>
|
|
144
|
-
</>
|
|
145
|
-
)}
|
|
146
|
-
|
|
147
148
|
{status === 'error' && (
|
|
148
149
|
<>
|
|
149
150
|
<div className="mx-auto w-12 h-12 bg-destructive/10 rounded-full flex items-center justify-center mb-4">
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Github } from 'lucide-react';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
import { useGithubAuth } from '@djangocfg/api/auth';
|
|
7
|
+
import { Button } from '@djangocfg/ui-nextjs/components';
|
|
8
|
+
|
|
9
|
+
import { useAuthFormContext } from '../../context';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* OAuth Providers Component
|
|
13
|
+
*
|
|
14
|
+
* Shows OAuth login buttons (GitHub, etc.) when enabled.
|
|
15
|
+
*/
|
|
16
|
+
export const OAuthProviders: React.FC = () => {
|
|
17
|
+
const { enableGithubAuth, sourceUrl, setError } = useAuthFormContext();
|
|
18
|
+
|
|
19
|
+
const { isLoading, startGithubAuth } = useGithubAuth({
|
|
20
|
+
sourceUrl,
|
|
21
|
+
onError: setError,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (!enableGithubAuth) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="space-y-4">
|
|
30
|
+
{/* Divider */}
|
|
31
|
+
<div className="relative">
|
|
32
|
+
<div className="absolute inset-0 flex items-center">
|
|
33
|
+
<div className="w-full border-t border-border" />
|
|
34
|
+
</div>
|
|
35
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
36
|
+
<span className="bg-card px-2 text-muted-foreground">
|
|
37
|
+
Or continue with
|
|
38
|
+
</span>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
{/* OAuth Buttons */}
|
|
43
|
+
<Button
|
|
44
|
+
type="button"
|
|
45
|
+
variant="outline"
|
|
46
|
+
size="lg"
|
|
47
|
+
className="w-full"
|
|
48
|
+
onClick={startGithubAuth}
|
|
49
|
+
loading={isLoading}
|
|
50
|
+
>
|
|
51
|
+
<Github className="w-5 h-5" />
|
|
52
|
+
Continue with GitHub
|
|
53
|
+
</Button>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
@@ -4,19 +4,20 @@ import React, { createContext, useContext } from 'react';
|
|
|
4
4
|
|
|
5
5
|
import { useAuthForm } from '@djangocfg/api/auth';
|
|
6
6
|
|
|
7
|
-
import type {
|
|
7
|
+
import type { AuthFormContextType, AuthLayoutProps } from './types';
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const AuthFormContext = createContext<AuthFormContextType | undefined>(undefined);
|
|
10
10
|
|
|
11
|
-
export const
|
|
11
|
+
export const AuthFormProvider: React.FC<AuthLayoutProps> = ({
|
|
12
12
|
children,
|
|
13
13
|
sourceUrl: sourceUrlProp,
|
|
14
14
|
supportUrl,
|
|
15
15
|
termsUrl,
|
|
16
16
|
privacyUrl,
|
|
17
|
-
enablePhoneAuth = false,
|
|
18
|
-
enableGithubAuth = false,
|
|
19
|
-
|
|
17
|
+
enablePhoneAuth = false,
|
|
18
|
+
enableGithubAuth = false,
|
|
19
|
+
logoUrl,
|
|
20
|
+
redirectUrl,
|
|
20
21
|
onIdentifierSuccess,
|
|
21
22
|
onOTPSuccess,
|
|
22
23
|
onError,
|
|
@@ -26,7 +27,7 @@ export const AuthProvider: React.FC<AuthProps> = ({
|
|
|
26
27
|
// Require terms acceptance only if terms or privacy URL is provided
|
|
27
28
|
const requireTermsAcceptance = Boolean(termsUrl || privacyUrl);
|
|
28
29
|
|
|
29
|
-
// Use the auth form hook
|
|
30
|
+
// Use the auth form hook
|
|
30
31
|
const authForm = useAuthForm({
|
|
31
32
|
onIdentifierSuccess,
|
|
32
33
|
onOTPSuccess,
|
|
@@ -36,10 +37,8 @@ export const AuthProvider: React.FC<AuthProps> = ({
|
|
|
36
37
|
requireTermsAcceptance,
|
|
37
38
|
});
|
|
38
39
|
|
|
39
|
-
const value:
|
|
40
|
-
// Form state from auth form hook
|
|
40
|
+
const value: AuthFormContextType = {
|
|
41
41
|
...authForm,
|
|
42
|
-
|
|
43
42
|
// UI-specific configuration
|
|
44
43
|
sourceUrl,
|
|
45
44
|
supportUrl,
|
|
@@ -47,16 +46,17 @@ export const AuthProvider: React.FC<AuthProps> = ({
|
|
|
47
46
|
privacyUrl,
|
|
48
47
|
enablePhoneAuth,
|
|
49
48
|
enableGithubAuth,
|
|
49
|
+
logoUrl,
|
|
50
|
+
redirectUrl,
|
|
50
51
|
};
|
|
51
52
|
|
|
52
|
-
return <
|
|
53
|
+
return <AuthFormContext.Provider value={value}>{children}</AuthFormContext.Provider>;
|
|
53
54
|
};
|
|
54
55
|
|
|
55
|
-
export const
|
|
56
|
-
const context = useContext(
|
|
56
|
+
export const useAuthFormContext = (): AuthFormContextType => {
|
|
57
|
+
const context = useContext(AuthFormContext);
|
|
57
58
|
if (context === undefined) {
|
|
58
|
-
throw new Error('
|
|
59
|
+
throw new Error('useAuthFormContext must be used within an AuthFormProvider');
|
|
59
60
|
}
|
|
60
61
|
return context;
|
|
61
62
|
};
|
|
62
|
-
|
|
@@ -2,27 +2,29 @@
|
|
|
2
2
|
* Auth Layout exports
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
// Main layout
|
|
5
6
|
export { AuthLayout } from './AuthLayout';
|
|
6
7
|
export type { AuthLayoutProps } from './AuthLayout';
|
|
7
8
|
|
|
8
|
-
// Context and hooks
|
|
9
|
-
export {
|
|
9
|
+
// Context and hooks
|
|
10
|
+
export { AuthFormProvider, useAuthFormContext } from './context';
|
|
10
11
|
|
|
11
|
-
//
|
|
12
|
-
export {
|
|
13
|
-
|
|
12
|
+
// Components
|
|
13
|
+
export {
|
|
14
|
+
IdentifierForm,
|
|
15
|
+
OTPForm,
|
|
16
|
+
AuthHelp,
|
|
17
|
+
OAuthProviders,
|
|
18
|
+
OAuthCallback,
|
|
19
|
+
type OAuthCallbackProps,
|
|
20
|
+
} from './components';
|
|
14
21
|
|
|
15
|
-
//
|
|
16
|
-
export {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
export type {
|
|
24
|
-
AuthContextType as AuthLayoutContextType,
|
|
25
|
-
AuthProps as AuthLayoutFormProps,
|
|
26
|
-
AuthHelpProps
|
|
22
|
+
// Types (re-exported from @djangocfg/api/auth)
|
|
23
|
+
export type {
|
|
24
|
+
AuthChannel,
|
|
25
|
+
AuthStep,
|
|
26
|
+
AuthFormState,
|
|
27
|
+
AuthFormContextType,
|
|
28
|
+
AuthLayoutConfig,
|
|
29
|
+
AuthHelpProps,
|
|
27
30
|
} from './types';
|
|
28
|
-
|
|
@@ -1,66 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// Form handlers
|
|
25
|
-
setIdentifier: (identifier: string) => void;
|
|
26
|
-
setChannel: (channel: 'email' | 'phone') => void;
|
|
27
|
-
setOtp: (otp: string) => void;
|
|
28
|
-
setAcceptedTerms: (accepted: boolean) => void;
|
|
29
|
-
setError: (error: string) => void;
|
|
30
|
-
clearError: () => void;
|
|
31
|
-
|
|
32
|
-
// Auth handlers
|
|
33
|
-
handleIdentifierSubmit: (e: React.FormEvent) => Promise<void>;
|
|
34
|
-
handleOTPSubmit: (e: React.FormEvent) => Promise<void>;
|
|
35
|
-
handleResendOTP: () => Promise<void>;
|
|
36
|
-
handleBackToIdentifier: () => void;
|
|
37
|
-
forceOTPStep: () => void;
|
|
38
|
-
|
|
39
|
-
// Utility methods
|
|
40
|
-
detectChannelFromIdentifier: (identifier: string) => 'email' | 'phone' | null;
|
|
41
|
-
validateIdentifier: (identifier: string, channel?: 'email' | 'phone') => boolean;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Unified Auth Props - used by both AuthProvider and AuthLayout
|
|
45
|
-
export interface AuthProps {
|
|
46
|
-
children?: React.ReactNode;
|
|
47
|
-
sourceUrl?: string;
|
|
48
|
-
supportUrl?: string;
|
|
49
|
-
termsUrl?: string;
|
|
50
|
-
privacyUrl?: string;
|
|
51
|
-
className?: string;
|
|
52
|
-
enablePhoneAuth?: boolean; // Controls whether phone authentication is available
|
|
53
|
-
enableGithubAuth?: boolean; // Controls whether GitHub OAuth is available
|
|
54
|
-
redirectUrl?: string; // URL to redirect after successful auth (default: /dashboard)
|
|
55
|
-
onIdentifierSuccess?: (identifier: string, channel: 'email' | 'phone') => void;
|
|
56
|
-
onOTPSuccess?: () => void;
|
|
57
|
-
onOAuthSuccess?: (user: any, isNewUser: boolean, provider: string) => void;
|
|
58
|
-
onError?: (message: string) => void;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Auth Help Types
|
|
62
|
-
export interface AuthHelpProps {
|
|
63
|
-
className?: string;
|
|
64
|
-
variant?: 'default' | 'compact';
|
|
65
|
-
}
|
|
66
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Auth Layout Types
|
|
3
|
+
*
|
|
4
|
+
* Re-exports from @djangocfg/api/auth for convenience.
|
|
5
|
+
* Single source of truth is in @djangocfg/api/auth/types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type {
|
|
9
|
+
AuthChannel,
|
|
10
|
+
AuthStep,
|
|
11
|
+
AuthFormState,
|
|
12
|
+
AuthFormStateHandlers,
|
|
13
|
+
AuthFormSubmitHandlers,
|
|
14
|
+
AuthFormValidation,
|
|
15
|
+
AuthFormAutoSubmit,
|
|
16
|
+
AuthFormReturn,
|
|
17
|
+
UseAuthFormOptions,
|
|
18
|
+
AuthLayoutConfig,
|
|
19
|
+
AuthFormContextType,
|
|
20
|
+
AuthLayoutProps,
|
|
21
|
+
AuthHelpProps,
|
|
22
|
+
} from '@djangocfg/api/auth';
|