@djangocfg/layouts 2.1.263 → 2.1.266
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 +40 -1
- package/package.json +19 -18
- package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +50 -6
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +203 -195
- package/src/layouts/ProfileLayout/components/AvatarSection.tsx +2 -3
- package/src/layouts/ProfileLayout/components/EditableField.tsx +15 -10
- package/src/layouts/ProfileLayout/components/Section.tsx +1 -1
- package/src/layouts/ProfileLayout/components/TwoFactorSection.tsx +255 -215
- package/src/layouts/ProfileLayout/context.tsx +110 -10
- package/src/layouts/ProfileLayout/index.ts +1 -1
- package/src/layouts/ProfileLayout/.claude/.sidecar/activity.jsonl +0 -2
- package/src/layouts/ProfileLayout/.claude/.sidecar/history/2026-03-15.md +0 -35
- package/src/layouts/ProfileLayout/.claude/.sidecar/review.md +0 -35
- package/src/layouts/ProfileLayout/.claude/.sidecar/scan.log +0 -3
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-001.md +0 -18
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-002.md +0 -19
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-003.md +0 -18
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-004.md +0 -18
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-005.md +0 -18
- package/src/layouts/ProfileLayout/.claude/.sidecar/usage.json +0 -5
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { useEffect,
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
import { parsePhoneNumberFromString } from 'libphonenumber-js';
|
|
4
5
|
|
|
5
|
-
import { useAppT } from '@djangocfg/i18n';
|
|
6
6
|
import { Button, Input, PhoneInput } from '@djangocfg/ui-core/components';
|
|
7
7
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
8
8
|
|
|
9
|
+
import { useProfileContext } from '../context';
|
|
10
|
+
|
|
11
|
+
function formatPhone(raw: string): string {
|
|
12
|
+
if (!raw) return '';
|
|
13
|
+
try {
|
|
14
|
+
return parsePhoneNumberFromString(raw)?.formatInternational() ?? raw;
|
|
15
|
+
} catch {
|
|
16
|
+
return raw;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
interface EditableFieldProps {
|
|
10
21
|
label: string;
|
|
11
22
|
value: string;
|
|
@@ -23,17 +34,11 @@ export const EditableField = ({
|
|
|
23
34
|
disabled,
|
|
24
35
|
type = 'text',
|
|
25
36
|
}: EditableFieldProps) => {
|
|
26
|
-
const
|
|
37
|
+
const { labels } = useProfileContext();
|
|
27
38
|
const [isEditing, setIsEditing] = useState(false);
|
|
28
39
|
const [editValue, setEditValue] = useState(value);
|
|
29
40
|
const [isSaving, setIsSaving] = useState(false);
|
|
30
41
|
|
|
31
|
-
const labels = useMemo(() => ({
|
|
32
|
-
save: t('layouts.profilePage.save'),
|
|
33
|
-
saving: t('layouts.profilePage.saving'),
|
|
34
|
-
cancel: t('layouts.profilePage.cancel'),
|
|
35
|
-
}), [t]);
|
|
36
|
-
|
|
37
42
|
useEffect(() => {
|
|
38
43
|
setEditValue(value);
|
|
39
44
|
}, [value]);
|
|
@@ -112,7 +117,7 @@ export const EditableField = ({
|
|
|
112
117
|
>
|
|
113
118
|
<div className="text-[13px] text-muted-foreground mb-0.5">{label}</div>
|
|
114
119
|
<div className={cn('text-[15px]', value ? 'text-foreground' : 'text-muted-foreground/60')}>
|
|
115
|
-
{value
|
|
120
|
+
{value ? (type === 'phone' ? formatPhone(value) : value) : placeholder}
|
|
116
121
|
</div>
|
|
117
122
|
</button>
|
|
118
123
|
);
|
|
@@ -11,7 +11,7 @@ interface SectionProps {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export const Section = ({ title, children, className }: SectionProps) => (
|
|
14
|
-
<div className={cn('mb-
|
|
14
|
+
<div className={cn('mb-4 md:mb-6', className)}>
|
|
15
15
|
{title && (
|
|
16
16
|
<h2 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2 px-1">
|
|
17
17
|
{title}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Loader2, Shield, ShieldCheck, ShieldOff } from 'lucide-react';
|
|
3
|
+
import { Loader2, Shield, ShieldCheck, ShieldOff, Smartphone, Trash2 } from 'lucide-react';
|
|
4
4
|
import React, { useEffect, useState } from 'react';
|
|
5
5
|
|
|
6
6
|
import { useTwoFactorSetup, useTwoFactorStatus } from '@djangocfg/api/auth';
|
|
7
7
|
import {
|
|
8
8
|
Alert,
|
|
9
9
|
AlertDescription,
|
|
10
|
+
Badge,
|
|
10
11
|
Button,
|
|
11
12
|
Card,
|
|
12
13
|
CardContent,
|
|
@@ -20,279 +21,318 @@ import {
|
|
|
20
21
|
DialogHeader,
|
|
21
22
|
DialogTitle,
|
|
22
23
|
OTPInput,
|
|
24
|
+
Separator,
|
|
23
25
|
} from '@djangocfg/ui-core/components';
|
|
26
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
24
27
|
|
|
25
|
-
import {
|
|
28
|
+
import { SetupStepStandalone } from '../../AuthLayout/components/steps/SetupStep';
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
// Types
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
type View = 'status' | 'setup';
|
|
35
|
+
|
|
36
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
// Status badge
|
|
38
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
function StatusBadge({ enabled }: { enabled: boolean }) {
|
|
41
|
+
return (
|
|
42
|
+
<Badge
|
|
43
|
+
variant={enabled ? 'default' : 'secondary'}
|
|
44
|
+
className={cn(
|
|
45
|
+
'text-xs font-medium',
|
|
46
|
+
enabled && 'bg-green-500/15 text-green-600 dark:text-green-400 border-green-500/20',
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
{enabled ? 'Enabled' : 'Disabled'}
|
|
50
|
+
</Badge>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
// Disable dialog
|
|
56
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
function DisableDialog({
|
|
59
|
+
open,
|
|
60
|
+
isLoading,
|
|
61
|
+
error,
|
|
62
|
+
onConfirm,
|
|
63
|
+
onCancel,
|
|
64
|
+
}: {
|
|
65
|
+
open: boolean;
|
|
66
|
+
isLoading: boolean;
|
|
67
|
+
error: string | null;
|
|
68
|
+
onConfirm: (code: string) => void;
|
|
69
|
+
onCancel: () => void;
|
|
70
|
+
}) {
|
|
71
|
+
const [code, setCode] = useState('');
|
|
72
|
+
|
|
73
|
+
// Reset on open
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (open) setCode('');
|
|
76
|
+
}, [open]);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<Dialog open={open} onOpenChange={(v) => !v && onCancel()}>
|
|
80
|
+
<DialogContent className="sm:max-w-sm">
|
|
81
|
+
<DialogHeader>
|
|
82
|
+
<DialogTitle className="flex items-center gap-2">
|
|
83
|
+
<ShieldOff className="w-5 h-5 text-destructive" />
|
|
84
|
+
Disable Two-Factor Authentication
|
|
85
|
+
</DialogTitle>
|
|
86
|
+
<DialogDescription>
|
|
87
|
+
Enter the 6-digit code from your authenticator app to confirm.
|
|
88
|
+
This will make your account less secure.
|
|
89
|
+
</DialogDescription>
|
|
90
|
+
</DialogHeader>
|
|
91
|
+
|
|
92
|
+
<div className="py-2 space-y-4">
|
|
93
|
+
{error && (
|
|
94
|
+
<Alert variant="destructive">
|
|
95
|
+
<AlertDescription>{error}</AlertDescription>
|
|
96
|
+
</Alert>
|
|
97
|
+
)}
|
|
98
|
+
<div className="flex justify-center">
|
|
99
|
+
<OTPInput
|
|
100
|
+
length={6}
|
|
101
|
+
validationMode="numeric"
|
|
102
|
+
pasteBehavior="clean"
|
|
103
|
+
value={code}
|
|
104
|
+
onChange={setCode}
|
|
105
|
+
disabled={isLoading}
|
|
106
|
+
autoFocus
|
|
107
|
+
size="lg"
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<DialogFooter>
|
|
113
|
+
<Button variant="outline" onClick={onCancel} disabled={isLoading}>
|
|
114
|
+
Cancel
|
|
115
|
+
</Button>
|
|
116
|
+
<Button
|
|
117
|
+
variant="destructive"
|
|
118
|
+
onClick={() => onConfirm(code)}
|
|
119
|
+
disabled={isLoading || code.length !== 6}
|
|
120
|
+
>
|
|
121
|
+
{isLoading ? (
|
|
122
|
+
<><Loader2 className="mr-2 h-4 w-4 animate-spin" />Disabling…</>
|
|
123
|
+
) : (
|
|
124
|
+
'Disable 2FA'
|
|
125
|
+
)}
|
|
126
|
+
</Button>
|
|
127
|
+
</DialogFooter>
|
|
128
|
+
</DialogContent>
|
|
129
|
+
</Dialog>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
134
|
+
// Device list row
|
|
135
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
function DeviceRow({ name, createdAt, isPrimary }: {
|
|
138
|
+
name: string;
|
|
139
|
+
createdAt: string;
|
|
140
|
+
isPrimary: boolean;
|
|
141
|
+
}) {
|
|
142
|
+
return (
|
|
143
|
+
<div className="flex items-center gap-3 py-3">
|
|
144
|
+
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-muted flex items-center justify-center">
|
|
145
|
+
<Smartphone className="w-4 h-4 text-muted-foreground" />
|
|
146
|
+
</div>
|
|
147
|
+
<div className="flex-1 min-w-0">
|
|
148
|
+
<p className="text-sm font-medium truncate">{name}</p>
|
|
149
|
+
<p className="text-xs text-muted-foreground">
|
|
150
|
+
Added {new Date(createdAt).toLocaleDateString()}
|
|
151
|
+
{isPrimary && ' · Primary'}
|
|
152
|
+
</p>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
159
|
+
// Main component
|
|
160
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
28
161
|
|
|
29
|
-
/**
|
|
30
|
-
* Two-Factor Authentication section for ProfileLayout.
|
|
31
|
-
* Allows users to enable/disable 2FA from their profile.
|
|
32
|
-
*/
|
|
33
162
|
export const TwoFactorSection: React.FC = () => {
|
|
34
|
-
const [
|
|
35
|
-
const [
|
|
36
|
-
const [showDisableDialog, setShowDisableDialog] = useState(false);
|
|
163
|
+
const [view, setView] = useState<View>('status');
|
|
164
|
+
const [showDisable, setShowDisable] = useState(false);
|
|
37
165
|
|
|
38
166
|
const {
|
|
39
|
-
isLoading
|
|
40
|
-
error
|
|
167
|
+
isLoading,
|
|
168
|
+
error,
|
|
41
169
|
has2FAEnabled,
|
|
42
170
|
devices,
|
|
43
171
|
fetchStatus,
|
|
44
172
|
disable2FA,
|
|
45
|
-
clearError
|
|
173
|
+
clearError,
|
|
46
174
|
} = useTwoFactorStatus();
|
|
47
175
|
|
|
48
|
-
const {
|
|
49
|
-
resetSetup,
|
|
50
|
-
} = useTwoFactorSetup();
|
|
176
|
+
const { resetSetup } = useTwoFactorSetup();
|
|
51
177
|
|
|
52
|
-
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
fetchStatus();
|
|
55
|
-
}, [fetchStatus]);
|
|
178
|
+
useEffect(() => { fetchStatus(); }, [fetchStatus]);
|
|
56
179
|
|
|
57
180
|
const handleEnableClick = () => {
|
|
58
181
|
resetSetup();
|
|
59
|
-
|
|
182
|
+
setView('setup');
|
|
60
183
|
};
|
|
61
184
|
|
|
62
|
-
const
|
|
63
|
-
|
|
185
|
+
const handleSetupDone = () => {
|
|
186
|
+
setView('status');
|
|
64
187
|
fetchStatus();
|
|
65
188
|
};
|
|
66
189
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const handleDisableClick = () => {
|
|
72
|
-
setShowDisableDialog(true);
|
|
73
|
-
setDisableCode('');
|
|
74
|
-
clearStatusError();
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const handleDisableConfirm = async () => {
|
|
78
|
-
const success = await disable2FA(disableCode);
|
|
79
|
-
if (success) {
|
|
80
|
-
setShowDisableDialog(false);
|
|
81
|
-
setDisableCode('');
|
|
82
|
-
}
|
|
190
|
+
const handleDisableConfirm = async (code: string) => {
|
|
191
|
+
const ok = await disable2FA(code);
|
|
192
|
+
if (ok) setShowDisable(false);
|
|
83
193
|
};
|
|
84
194
|
|
|
85
195
|
const handleDisableCancel = () => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
clearStatusError();
|
|
196
|
+
setShowDisable(false);
|
|
197
|
+
clearError();
|
|
89
198
|
};
|
|
90
199
|
|
|
91
|
-
//
|
|
92
|
-
if (
|
|
200
|
+
// ── Setup flow ──────────────────────────────────────────────────────────────
|
|
201
|
+
if (view === 'setup') {
|
|
93
202
|
return (
|
|
94
|
-
<Card
|
|
203
|
+
<Card>
|
|
95
204
|
<CardHeader>
|
|
96
|
-
<
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
205
|
+
<div className="flex items-center justify-between">
|
|
206
|
+
<CardTitle className="flex items-center gap-2 text-base">
|
|
207
|
+
<Shield className="w-4 h-4" />
|
|
208
|
+
Set up Two-Factor Authentication
|
|
209
|
+
</CardTitle>
|
|
210
|
+
<Button variant="ghost" size="sm" onClick={() => setView('status')}>
|
|
211
|
+
Cancel
|
|
212
|
+
</Button>
|
|
213
|
+
</div>
|
|
100
214
|
<CardDescription>
|
|
101
|
-
|
|
215
|
+
Scan the QR code with your authenticator app (Google Authenticator, Authy, etc.)
|
|
102
216
|
</CardDescription>
|
|
103
217
|
</CardHeader>
|
|
104
218
|
<CardContent>
|
|
105
|
-
<
|
|
106
|
-
onComplete={
|
|
107
|
-
onSkip={
|
|
219
|
+
<SetupStepStandalone
|
|
220
|
+
onComplete={handleSetupDone}
|
|
221
|
+
onSkip={() => setView('status')}
|
|
108
222
|
/>
|
|
109
223
|
</CardContent>
|
|
110
224
|
</Card>
|
|
111
225
|
);
|
|
112
226
|
}
|
|
113
227
|
|
|
114
|
-
// Loading
|
|
115
|
-
if (
|
|
228
|
+
// ── Loading skeleton ────────────────────────────────────────────────────────
|
|
229
|
+
if (isLoading && has2FAEnabled === null) {
|
|
116
230
|
return (
|
|
117
|
-
<Card
|
|
118
|
-
<CardContent className="flex items-center justify-center py-
|
|
119
|
-
<Loader2 className="w-
|
|
231
|
+
<Card>
|
|
232
|
+
<CardContent className="flex items-center justify-center py-10">
|
|
233
|
+
<Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
|
|
120
234
|
</CardContent>
|
|
121
235
|
</Card>
|
|
122
236
|
);
|
|
123
237
|
}
|
|
124
238
|
|
|
125
|
-
//
|
|
239
|
+
// ── Status view ─────────────────────────────────────────────────────────────
|
|
126
240
|
return (
|
|
127
241
|
<>
|
|
128
|
-
<Card
|
|
242
|
+
<Card>
|
|
129
243
|
<CardHeader>
|
|
130
|
-
<
|
|
244
|
+
<div className="flex items-start justify-between gap-4">
|
|
245
|
+
<div className="flex items-center gap-3">
|
|
246
|
+
{has2FAEnabled ? (
|
|
247
|
+
<div className="w-10 h-10 rounded-full bg-green-500/10 flex items-center justify-center flex-shrink-0">
|
|
248
|
+
<ShieldCheck className="w-5 h-5 text-green-500" />
|
|
249
|
+
</div>
|
|
250
|
+
) : (
|
|
251
|
+
<div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center flex-shrink-0">
|
|
252
|
+
<ShieldOff className="w-5 h-5 text-muted-foreground" />
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
<div>
|
|
256
|
+
<div className="flex items-center gap-2">
|
|
257
|
+
<CardTitle className="text-base">Two-Factor Authentication</CardTitle>
|
|
258
|
+
<StatusBadge enabled={!!has2FAEnabled} />
|
|
259
|
+
</div>
|
|
260
|
+
<CardDescription className="mt-0.5">
|
|
261
|
+
{has2FAEnabled
|
|
262
|
+
? `${devices.length} authenticator device${devices.length !== 1 ? 's' : ''} connected`
|
|
263
|
+
: 'Add an extra layer of security to your account'}
|
|
264
|
+
</CardDescription>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
131
268
|
{has2FAEnabled ? (
|
|
132
|
-
<
|
|
269
|
+
<Button
|
|
270
|
+
variant="outline"
|
|
271
|
+
size="sm"
|
|
272
|
+
onClick={() => { clearError(); setShowDisable(true); }}
|
|
273
|
+
disabled={isLoading}
|
|
274
|
+
className="flex-shrink-0 text-destructive hover:text-destructive hover:bg-destructive/10 border-destructive/30"
|
|
275
|
+
>
|
|
276
|
+
Disable
|
|
277
|
+
</Button>
|
|
133
278
|
) : (
|
|
134
|
-
<
|
|
279
|
+
<Button size="sm" onClick={handleEnableClick} disabled={isLoading} className="flex-shrink-0">
|
|
280
|
+
Enable 2FA
|
|
281
|
+
</Button>
|
|
135
282
|
)}
|
|
136
|
-
|
|
137
|
-
</CardTitle>
|
|
138
|
-
<CardDescription>
|
|
139
|
-
{has2FAEnabled
|
|
140
|
-
? 'Your account is protected with two-factor authentication'
|
|
141
|
-
: 'Add an extra layer of security to your account'}
|
|
142
|
-
</CardDescription>
|
|
283
|
+
</div>
|
|
143
284
|
</CardHeader>
|
|
144
285
|
|
|
145
|
-
|
|
146
|
-
|
|
286
|
+
{/* Fetch error */}
|
|
287
|
+
{error && !showDisable && (
|
|
288
|
+
<CardContent className="pt-0">
|
|
147
289
|
<Alert variant="destructive">
|
|
148
|
-
<AlertDescription>{
|
|
290
|
+
<AlertDescription>{error}</AlertDescription>
|
|
149
291
|
</Alert>
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
{has2FAEnabled ? (
|
|
153
|
-
<>
|
|
154
|
-
{/* 2FA Enabled State */}
|
|
155
|
-
<div className="flex items-center justify-between p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
|
|
156
|
-
<div className="flex items-center gap-3">
|
|
157
|
-
<ShieldCheck className="w-8 h-8 text-green-600 dark:text-green-400" />
|
|
158
|
-
<div>
|
|
159
|
-
<p className="font-medium text-green-800 dark:text-green-200">
|
|
160
|
-
2FA is enabled
|
|
161
|
-
</p>
|
|
162
|
-
<p className="text-sm text-green-600 dark:text-green-400">
|
|
163
|
-
{devices.length} authenticator device{devices.length !== 1 ? 's' : ''} connected
|
|
164
|
-
</p>
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
<Button
|
|
168
|
-
variant="outline"
|
|
169
|
-
size="sm"
|
|
170
|
-
onClick={handleDisableClick}
|
|
171
|
-
disabled={statusLoading}
|
|
172
|
-
className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20"
|
|
173
|
-
>
|
|
174
|
-
Disable 2FA
|
|
175
|
-
</Button>
|
|
176
|
-
</div>
|
|
177
|
-
|
|
178
|
-
{/* Device list */}
|
|
179
|
-
{devices.length > 0 && (
|
|
180
|
-
<div className="space-y-2">
|
|
181
|
-
<p className="text-sm font-medium text-muted-foreground">
|
|
182
|
-
Connected Devices
|
|
183
|
-
</p>
|
|
184
|
-
<div className="space-y-2">
|
|
185
|
-
{devices.map((device) => (
|
|
186
|
-
<div
|
|
187
|
-
key={device.id}
|
|
188
|
-
className="flex items-center justify-between p-3 bg-muted/50 rounded-lg"
|
|
189
|
-
>
|
|
190
|
-
<div className="flex items-center gap-3">
|
|
191
|
-
<Shield className="w-5 h-5 text-muted-foreground" />
|
|
192
|
-
<div>
|
|
193
|
-
<p className="font-medium text-sm">{device.name}</p>
|
|
194
|
-
<p className="text-xs text-muted-foreground">
|
|
195
|
-
Added {new Date(device.createdAt).toLocaleDateString()}
|
|
196
|
-
{device.isPrimary && ' • Primary'}
|
|
197
|
-
</p>
|
|
198
|
-
</div>
|
|
199
|
-
</div>
|
|
200
|
-
</div>
|
|
201
|
-
))}
|
|
202
|
-
</div>
|
|
203
|
-
</div>
|
|
204
|
-
)}
|
|
205
|
-
</>
|
|
206
|
-
) : (
|
|
207
|
-
<>
|
|
208
|
-
{/* 2FA Disabled State */}
|
|
209
|
-
<div className="flex items-center justify-between p-4 bg-muted/50 rounded-lg">
|
|
210
|
-
<div className="flex items-center gap-3">
|
|
211
|
-
<ShieldOff className="w-8 h-8 text-muted-foreground" />
|
|
212
|
-
<div>
|
|
213
|
-
<p className="font-medium">2FA is not enabled</p>
|
|
214
|
-
<p className="text-sm text-muted-foreground">
|
|
215
|
-
Protect your account with an authenticator app
|
|
216
|
-
</p>
|
|
217
|
-
</div>
|
|
218
|
-
</div>
|
|
219
|
-
<Button
|
|
220
|
-
onClick={handleEnableClick}
|
|
221
|
-
disabled={statusLoading}
|
|
222
|
-
>
|
|
223
|
-
Enable 2FA
|
|
224
|
-
</Button>
|
|
225
|
-
</div>
|
|
292
|
+
</CardContent>
|
|
293
|
+
)}
|
|
226
294
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
This will make your account less secure.
|
|
247
|
-
</DialogDescription>
|
|
248
|
-
</DialogHeader>
|
|
249
|
-
|
|
250
|
-
<div className="py-4">
|
|
251
|
-
{statusError && (
|
|
252
|
-
<Alert variant="destructive" className="mb-4">
|
|
253
|
-
<AlertDescription>{statusError}</AlertDescription>
|
|
254
|
-
</Alert>
|
|
255
|
-
)}
|
|
295
|
+
{/* Device list */}
|
|
296
|
+
{has2FAEnabled && devices.length > 0 && (
|
|
297
|
+
<CardContent className="pt-0">
|
|
298
|
+
<Separator className="mb-1" />
|
|
299
|
+
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider mt-3 mb-1">
|
|
300
|
+
Connected devices
|
|
301
|
+
</p>
|
|
302
|
+
<div className="divide-y">
|
|
303
|
+
{devices.map((device) => (
|
|
304
|
+
<DeviceRow
|
|
305
|
+
key={device.id}
|
|
306
|
+
name={device.name}
|
|
307
|
+
createdAt={device.createdAt}
|
|
308
|
+
isPrimary={device.isPrimary}
|
|
309
|
+
/>
|
|
310
|
+
))}
|
|
311
|
+
</div>
|
|
312
|
+
</CardContent>
|
|
313
|
+
)}
|
|
256
314
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
size="lg"
|
|
267
|
-
/>
|
|
315
|
+
{/* Not enabled — info callout */}
|
|
316
|
+
{!has2FAEnabled && (
|
|
317
|
+
<CardContent className="pt-0">
|
|
318
|
+
<div className="flex gap-3 p-3 rounded-lg bg-muted/50 text-sm text-muted-foreground">
|
|
319
|
+
<Shield className="w-4 h-4 mt-0.5 flex-shrink-0" />
|
|
320
|
+
<span>
|
|
321
|
+
Two-factor authentication adds a second verification step when signing in,
|
|
322
|
+
protecting your account even if your password is compromised.
|
|
323
|
+
</span>
|
|
268
324
|
</div>
|
|
269
|
-
</
|
|
325
|
+
</CardContent>
|
|
326
|
+
)}
|
|
327
|
+
</Card>
|
|
270
328
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
</Button>
|
|
279
|
-
<Button
|
|
280
|
-
variant="destructive"
|
|
281
|
-
onClick={handleDisableConfirm}
|
|
282
|
-
disabled={statusLoading || disableCode.length !== 6}
|
|
283
|
-
>
|
|
284
|
-
{statusLoading ? (
|
|
285
|
-
<>
|
|
286
|
-
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
287
|
-
Disabling...
|
|
288
|
-
</>
|
|
289
|
-
) : (
|
|
290
|
-
'Disable 2FA'
|
|
291
|
-
)}
|
|
292
|
-
</Button>
|
|
293
|
-
</DialogFooter>
|
|
294
|
-
</DialogContent>
|
|
295
|
-
</Dialog>
|
|
329
|
+
<DisableDialog
|
|
330
|
+
open={showDisable}
|
|
331
|
+
isLoading={isLoading}
|
|
332
|
+
error={error}
|
|
333
|
+
onConfirm={handleDisableConfirm}
|
|
334
|
+
onCancel={handleDisableCancel}
|
|
335
|
+
/>
|
|
296
336
|
</>
|
|
297
337
|
);
|
|
298
338
|
};
|