Files
ProxMenux/AppImage/components/two-factor-setup.tsx

262 lines
9.2 KiB
TypeScript
Raw Normal View History

2025-11-07 20:36:46 +01:00
"use client"
import { useState } from "react"
import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog"
import { AlertCircle, CheckCircle, Copy, Shield, Check } from "lucide-react"
import { getApiUrl } from "../lib/api-config"
interface TwoFactorSetupProps {
open: boolean
onClose: () => void
onSuccess: () => void
}
export function TwoFactorSetup({ open, onClose, onSuccess }: TwoFactorSetupProps) {
const [step, setStep] = useState(1)
const [qrCode, setQrCode] = useState("")
const [secret, setSecret] = useState("")
const [backupCodes, setBackupCodes] = useState<string[]>([])
const [verificationCode, setVerificationCode] = useState("")
const [error, setError] = useState("")
const [loading, setLoading] = useState(false)
const [copiedSecret, setCopiedSecret] = useState(false)
const [copiedCodes, setCopiedCodes] = useState(false)
const handleSetupStart = async () => {
setError("")
setLoading(true)
try {
const token = localStorage.getItem("proxmenux-auth-token")
const response = await fetch(getApiUrl("/api/auth/totp/setup"), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || "Failed to setup 2FA")
}
setQrCode(data.qr_code)
setSecret(data.secret)
setBackupCodes(data.backup_codes)
setStep(2)
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to setup 2FA")
} finally {
setLoading(false)
}
}
const handleVerify = async () => {
if (!verificationCode || verificationCode.length !== 6) {
setError("Please enter a 6-digit code")
return
}
setError("")
setLoading(true)
try {
const token = localStorage.getItem("proxmenux-auth-token")
const response = await fetch(getApiUrl("/api/auth/totp/enable"), {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ token: verificationCode }),
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || "Invalid verification code")
}
setStep(3)
} catch (err) {
setError(err instanceof Error ? err.message : "Verification failed")
} finally {
setLoading(false)
}
}
const copyToClipboard = (text: string, type: "secret" | "codes") => {
navigator.clipboard.writeText(text)
if (type === "secret") {
setCopiedSecret(true)
setTimeout(() => setCopiedSecret(false), 2000)
} else {
setCopiedCodes(true)
setTimeout(() => setCopiedCodes(false), 2000)
}
}
const handleClose = () => {
setStep(1)
setQrCode("")
setSecret("")
setBackupCodes([])
setVerificationCode("")
setError("")
onClose()
}
const handleFinish = () => {
handleClose()
onSuccess()
}
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-md max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Shield className="h-5 w-5 text-blue-500" />
2025-11-07 21:14:56 +01:00
Setup Two-Factor Authentication
2025-11-07 20:36:46 +01:00
</DialogTitle>
2025-11-07 21:14:56 +01:00
<DialogDescription>Add an extra layer of security to your account</DialogDescription>
2025-11-07 20:36:46 +01:00
</DialogHeader>
{error && (
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3 flex items-start gap-2">
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-500">{error}</p>
</div>
)}
{step === 1 && (
<div className="space-y-4">
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4">
<p className="text-sm text-blue-500">
2025-11-07 21:14:56 +01:00
Two-factor authentication (2FA) adds an extra layer of security by requiring a code from your
authentication app in addition to your password.
2025-11-07 20:36:46 +01:00
</p>
</div>
<div className="space-y-2">
2025-11-07 21:14:56 +01:00
<h4 className="font-medium">You will need:</h4>
2025-11-07 20:36:46 +01:00
<ul className="text-sm text-muted-foreground space-y-1 list-disc list-inside">
2025-11-07 21:14:56 +01:00
<li>An authentication app (Google Authenticator, Authy, etc.)</li>
<li>Scan a QR code or enter a key manually</li>
<li>Store backup codes securely</li>
2025-11-07 20:36:46 +01:00
</ul>
</div>
<Button onClick={handleSetupStart} className="w-full bg-blue-500 hover:bg-blue-600" disabled={loading}>
2025-11-07 21:14:56 +01:00
{loading ? "Starting..." : "Start Setup"}
2025-11-07 20:36:46 +01:00
</Button>
</div>
)}
{step === 2 && (
<div className="space-y-4">
<div className="space-y-2">
2025-11-07 21:14:56 +01:00
<h4 className="font-medium">1. Scan the QR code</h4>
<p className="text-sm text-muted-foreground">Open your authentication app and scan this QR code</p>
2025-11-07 20:36:46 +01:00
{qrCode && (
<div className="flex justify-center p-4 bg-white rounded-lg">
2025-11-07 21:02:20 +01:00
<img src={qrCode || "/placeholder.svg"} alt="QR Code" width={200} height={200} className="rounded" />
2025-11-07 20:36:46 +01:00
</div>
)}
</div>
<div className="space-y-2">
2025-11-07 21:14:56 +01:00
<h4 className="font-medium">Or enter the key manually:</h4>
2025-11-07 20:36:46 +01:00
<div className="flex gap-2">
<Input value={secret} readOnly className="font-mono text-sm" />
<Button
variant="outline"
size="icon"
onClick={() => copyToClipboard(secret, "secret")}
2025-11-07 21:14:56 +01:00
title="Copy key"
2025-11-07 20:36:46 +01:00
>
{copiedSecret ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
</div>
<div className="space-y-2">
2025-11-07 21:14:56 +01:00
<h4 className="font-medium">2. Enter the verification code</h4>
<p className="text-sm text-muted-foreground">Enter the 6-digit code that appears in your app</p>
2025-11-07 20:36:46 +01:00
<Input
type="text"
placeholder="000000"
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
2025-11-07 20:55:00 +01:00
className="text-center text-lg tracking-widest font-mono text-base"
2025-11-07 20:36:46 +01:00
maxLength={6}
disabled={loading}
/>
</div>
<div className="flex gap-2">
<Button onClick={handleVerify} className="flex-1 bg-blue-500 hover:bg-blue-600" disabled={loading}>
2025-11-07 21:14:56 +01:00
{loading ? "Verifying..." : "Verify and Enable"}
2025-11-07 20:36:46 +01:00
</Button>
<Button onClick={handleClose} variant="outline" className="flex-1 bg-transparent" disabled={loading}>
2025-11-07 21:14:56 +01:00
Cancel
2025-11-07 20:36:46 +01:00
</Button>
</div>
</div>
)}
{step === 3 && (
<div className="space-y-4">
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-4 flex items-start gap-2">
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" />
<div>
2025-11-07 21:14:56 +01:00
<p className="font-medium text-green-500">2FA Enabled Successfully</p>
2025-11-07 20:36:46 +01:00
<p className="text-sm text-green-500 mt-1">
2025-11-07 21:14:56 +01:00
Your account is now protected with two-factor authentication
2025-11-07 20:36:46 +01:00
</p>
</div>
</div>
<div className="space-y-2">
2025-11-07 21:14:56 +01:00
<h4 className="font-medium text-orange-500">Important: Save your backup codes</h4>
2025-11-07 20:36:46 +01:00
<p className="text-sm text-muted-foreground">
2025-11-07 21:14:56 +01:00
These codes will allow you to access your account if you lose access to your authentication app. Store
them in a safe place.
2025-11-07 20:36:46 +01:00
</p>
<div className="bg-muted/50 rounded-lg p-4 space-y-2">
<div className="flex justify-between items-center mb-2">
2025-11-07 21:14:56 +01:00
<span className="text-sm font-medium">Backup Codes</span>
2025-11-07 20:36:46 +01:00
<Button variant="outline" size="sm" onClick={() => copyToClipboard(backupCodes.join("\n"), "codes")}>
{copiedCodes ? (
<Check className="h-4 w-4 text-green-500 mr-2" />
) : (
<Copy className="h-4 w-4 mr-2" />
)}
2025-11-07 21:14:56 +01:00
Copy All
2025-11-07 20:36:46 +01:00
</Button>
</div>
<div className="grid grid-cols-2 gap-2">
{backupCodes.map((code, index) => (
<div key={index} className="bg-background rounded px-3 py-2 font-mono text-sm text-center">
{code}
</div>
))}
</div>
</div>
</div>
<Button onClick={handleFinish} className="w-full bg-blue-500 hover:bg-blue-600">
2025-11-07 21:14:56 +01:00
Finish
2025-11-07 20:36:46 +01:00
</Button>
</div>
)}
</DialogContent>
</Dialog>
)
}