@djangocfg/layouts 2.1.109 → 2.1.111
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/package.json +16 -14
- package/src/components/errors/ErrorBoundary.tsx +12 -6
- package/src/components/errors/ErrorLayout.tsx +19 -9
- package/src/components/errors/errorConfig.ts +28 -22
- package/src/layouts/AuthLayout/AuthLayout.tsx +92 -20
- package/src/layouts/AuthLayout/components/index.ts +11 -7
- package/src/layouts/AuthLayout/components/oauth/index.ts +0 -1
- package/src/layouts/AuthLayout/components/shared/AuthButton.tsx +35 -0
- package/src/layouts/AuthLayout/components/shared/AuthContainer.tsx +56 -0
- package/src/layouts/AuthLayout/components/shared/AuthDivider.tsx +22 -0
- package/src/layouts/AuthLayout/components/shared/AuthError.tsx +26 -0
- package/src/layouts/AuthLayout/components/shared/AuthFooter.tsx +47 -0
- package/src/layouts/AuthLayout/components/shared/AuthHeader.tsx +53 -0
- package/src/layouts/AuthLayout/components/shared/AuthLink.tsx +41 -0
- package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +42 -0
- package/src/layouts/AuthLayout/components/shared/ChannelToggle.tsx +48 -0
- package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +57 -0
- package/src/layouts/AuthLayout/components/shared/index.ts +21 -0
- package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +171 -0
- package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +114 -0
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +70 -0
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +24 -0
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +125 -0
- package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +91 -0
- package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +92 -0
- package/src/layouts/AuthLayout/components/steps/index.ts +6 -0
- package/src/layouts/AuthLayout/constants.ts +24 -0
- package/src/layouts/AuthLayout/content.ts +78 -0
- package/src/layouts/AuthLayout/hooks/index.ts +1 -0
- package/src/layouts/AuthLayout/hooks/useCopyToClipboard.ts +37 -0
- package/src/layouts/AuthLayout/index.ts +9 -5
- package/src/layouts/AuthLayout/styles/auth.css +578 -0
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +130 -58
- package/src/layouts/ProfileLayout/components/TwoFactorSection.tsx +2 -2
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +12 -4
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +6 -2
- package/src/layouts/_components/UserMenu.tsx +14 -6
- package/src/snippets/AuthDialog/AuthDialog.tsx +15 -6
- package/src/snippets/Breadcrumbs.tsx +19 -8
- package/src/snippets/McpChat/components/ChatPanel.tsx +16 -6
- package/src/snippets/McpChat/components/ChatSidebar.tsx +20 -8
- package/src/snippets/PWAInstall/components/A2HSHint.tsx +23 -10
- package/src/snippets/PWAInstall/components/DesktopGuide.tsx +44 -32
- package/src/snippets/PWAInstall/components/IOSGuideDrawer.tsx +34 -25
- package/src/snippets/PWAInstall/components/IOSGuideModal.tsx +34 -25
- package/src/snippets/PushNotifications/components/PushPrompt.tsx +16 -6
- package/src/layouts/AuthLayout/components/AuthHelp.tsx +0 -114
- package/src/layouts/AuthLayout/components/AuthSuccess.tsx +0 -101
- package/src/layouts/AuthLayout/components/IdentifierForm.tsx +0 -322
- package/src/layouts/AuthLayout/components/OTPForm.tsx +0 -174
- package/src/layouts/AuthLayout/components/TwoFactorForm.tsx +0 -140
- package/src/layouts/AuthLayout/components/TwoFactorSetup.tsx +0 -286
- package/src/layouts/AuthLayout/components/oauth/OAuthProviders.tsx +0 -56
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { ArrowLeft, Mail, MessageCircle, RotateCw, ShieldCheck } from 'lucide-react';
|
|
4
|
-
import React, { useCallback, useRef } from 'react';
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
Button, Card, CardContent, CardDescription, CardHeader, CardTitle, OTPInput
|
|
8
|
-
} from '@djangocfg/ui-core/components';
|
|
9
|
-
|
|
10
|
-
import { config } from '../../../utils';
|
|
11
|
-
import { useAuthFormContext } from '../context';
|
|
12
|
-
import { AuthHelp } from './AuthHelp';
|
|
13
|
-
|
|
14
|
-
export const OTPForm: React.FC = () => {
|
|
15
|
-
const {
|
|
16
|
-
identifier,
|
|
17
|
-
channel,
|
|
18
|
-
otp,
|
|
19
|
-
isLoading,
|
|
20
|
-
error,
|
|
21
|
-
supportUrl,
|
|
22
|
-
setOtp,
|
|
23
|
-
handleOTPSubmit,
|
|
24
|
-
handleResendOTP,
|
|
25
|
-
handleBackToIdentifier,
|
|
26
|
-
isAutoSubmittingFromUrl,
|
|
27
|
-
} = useAuthFormContext();
|
|
28
|
-
|
|
29
|
-
// Ref to track if auto-submit is in progress to prevent duplicate submissions
|
|
30
|
-
const isAutoSubmittingRef = useRef(false);
|
|
31
|
-
|
|
32
|
-
// Handle auto-submit when OTP is complete (after paste or last digit entry)
|
|
33
|
-
// Note: useAutoAuth already handles auto-submit from URL, this handles manual input/paste
|
|
34
|
-
const handleOTPComplete = useCallback((completedValue: string) => {
|
|
35
|
-
// Prevent duplicate submissions - check local ref, isLoading, and URL auto-submit ref
|
|
36
|
-
if (isAutoSubmittingRef.current || isLoading || isAutoSubmittingFromUrl.current) return;
|
|
37
|
-
|
|
38
|
-
if (completedValue.length === 6) {
|
|
39
|
-
isAutoSubmittingRef.current = true;
|
|
40
|
-
|
|
41
|
-
// Create a fake form event for handleOTPSubmit
|
|
42
|
-
const fakeEvent = {
|
|
43
|
-
preventDefault: () => {},
|
|
44
|
-
} as React.FormEvent;
|
|
45
|
-
|
|
46
|
-
// Small delay to ensure state is updated, then submit
|
|
47
|
-
// Reset ref after submit completes (isLoading will handle preventing re-submits)
|
|
48
|
-
setTimeout(async () => {
|
|
49
|
-
try {
|
|
50
|
-
await handleOTPSubmit(fakeEvent);
|
|
51
|
-
} finally {
|
|
52
|
-
isAutoSubmittingRef.current = false;
|
|
53
|
-
}
|
|
54
|
-
}, 100);
|
|
55
|
-
}
|
|
56
|
-
}, [handleOTPSubmit, isLoading, isAutoSubmittingFromUrl]);
|
|
57
|
-
|
|
58
|
-
const getChannelIcon = () => {
|
|
59
|
-
return channel === 'phone' ? (
|
|
60
|
-
<div className="flex items-center justify-center">
|
|
61
|
-
<MessageCircle className="w-5 h-5 text-primary" />
|
|
62
|
-
</div>
|
|
63
|
-
) : (
|
|
64
|
-
<Mail className="w-5 h-5 text-primary" />
|
|
65
|
-
);
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const getChannelTitle = () => {
|
|
69
|
-
return channel === 'phone' ? 'Verify Your Phone' : 'Verify Your Email';
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const getChannelDescription = () => {
|
|
73
|
-
const channelName = channel === 'phone' ? 'phone number' : 'email address';
|
|
74
|
-
const method = channel === 'phone' ? 'WhatsApp/SMS' : 'email';
|
|
75
|
-
return `We've sent a 6-digit verification code to your ${channelName} via ${method}`;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
<Card className="w-full max-w-md mx-auto shadow-lg border border-border bg-card/50 backdrop-blur-sm">
|
|
80
|
-
<CardHeader className="text-center pb-6">
|
|
81
|
-
<div className="mx-auto w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mb-4">
|
|
82
|
-
{getChannelIcon()}
|
|
83
|
-
</div>
|
|
84
|
-
<CardTitle className="text-xl font-semibold">{getChannelTitle()}</CardTitle>
|
|
85
|
-
<CardDescription className="text-muted-foreground">
|
|
86
|
-
{getChannelDescription()}
|
|
87
|
-
<br />
|
|
88
|
-
<span className="font-medium text-foreground">{identifier}</span>
|
|
89
|
-
</CardDescription>
|
|
90
|
-
</CardHeader>
|
|
91
|
-
<CardContent className="space-y-6">
|
|
92
|
-
<form onSubmit={handleOTPSubmit} className="space-y-6">
|
|
93
|
-
<div className="space-y-3">
|
|
94
|
-
<label className="text-sm font-medium text-foreground text-center block">
|
|
95
|
-
Enter verification code
|
|
96
|
-
</label>
|
|
97
|
-
<div className="flex justify-center">
|
|
98
|
-
<OTPInput
|
|
99
|
-
length={6}
|
|
100
|
-
validationMode="numeric"
|
|
101
|
-
pasteBehavior="clean"
|
|
102
|
-
value={otp}
|
|
103
|
-
onChange={setOtp}
|
|
104
|
-
onComplete={handleOTPComplete}
|
|
105
|
-
disabled={isLoading}
|
|
106
|
-
autoFocus={true}
|
|
107
|
-
autoSubmit={false}
|
|
108
|
-
size="lg"
|
|
109
|
-
/>
|
|
110
|
-
</div>
|
|
111
|
-
|
|
112
|
-
{/* Development Mode Notice */}
|
|
113
|
-
{config.isDevelopment && (
|
|
114
|
-
<div className="text-xs text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/30 p-2 rounded-md border border-amber-200 dark:border-amber-800 text-center">
|
|
115
|
-
🔧 Dev Mode: Any OTP code works
|
|
116
|
-
</div>
|
|
117
|
-
)}
|
|
118
|
-
</div>
|
|
119
|
-
|
|
120
|
-
<div className="space-y-4">
|
|
121
|
-
<Button
|
|
122
|
-
type="submit"
|
|
123
|
-
size="lg"
|
|
124
|
-
className="w-full"
|
|
125
|
-
disabled={otp.length < 6}
|
|
126
|
-
loading={isLoading}
|
|
127
|
-
>
|
|
128
|
-
<ShieldCheck className="w-5 h-5" />
|
|
129
|
-
Verify Code
|
|
130
|
-
</Button>
|
|
131
|
-
|
|
132
|
-
<div className="flex gap-3">
|
|
133
|
-
<Button
|
|
134
|
-
type="button"
|
|
135
|
-
variant="outline"
|
|
136
|
-
onClick={handleBackToIdentifier}
|
|
137
|
-
disabled={isLoading}
|
|
138
|
-
className="flex-1"
|
|
139
|
-
>
|
|
140
|
-
<ArrowLeft className="w-4 h-4" />
|
|
141
|
-
Back
|
|
142
|
-
</Button>
|
|
143
|
-
|
|
144
|
-
<Button
|
|
145
|
-
type="button"
|
|
146
|
-
variant="outline"
|
|
147
|
-
onClick={handleResendOTP}
|
|
148
|
-
disabled={isLoading}
|
|
149
|
-
className="flex-1"
|
|
150
|
-
>
|
|
151
|
-
<RotateCw className="w-4 h-4" />
|
|
152
|
-
Resend
|
|
153
|
-
</Button>
|
|
154
|
-
</div>
|
|
155
|
-
</div>
|
|
156
|
-
</form>
|
|
157
|
-
|
|
158
|
-
{/* Error Message */}
|
|
159
|
-
{error && (
|
|
160
|
-
<div className="text-sm text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20">
|
|
161
|
-
{error}
|
|
162
|
-
</div>
|
|
163
|
-
)}
|
|
164
|
-
|
|
165
|
-
{supportUrl && (
|
|
166
|
-
<div className="mt-4">
|
|
167
|
-
<AuthHelp />
|
|
168
|
-
</div>
|
|
169
|
-
)}
|
|
170
|
-
</CardContent>
|
|
171
|
-
</Card>
|
|
172
|
-
);
|
|
173
|
-
};
|
|
174
|
-
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { KeyRound, Loader2, ShieldCheck } from 'lucide-react';
|
|
4
|
-
import React from 'react';
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
Alert,
|
|
8
|
-
AlertDescription,
|
|
9
|
-
Button,
|
|
10
|
-
Card,
|
|
11
|
-
CardContent,
|
|
12
|
-
CardDescription,
|
|
13
|
-
CardFooter,
|
|
14
|
-
CardHeader,
|
|
15
|
-
CardTitle,
|
|
16
|
-
OTPInput,
|
|
17
|
-
Input,
|
|
18
|
-
} from '@djangocfg/ui-core/components';
|
|
19
|
-
|
|
20
|
-
import { useAuthFormContext } from '../context';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Two-Factor Authentication Form
|
|
24
|
-
*
|
|
25
|
-
* Displays TOTP code input or backup code input based on user selection.
|
|
26
|
-
* Used after OTP/OAuth verification when user has 2FA enabled.
|
|
27
|
-
*/
|
|
28
|
-
export const TwoFactorForm: React.FC = () => {
|
|
29
|
-
const {
|
|
30
|
-
twoFactorCode,
|
|
31
|
-
useBackupCode,
|
|
32
|
-
error,
|
|
33
|
-
is2FALoading,
|
|
34
|
-
twoFactorWarning,
|
|
35
|
-
setTwoFactorCode,
|
|
36
|
-
handle2FASubmit,
|
|
37
|
-
handleUseBackupCode,
|
|
38
|
-
handleUseTOTP,
|
|
39
|
-
} = useAuthFormContext();
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<Card className="w-full">
|
|
43
|
-
<CardHeader className="space-y-1 text-center">
|
|
44
|
-
<div className="mx-auto w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mb-2">
|
|
45
|
-
<ShieldCheck className="w-6 h-6 text-primary" />
|
|
46
|
-
</div>
|
|
47
|
-
<CardTitle className="text-2xl">Two-Factor Authentication</CardTitle>
|
|
48
|
-
<CardDescription>
|
|
49
|
-
{useBackupCode
|
|
50
|
-
? 'Enter one of your backup recovery codes'
|
|
51
|
-
: 'Enter the 6-digit code from your authenticator app'}
|
|
52
|
-
</CardDescription>
|
|
53
|
-
</CardHeader>
|
|
54
|
-
|
|
55
|
-
<form onSubmit={handle2FASubmit}>
|
|
56
|
-
<CardContent className="space-y-4">
|
|
57
|
-
{/* Error Alert */}
|
|
58
|
-
{error && (
|
|
59
|
-
<Alert variant="destructive">
|
|
60
|
-
<AlertDescription>{error}</AlertDescription>
|
|
61
|
-
</Alert>
|
|
62
|
-
)}
|
|
63
|
-
|
|
64
|
-
{/* Warning Alert (e.g., low backup codes) */}
|
|
65
|
-
{twoFactorWarning && (
|
|
66
|
-
<Alert>
|
|
67
|
-
<AlertDescription>{twoFactorWarning}</AlertDescription>
|
|
68
|
-
</Alert>
|
|
69
|
-
)}
|
|
70
|
-
|
|
71
|
-
{/* TOTP Code Input */}
|
|
72
|
-
{!useBackupCode && (
|
|
73
|
-
<div className="flex justify-center">
|
|
74
|
-
<OTPInput
|
|
75
|
-
length={6}
|
|
76
|
-
validationMode="numeric"
|
|
77
|
-
pasteBehavior="clean"
|
|
78
|
-
value={twoFactorCode}
|
|
79
|
-
onChange={setTwoFactorCode}
|
|
80
|
-
disabled={is2FALoading}
|
|
81
|
-
autoFocus={true}
|
|
82
|
-
size="lg"
|
|
83
|
-
/>
|
|
84
|
-
</div>
|
|
85
|
-
)}
|
|
86
|
-
|
|
87
|
-
{/* Backup Code Input */}
|
|
88
|
-
{useBackupCode && (
|
|
89
|
-
<div className="space-y-2">
|
|
90
|
-
<Input
|
|
91
|
-
type="text"
|
|
92
|
-
placeholder="Enter backup code"
|
|
93
|
-
value={twoFactorCode}
|
|
94
|
-
onChange={(e) => setTwoFactorCode(e.target.value.toUpperCase())}
|
|
95
|
-
disabled={is2FALoading}
|
|
96
|
-
className="text-center font-mono text-lg tracking-widest"
|
|
97
|
-
maxLength={12}
|
|
98
|
-
autoComplete="off"
|
|
99
|
-
/>
|
|
100
|
-
<p className="text-xs text-muted-foreground text-center">
|
|
101
|
-
Backup codes are 8 characters, letters and numbers
|
|
102
|
-
</p>
|
|
103
|
-
</div>
|
|
104
|
-
)}
|
|
105
|
-
</CardContent>
|
|
106
|
-
|
|
107
|
-
<CardFooter className="flex flex-col space-y-3">
|
|
108
|
-
<Button
|
|
109
|
-
type="submit"
|
|
110
|
-
className="w-full"
|
|
111
|
-
disabled={is2FALoading || (!useBackupCode && twoFactorCode.length !== 6)}
|
|
112
|
-
>
|
|
113
|
-
{is2FALoading ? (
|
|
114
|
-
<>
|
|
115
|
-
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
116
|
-
Verifying...
|
|
117
|
-
</>
|
|
118
|
-
) : (
|
|
119
|
-
'Verify'
|
|
120
|
-
)}
|
|
121
|
-
</Button>
|
|
122
|
-
|
|
123
|
-
{/* Toggle between TOTP and Backup Code */}
|
|
124
|
-
<Button
|
|
125
|
-
type="button"
|
|
126
|
-
variant="ghost"
|
|
127
|
-
className="w-full text-sm"
|
|
128
|
-
onClick={useBackupCode ? handleUseTOTP : handleUseBackupCode}
|
|
129
|
-
disabled={is2FALoading}
|
|
130
|
-
>
|
|
131
|
-
<KeyRound className="mr-2 h-4 w-4" />
|
|
132
|
-
{useBackupCode
|
|
133
|
-
? 'Use authenticator app instead'
|
|
134
|
-
: "Can't access your authenticator? Use a backup code"}
|
|
135
|
-
</Button>
|
|
136
|
-
</CardFooter>
|
|
137
|
-
</form>
|
|
138
|
-
</Card>
|
|
139
|
-
);
|
|
140
|
-
};
|
|
@@ -1,286 +0,0 @@
|
|
|
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-core/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
|
-
};
|
|
@@ -1,56 +0,0 @@
|
|
|
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-core/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
|
-
};
|