@wakastellar/ui 2.1.0 → 2.1.2
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 +71 -8
- package/dist/blocks/auth-2fa/index.d.ts +38 -0
- package/dist/blocks/chat-interface/index.d.ts +66 -0
- package/dist/blocks/checkout-flow/index.d.ts +76 -0
- package/dist/blocks/dashboard-kpi/index.d.ts +69 -0
- package/dist/blocks/deployment-dashboard/index.d.ts +68 -0
- package/dist/blocks/index.d.ts +7 -0
- package/dist/blocks/player-profile/index.d.ts +78 -0
- package/dist/cli/index.cjs +1324 -154
- package/dist/index.cjs.js +136 -134
- package/dist/index.es.js +17304 -15120
- package/package.json +1 -1
- package/src/blocks/auth-2fa/index.tsx +655 -0
- package/src/blocks/chat-interface/index.tsx +611 -0
- package/src/blocks/checkout-flow/index.tsx +771 -0
- package/src/blocks/dashboard-kpi/index.tsx +424 -0
- package/src/blocks/deployment-dashboard/index.tsx +609 -0
- package/src/blocks/index.ts +24 -0
- package/src/blocks/player-profile/index.tsx +541 -0
- package/src/components/language-selector/index.tsx +21 -11
package/package.json
CHANGED
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../utils"
|
|
5
|
+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "../../components/card"
|
|
6
|
+
import { Button } from "../../components/button"
|
|
7
|
+
import { Input } from "../../components/input"
|
|
8
|
+
import { Label } from "../../components/label"
|
|
9
|
+
import { Badge } from "../../components/badge"
|
|
10
|
+
import { Separator } from "../../components/separator"
|
|
11
|
+
import { RadioGroup, RadioGroupItem } from "../../components/radio-group"
|
|
12
|
+
import {
|
|
13
|
+
Shield,
|
|
14
|
+
Smartphone,
|
|
15
|
+
Mail,
|
|
16
|
+
Key,
|
|
17
|
+
Copy,
|
|
18
|
+
Check,
|
|
19
|
+
RefreshCw,
|
|
20
|
+
AlertTriangle,
|
|
21
|
+
Lock,
|
|
22
|
+
QrCode,
|
|
23
|
+
Download,
|
|
24
|
+
Eye,
|
|
25
|
+
EyeOff,
|
|
26
|
+
ChevronRight,
|
|
27
|
+
ShieldCheck,
|
|
28
|
+
ShieldAlert,
|
|
29
|
+
Fingerprint,
|
|
30
|
+
MessageSquare,
|
|
31
|
+
} from "lucide-react"
|
|
32
|
+
|
|
33
|
+
// ============================================
|
|
34
|
+
// TYPES
|
|
35
|
+
// ============================================
|
|
36
|
+
|
|
37
|
+
export type TwoFactorMethod = "authenticator" | "sms" | "email" | "hardware"
|
|
38
|
+
|
|
39
|
+
export interface BackupCode {
|
|
40
|
+
code: string
|
|
41
|
+
used: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface TwoFactorStatus {
|
|
45
|
+
enabled: boolean
|
|
46
|
+
method?: TwoFactorMethod
|
|
47
|
+
phone?: string
|
|
48
|
+
email?: string
|
|
49
|
+
lastVerified?: Date | string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface Auth2FAProps {
|
|
53
|
+
/** Current 2FA status */
|
|
54
|
+
status?: TwoFactorStatus
|
|
55
|
+
/** Available methods */
|
|
56
|
+
availableMethods?: TwoFactorMethod[]
|
|
57
|
+
/** QR code data URL for authenticator setup */
|
|
58
|
+
qrCodeUrl?: string
|
|
59
|
+
/** Secret key for manual entry */
|
|
60
|
+
secretKey?: string
|
|
61
|
+
/** Backup codes */
|
|
62
|
+
backupCodes?: BackupCode[]
|
|
63
|
+
/** Handler for enabling 2FA */
|
|
64
|
+
onEnable?: (method: TwoFactorMethod, verificationCode: string) => Promise<boolean>
|
|
65
|
+
/** Handler for disabling 2FA */
|
|
66
|
+
onDisable?: (verificationCode: string) => Promise<boolean>
|
|
67
|
+
/** Handler for regenerating backup codes */
|
|
68
|
+
onRegenerateBackupCodes?: () => Promise<BackupCode[]>
|
|
69
|
+
/** Handler for sending verification code */
|
|
70
|
+
onSendCode?: (method: TwoFactorMethod) => Promise<boolean>
|
|
71
|
+
/** Custom className */
|
|
72
|
+
className?: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============================================
|
|
76
|
+
// SUBCOMPONENTS
|
|
77
|
+
// ============================================
|
|
78
|
+
|
|
79
|
+
const methodConfig = {
|
|
80
|
+
authenticator: {
|
|
81
|
+
icon: <Smartphone className="h-5 w-5" />,
|
|
82
|
+
title: "Authenticator App",
|
|
83
|
+
description: "Use an app like Google Authenticator or Authy",
|
|
84
|
+
recommended: true,
|
|
85
|
+
},
|
|
86
|
+
sms: {
|
|
87
|
+
icon: <MessageSquare className="h-5 w-5" />,
|
|
88
|
+
title: "SMS",
|
|
89
|
+
description: "Receive codes via text message",
|
|
90
|
+
recommended: false,
|
|
91
|
+
},
|
|
92
|
+
email: {
|
|
93
|
+
icon: <Mail className="h-5 w-5" />,
|
|
94
|
+
title: "Email",
|
|
95
|
+
description: "Receive codes via email",
|
|
96
|
+
recommended: false,
|
|
97
|
+
},
|
|
98
|
+
hardware: {
|
|
99
|
+
icon: <Key className="h-5 w-5" />,
|
|
100
|
+
title: "Hardware Key",
|
|
101
|
+
description: "Use a physical security key (YubiKey, etc.)",
|
|
102
|
+
recommended: true,
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function MethodSelector({
|
|
107
|
+
methods,
|
|
108
|
+
selected,
|
|
109
|
+
onSelect,
|
|
110
|
+
}: {
|
|
111
|
+
methods: TwoFactorMethod[]
|
|
112
|
+
selected: TwoFactorMethod
|
|
113
|
+
onSelect: (method: TwoFactorMethod) => void
|
|
114
|
+
}) {
|
|
115
|
+
return (
|
|
116
|
+
<RadioGroup value={selected} onValueChange={(v) => onSelect(v as TwoFactorMethod)}>
|
|
117
|
+
<div className="space-y-3">
|
|
118
|
+
{methods.map((method) => {
|
|
119
|
+
const config = methodConfig[method]
|
|
120
|
+
return (
|
|
121
|
+
<Label
|
|
122
|
+
key={method}
|
|
123
|
+
className={cn(
|
|
124
|
+
"flex items-center gap-4 p-4 rounded-lg border cursor-pointer transition-colors",
|
|
125
|
+
selected === method ? "border-primary bg-primary/5" : "hover:bg-muted/50"
|
|
126
|
+
)}
|
|
127
|
+
>
|
|
128
|
+
<RadioGroupItem value={method} />
|
|
129
|
+
<div className="h-10 w-10 rounded-lg bg-muted flex items-center justify-center">
|
|
130
|
+
{config.icon}
|
|
131
|
+
</div>
|
|
132
|
+
<div className="flex-1">
|
|
133
|
+
<div className="flex items-center gap-2">
|
|
134
|
+
<span className="font-medium">{config.title}</span>
|
|
135
|
+
{config.recommended && (
|
|
136
|
+
<Badge variant="secondary" className="text-xs">Recommended</Badge>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
<p className="text-sm text-muted-foreground">{config.description}</p>
|
|
140
|
+
</div>
|
|
141
|
+
</Label>
|
|
142
|
+
)
|
|
143
|
+
})}
|
|
144
|
+
</div>
|
|
145
|
+
</RadioGroup>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function CodeInput({
|
|
150
|
+
length = 6,
|
|
151
|
+
value,
|
|
152
|
+
onChange,
|
|
153
|
+
}: {
|
|
154
|
+
length?: number
|
|
155
|
+
value: string
|
|
156
|
+
onChange: (value: string) => void
|
|
157
|
+
}) {
|
|
158
|
+
const inputRefs = React.useRef<(HTMLInputElement | null)[]>([])
|
|
159
|
+
const digits = value.padEnd(length, "").split("").slice(0, length)
|
|
160
|
+
|
|
161
|
+
const handleInput = (index: number, newValue: string) => {
|
|
162
|
+
if (!/^\d*$/.test(newValue)) return
|
|
163
|
+
|
|
164
|
+
const newDigits = [...digits]
|
|
165
|
+
newDigits[index] = newValue.slice(-1)
|
|
166
|
+
onChange(newDigits.join(""))
|
|
167
|
+
|
|
168
|
+
if (newValue && index < length - 1) {
|
|
169
|
+
inputRefs.current[index + 1]?.focus()
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const handleKeyDown = (index: number, e: React.KeyboardEvent) => {
|
|
174
|
+
if (e.key === "Backspace" && !digits[index] && index > 0) {
|
|
175
|
+
inputRefs.current[index - 1]?.focus()
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const handlePaste = (e: React.ClipboardEvent) => {
|
|
180
|
+
e.preventDefault()
|
|
181
|
+
const pasteData = e.clipboardData.getData("text").replace(/\D/g, "").slice(0, length)
|
|
182
|
+
onChange(pasteData)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div className="flex gap-2 justify-center">
|
|
187
|
+
{digits.map((digit, index) => (
|
|
188
|
+
<Input
|
|
189
|
+
key={index}
|
|
190
|
+
ref={(el) => { inputRefs.current[index] = el }}
|
|
191
|
+
type="text"
|
|
192
|
+
inputMode="numeric"
|
|
193
|
+
maxLength={1}
|
|
194
|
+
value={digit}
|
|
195
|
+
onChange={(e) => handleInput(index, e.target.value)}
|
|
196
|
+
onKeyDown={(e) => handleKeyDown(index, e)}
|
|
197
|
+
onPaste={handlePaste}
|
|
198
|
+
className="w-12 h-14 text-center text-2xl font-mono"
|
|
199
|
+
/>
|
|
200
|
+
))}
|
|
201
|
+
</div>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function BackupCodesDisplay({
|
|
206
|
+
codes,
|
|
207
|
+
onRegenerate,
|
|
208
|
+
onDownload,
|
|
209
|
+
}: {
|
|
210
|
+
codes: BackupCode[]
|
|
211
|
+
onRegenerate?: () => void
|
|
212
|
+
onDownload?: () => void
|
|
213
|
+
}) {
|
|
214
|
+
const [copied, setCopied] = React.useState(false)
|
|
215
|
+
const [showCodes, setShowCodes] = React.useState(false)
|
|
216
|
+
|
|
217
|
+
const copyAllCodes = () => {
|
|
218
|
+
const codesText = codes.map((c) => c.code).join("\n")
|
|
219
|
+
navigator.clipboard.writeText(codesText)
|
|
220
|
+
setCopied(true)
|
|
221
|
+
setTimeout(() => setCopied(false), 2000)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const unusedCodes = codes.filter((c) => !c.used)
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<Card>
|
|
228
|
+
<CardHeader>
|
|
229
|
+
<div className="flex items-center justify-between">
|
|
230
|
+
<div>
|
|
231
|
+
<CardTitle className="flex items-center gap-2">
|
|
232
|
+
<Key className="h-5 w-5" />
|
|
233
|
+
Backup Codes
|
|
234
|
+
</CardTitle>
|
|
235
|
+
<CardDescription>
|
|
236
|
+
{unusedCodes.length} of {codes.length} codes remaining
|
|
237
|
+
</CardDescription>
|
|
238
|
+
</div>
|
|
239
|
+
<Button variant="ghost" size="icon" onClick={() => setShowCodes(!showCodes)}>
|
|
240
|
+
{showCodes ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
241
|
+
</Button>
|
|
242
|
+
</div>
|
|
243
|
+
</CardHeader>
|
|
244
|
+
<CardContent>
|
|
245
|
+
<div className="grid grid-cols-2 gap-2">
|
|
246
|
+
{codes.map((code, index) => (
|
|
247
|
+
<div
|
|
248
|
+
key={index}
|
|
249
|
+
className={cn(
|
|
250
|
+
"font-mono text-sm p-2 rounded border text-center",
|
|
251
|
+
code.used
|
|
252
|
+
? "bg-muted text-muted-foreground line-through"
|
|
253
|
+
: "bg-card"
|
|
254
|
+
)}
|
|
255
|
+
>
|
|
256
|
+
{showCodes ? code.code : "••••••••"}
|
|
257
|
+
</div>
|
|
258
|
+
))}
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
{unusedCodes.length < 3 && (
|
|
262
|
+
<div className="mt-4 p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/20 flex items-center gap-3">
|
|
263
|
+
<AlertTriangle className="h-5 w-5 text-yellow-500" />
|
|
264
|
+
<p className="text-sm">You're running low on backup codes. Consider regenerating them.</p>
|
|
265
|
+
</div>
|
|
266
|
+
)}
|
|
267
|
+
</CardContent>
|
|
268
|
+
<CardFooter className="flex gap-2">
|
|
269
|
+
<Button variant="outline" size="sm" onClick={copyAllCodes}>
|
|
270
|
+
{copied ? <Check className="h-4 w-4 mr-2" /> : <Copy className="h-4 w-4 mr-2" />}
|
|
271
|
+
{copied ? "Copied" : "Copy All"}
|
|
272
|
+
</Button>
|
|
273
|
+
{onDownload && (
|
|
274
|
+
<Button variant="outline" size="sm" onClick={onDownload}>
|
|
275
|
+
<Download className="h-4 w-4 mr-2" />
|
|
276
|
+
Download
|
|
277
|
+
</Button>
|
|
278
|
+
)}
|
|
279
|
+
{onRegenerate && (
|
|
280
|
+
<Button variant="outline" size="sm" onClick={onRegenerate}>
|
|
281
|
+
<RefreshCw className="h-4 w-4 mr-2" />
|
|
282
|
+
Regenerate
|
|
283
|
+
</Button>
|
|
284
|
+
)}
|
|
285
|
+
</CardFooter>
|
|
286
|
+
</Card>
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ============================================
|
|
291
|
+
// MAIN COMPONENT
|
|
292
|
+
// ============================================
|
|
293
|
+
|
|
294
|
+
export function Auth2FA({
|
|
295
|
+
status = { enabled: false },
|
|
296
|
+
availableMethods = ["authenticator", "sms", "email"],
|
|
297
|
+
qrCodeUrl,
|
|
298
|
+
secretKey,
|
|
299
|
+
backupCodes,
|
|
300
|
+
onEnable,
|
|
301
|
+
onDisable,
|
|
302
|
+
onRegenerateBackupCodes,
|
|
303
|
+
onSendCode,
|
|
304
|
+
className,
|
|
305
|
+
}: Auth2FAProps) {
|
|
306
|
+
const [step, setStep] = React.useState<"overview" | "setup" | "verify" | "disable">("overview")
|
|
307
|
+
const [selectedMethod, setSelectedMethod] = React.useState<TwoFactorMethod>(availableMethods[0])
|
|
308
|
+
const [verificationCode, setVerificationCode] = React.useState("")
|
|
309
|
+
const [isLoading, setIsLoading] = React.useState(false)
|
|
310
|
+
const [error, setError] = React.useState<string | null>(null)
|
|
311
|
+
const [copied, setCopied] = React.useState(false)
|
|
312
|
+
const [codeSent, setCodeSent] = React.useState(false)
|
|
313
|
+
|
|
314
|
+
const handleCopySecret = () => {
|
|
315
|
+
if (secretKey) {
|
|
316
|
+
navigator.clipboard.writeText(secretKey)
|
|
317
|
+
setCopied(true)
|
|
318
|
+
setTimeout(() => setCopied(false), 2000)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const handleSendCode = async () => {
|
|
323
|
+
if (!onSendCode) return
|
|
324
|
+
setIsLoading(true)
|
|
325
|
+
try {
|
|
326
|
+
await onSendCode(selectedMethod)
|
|
327
|
+
setCodeSent(true)
|
|
328
|
+
} finally {
|
|
329
|
+
setIsLoading(false)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const handleEnable = async () => {
|
|
334
|
+
if (!onEnable) return
|
|
335
|
+
setIsLoading(true)
|
|
336
|
+
setError(null)
|
|
337
|
+
try {
|
|
338
|
+
const success = await onEnable(selectedMethod, verificationCode)
|
|
339
|
+
if (success) {
|
|
340
|
+
setStep("overview")
|
|
341
|
+
setVerificationCode("")
|
|
342
|
+
} else {
|
|
343
|
+
setError("Invalid verification code. Please try again.")
|
|
344
|
+
}
|
|
345
|
+
} finally {
|
|
346
|
+
setIsLoading(false)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const handleDisable = async () => {
|
|
351
|
+
if (!onDisable) return
|
|
352
|
+
setIsLoading(true)
|
|
353
|
+
setError(null)
|
|
354
|
+
try {
|
|
355
|
+
const success = await onDisable(verificationCode)
|
|
356
|
+
if (success) {
|
|
357
|
+
setStep("overview")
|
|
358
|
+
setVerificationCode("")
|
|
359
|
+
} else {
|
|
360
|
+
setError("Invalid verification code. Please try again.")
|
|
361
|
+
}
|
|
362
|
+
} finally {
|
|
363
|
+
setIsLoading(false)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return (
|
|
368
|
+
<div className={cn("space-y-6 max-w-2xl", className)}>
|
|
369
|
+
{/* Status Card */}
|
|
370
|
+
<Card>
|
|
371
|
+
<CardHeader>
|
|
372
|
+
<div className="flex items-center justify-between">
|
|
373
|
+
<div className="flex items-center gap-3">
|
|
374
|
+
{status.enabled ? (
|
|
375
|
+
<div className="h-12 w-12 rounded-full bg-green-500/10 flex items-center justify-center">
|
|
376
|
+
<ShieldCheck className="h-6 w-6 text-green-500" />
|
|
377
|
+
</div>
|
|
378
|
+
) : (
|
|
379
|
+
<div className="h-12 w-12 rounded-full bg-yellow-500/10 flex items-center justify-center">
|
|
380
|
+
<ShieldAlert className="h-6 w-6 text-yellow-500" />
|
|
381
|
+
</div>
|
|
382
|
+
)}
|
|
383
|
+
<div>
|
|
384
|
+
<CardTitle>Two-Factor Authentication</CardTitle>
|
|
385
|
+
<CardDescription>
|
|
386
|
+
{status.enabled
|
|
387
|
+
? `Enabled via ${methodConfig[status.method!]?.title}`
|
|
388
|
+
: "Add an extra layer of security to your account"}
|
|
389
|
+
</CardDescription>
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
<Badge variant={status.enabled ? "default" : "secondary"}>
|
|
393
|
+
{status.enabled ? "Enabled" : "Disabled"}
|
|
394
|
+
</Badge>
|
|
395
|
+
</div>
|
|
396
|
+
</CardHeader>
|
|
397
|
+
{step === "overview" && (
|
|
398
|
+
<CardFooter>
|
|
399
|
+
{status.enabled ? (
|
|
400
|
+
<div className="flex gap-2 w-full">
|
|
401
|
+
<Button variant="outline" className="flex-1" onClick={() => setStep("setup")}>
|
|
402
|
+
Change Method
|
|
403
|
+
</Button>
|
|
404
|
+
<Button variant="destructive" className="flex-1" onClick={() => setStep("disable")}>
|
|
405
|
+
Disable 2FA
|
|
406
|
+
</Button>
|
|
407
|
+
</div>
|
|
408
|
+
) : (
|
|
409
|
+
<Button className="w-full" onClick={() => setStep("setup")}>
|
|
410
|
+
<Shield className="h-4 w-4 mr-2" />
|
|
411
|
+
Enable Two-Factor Authentication
|
|
412
|
+
</Button>
|
|
413
|
+
)}
|
|
414
|
+
</CardFooter>
|
|
415
|
+
)}
|
|
416
|
+
</Card>
|
|
417
|
+
|
|
418
|
+
{/* Setup Step */}
|
|
419
|
+
{step === "setup" && (
|
|
420
|
+
<Card>
|
|
421
|
+
<CardHeader>
|
|
422
|
+
<CardTitle>Choose Authentication Method</CardTitle>
|
|
423
|
+
<CardDescription>Select how you want to receive verification codes</CardDescription>
|
|
424
|
+
</CardHeader>
|
|
425
|
+
<CardContent>
|
|
426
|
+
<MethodSelector
|
|
427
|
+
methods={availableMethods}
|
|
428
|
+
selected={selectedMethod}
|
|
429
|
+
onSelect={setSelectedMethod}
|
|
430
|
+
/>
|
|
431
|
+
</CardContent>
|
|
432
|
+
<CardFooter className="flex gap-2">
|
|
433
|
+
<Button variant="outline" onClick={() => setStep("overview")}>
|
|
434
|
+
Cancel
|
|
435
|
+
</Button>
|
|
436
|
+
<Button className="flex-1" onClick={() => setStep("verify")}>
|
|
437
|
+
Continue
|
|
438
|
+
<ChevronRight className="h-4 w-4 ml-2" />
|
|
439
|
+
</Button>
|
|
440
|
+
</CardFooter>
|
|
441
|
+
</Card>
|
|
442
|
+
)}
|
|
443
|
+
|
|
444
|
+
{/* Verification Step */}
|
|
445
|
+
{step === "verify" && (
|
|
446
|
+
<Card>
|
|
447
|
+
<CardHeader>
|
|
448
|
+
<CardTitle>
|
|
449
|
+
{selectedMethod === "authenticator" ? "Set Up Authenticator" : "Verify Your Identity"}
|
|
450
|
+
</CardTitle>
|
|
451
|
+
<CardDescription>
|
|
452
|
+
{selectedMethod === "authenticator"
|
|
453
|
+
? "Scan the QR code with your authenticator app"
|
|
454
|
+
: `Enter the code sent to your ${selectedMethod}`}
|
|
455
|
+
</CardDescription>
|
|
456
|
+
</CardHeader>
|
|
457
|
+
<CardContent className="space-y-6">
|
|
458
|
+
{selectedMethod === "authenticator" && (
|
|
459
|
+
<>
|
|
460
|
+
{qrCodeUrl && (
|
|
461
|
+
<div className="flex justify-center">
|
|
462
|
+
<div className="p-4 bg-white rounded-lg">
|
|
463
|
+
<img src={qrCodeUrl} alt="QR Code" className="h-48 w-48" />
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
)}
|
|
467
|
+
|
|
468
|
+
{secretKey && (
|
|
469
|
+
<div className="space-y-2">
|
|
470
|
+
<Label>Or enter this code manually:</Label>
|
|
471
|
+
<div className="flex items-center gap-2">
|
|
472
|
+
<Input value={secretKey} readOnly className="font-mono" />
|
|
473
|
+
<Button variant="outline" size="icon" onClick={handleCopySecret}>
|
|
474
|
+
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
|
475
|
+
</Button>
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
)}
|
|
479
|
+
|
|
480
|
+
<Separator />
|
|
481
|
+
</>
|
|
482
|
+
)}
|
|
483
|
+
|
|
484
|
+
{(selectedMethod === "sms" || selectedMethod === "email") && !codeSent && (
|
|
485
|
+
<div className="text-center">
|
|
486
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
487
|
+
We'll send a verification code to your {selectedMethod === "sms" ? "phone" : "email"}
|
|
488
|
+
</p>
|
|
489
|
+
<Button onClick={handleSendCode} disabled={isLoading}>
|
|
490
|
+
{isLoading ? (
|
|
491
|
+
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
|
|
492
|
+
) : selectedMethod === "sms" ? (
|
|
493
|
+
<MessageSquare className="h-4 w-4 mr-2" />
|
|
494
|
+
) : (
|
|
495
|
+
<Mail className="h-4 w-4 mr-2" />
|
|
496
|
+
)}
|
|
497
|
+
Send Code
|
|
498
|
+
</Button>
|
|
499
|
+
</div>
|
|
500
|
+
)}
|
|
501
|
+
|
|
502
|
+
{(selectedMethod === "authenticator" || codeSent) && (
|
|
503
|
+
<div className="space-y-4">
|
|
504
|
+
<Label className="text-center block">Enter the 6-digit code</Label>
|
|
505
|
+
<CodeInput value={verificationCode} onChange={setVerificationCode} />
|
|
506
|
+
{error && (
|
|
507
|
+
<p className="text-sm text-destructive text-center">{error}</p>
|
|
508
|
+
)}
|
|
509
|
+
</div>
|
|
510
|
+
)}
|
|
511
|
+
</CardContent>
|
|
512
|
+
<CardFooter className="flex gap-2">
|
|
513
|
+
<Button variant="outline" onClick={() => setStep("setup")}>
|
|
514
|
+
Back
|
|
515
|
+
</Button>
|
|
516
|
+
<Button
|
|
517
|
+
className="flex-1"
|
|
518
|
+
onClick={handleEnable}
|
|
519
|
+
disabled={verificationCode.length !== 6 || isLoading}
|
|
520
|
+
>
|
|
521
|
+
{isLoading ? (
|
|
522
|
+
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
|
|
523
|
+
) : (
|
|
524
|
+
<Lock className="h-4 w-4 mr-2" />
|
|
525
|
+
)}
|
|
526
|
+
Verify & Enable
|
|
527
|
+
</Button>
|
|
528
|
+
</CardFooter>
|
|
529
|
+
</Card>
|
|
530
|
+
)}
|
|
531
|
+
|
|
532
|
+
{/* Disable Step */}
|
|
533
|
+
{step === "disable" && (
|
|
534
|
+
<Card>
|
|
535
|
+
<CardHeader>
|
|
536
|
+
<CardTitle className="text-destructive">Disable Two-Factor Authentication</CardTitle>
|
|
537
|
+
<CardDescription>
|
|
538
|
+
Enter your verification code to confirm. This will make your account less secure.
|
|
539
|
+
</CardDescription>
|
|
540
|
+
</CardHeader>
|
|
541
|
+
<CardContent className="space-y-4">
|
|
542
|
+
<div className="p-4 rounded-lg bg-destructive/10 border border-destructive/20">
|
|
543
|
+
<div className="flex items-start gap-3">
|
|
544
|
+
<AlertTriangle className="h-5 w-5 text-destructive mt-0.5" />
|
|
545
|
+
<div>
|
|
546
|
+
<p className="font-medium text-destructive">Warning</p>
|
|
547
|
+
<p className="text-sm text-muted-foreground">
|
|
548
|
+
Disabling 2FA will remove an important layer of security from your account.
|
|
549
|
+
Anyone with your password will be able to access your account.
|
|
550
|
+
</p>
|
|
551
|
+
</div>
|
|
552
|
+
</div>
|
|
553
|
+
</div>
|
|
554
|
+
|
|
555
|
+
<div className="space-y-2">
|
|
556
|
+
<Label>Enter your verification code</Label>
|
|
557
|
+
<CodeInput value={verificationCode} onChange={setVerificationCode} />
|
|
558
|
+
{error && (
|
|
559
|
+
<p className="text-sm text-destructive text-center">{error}</p>
|
|
560
|
+
)}
|
|
561
|
+
</div>
|
|
562
|
+
</CardContent>
|
|
563
|
+
<CardFooter className="flex gap-2">
|
|
564
|
+
<Button variant="outline" onClick={() => setStep("overview")}>
|
|
565
|
+
Cancel
|
|
566
|
+
</Button>
|
|
567
|
+
<Button
|
|
568
|
+
variant="destructive"
|
|
569
|
+
className="flex-1"
|
|
570
|
+
onClick={handleDisable}
|
|
571
|
+
disabled={verificationCode.length !== 6 || isLoading}
|
|
572
|
+
>
|
|
573
|
+
{isLoading ? (
|
|
574
|
+
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
|
|
575
|
+
) : (
|
|
576
|
+
<ShieldAlert className="h-4 w-4 mr-2" />
|
|
577
|
+
)}
|
|
578
|
+
Disable 2FA
|
|
579
|
+
</Button>
|
|
580
|
+
</CardFooter>
|
|
581
|
+
</Card>
|
|
582
|
+
)}
|
|
583
|
+
|
|
584
|
+
{/* Backup Codes */}
|
|
585
|
+
{status.enabled && backupCodes && backupCodes.length > 0 && step === "overview" && (
|
|
586
|
+
<BackupCodesDisplay
|
|
587
|
+
codes={backupCodes}
|
|
588
|
+
onRegenerate={onRegenerateBackupCodes}
|
|
589
|
+
onDownload={() => {
|
|
590
|
+
const codesText = backupCodes.map((c) => c.code).join("\n")
|
|
591
|
+
const blob = new Blob([codesText], { type: "text/plain" })
|
|
592
|
+
const url = URL.createObjectURL(blob)
|
|
593
|
+
const a = document.createElement("a")
|
|
594
|
+
a.href = url
|
|
595
|
+
a.download = "backup-codes.txt"
|
|
596
|
+
a.click()
|
|
597
|
+
}}
|
|
598
|
+
/>
|
|
599
|
+
)}
|
|
600
|
+
|
|
601
|
+
{/* Security Tips */}
|
|
602
|
+
<Card>
|
|
603
|
+
<CardHeader>
|
|
604
|
+
<CardTitle className="flex items-center gap-2">
|
|
605
|
+
<Fingerprint className="h-5 w-5" />
|
|
606
|
+
Security Tips
|
|
607
|
+
</CardTitle>
|
|
608
|
+
</CardHeader>
|
|
609
|
+
<CardContent className="space-y-3">
|
|
610
|
+
<div className="flex items-start gap-3">
|
|
611
|
+
<Check className="h-4 w-4 text-green-500 mt-1" />
|
|
612
|
+
<p className="text-sm text-muted-foreground">
|
|
613
|
+
Use an authenticator app for the most secure experience
|
|
614
|
+
</p>
|
|
615
|
+
</div>
|
|
616
|
+
<div className="flex items-start gap-3">
|
|
617
|
+
<Check className="h-4 w-4 text-green-500 mt-1" />
|
|
618
|
+
<p className="text-sm text-muted-foreground">
|
|
619
|
+
Store your backup codes in a safe place (password manager, safe deposit box)
|
|
620
|
+
</p>
|
|
621
|
+
</div>
|
|
622
|
+
<div className="flex items-start gap-3">
|
|
623
|
+
<Check className="h-4 w-4 text-green-500 mt-1" />
|
|
624
|
+
<p className="text-sm text-muted-foreground">
|
|
625
|
+
Never share your verification codes with anyone
|
|
626
|
+
</p>
|
|
627
|
+
</div>
|
|
628
|
+
</CardContent>
|
|
629
|
+
</Card>
|
|
630
|
+
</div>
|
|
631
|
+
)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// ============================================
|
|
635
|
+
// PRESET DATA
|
|
636
|
+
// ============================================
|
|
637
|
+
|
|
638
|
+
export const defaultBackupCodes: BackupCode[] = [
|
|
639
|
+
{ code: "ABCD-1234", used: false },
|
|
640
|
+
{ code: "EFGH-5678", used: false },
|
|
641
|
+
{ code: "IJKL-9012", used: true },
|
|
642
|
+
{ code: "MNOP-3456", used: false },
|
|
643
|
+
{ code: "QRST-7890", used: false },
|
|
644
|
+
{ code: "UVWX-1234", used: true },
|
|
645
|
+
{ code: "YZAB-5678", used: false },
|
|
646
|
+
{ code: "CDEF-9012", used: false },
|
|
647
|
+
]
|
|
648
|
+
|
|
649
|
+
export const defaultStatus: TwoFactorStatus = {
|
|
650
|
+
enabled: true,
|
|
651
|
+
method: "authenticator",
|
|
652
|
+
lastVerified: new Date("2024-03-01"),
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export default Auth2FA
|