187 lines
8.7 KiB
TypeScript
187 lines
8.7 KiB
TypeScript
|
|
import React, { useState } from 'react';
|
||
|
|
import { Button } from '../../ui/button';
|
||
|
|
import { Input } from '../../ui/input';
|
||
|
|
import { Smartphone, QrCode, ArrowLeft, Copy, Download, AlertTriangle } from 'lucide-react';
|
||
|
|
import { useToast } from '../../../context/ToastContext';
|
||
|
|
|
||
|
|
interface TwoFactorSetupProps {
|
||
|
|
onBack: () => void;
|
||
|
|
onComplete: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const TwoFactorSetup: React.FC<TwoFactorSetupProps> = ({ onBack, onComplete }) => {
|
||
|
|
const { addToast } = useToast();
|
||
|
|
const [step, setStep] = useState(1);
|
||
|
|
const [method, setMethod] = useState<'totp' | 'sms'>('totp');
|
||
|
|
const [verificationCode, setVerificationCode] = useState('');
|
||
|
|
const [backupCodes] = useState(Array.from({length: 10}, () => Math.random().toString(36).substr(2, 8).toUpperCase()));
|
||
|
|
|
||
|
|
const handleVerify = () => {
|
||
|
|
if (verificationCode.length < 6) {
|
||
|
|
addToast('Invalid code', 'error');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
setStep(3);
|
||
|
|
addToast('2FA Verified Successfully', 'success');
|
||
|
|
};
|
||
|
|
|
||
|
|
const copyCodes = () => {
|
||
|
|
navigator.clipboard.writeText(backupCodes.join('\n'));
|
||
|
|
addToast('Backup codes copied to clipboard');
|
||
|
|
};
|
||
|
|
|
||
|
|
const downloadCodes = () => {
|
||
|
|
const element = document.createElement("a");
|
||
|
|
const file = new Blob([backupCodes.join('\n')], {type: 'text/plain'});
|
||
|
|
element.href = URL.createObjectURL(file);
|
||
|
|
element.download = "veza-backup-codes.txt";
|
||
|
|
document.body.appendChild(element);
|
||
|
|
element.click();
|
||
|
|
addToast('Backup codes downloaded');
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="animate-fadeIn max-w-2xl mx-auto">
|
||
|
|
<div className="mb-6 flex items-center gap-4">
|
||
|
|
<button onClick={onBack} className="p-2 hover:bg-white/5 rounded-full text-gray-400 hover:text-white transition-colors">
|
||
|
|
<ArrowLeft className="w-5 h-5" />
|
||
|
|
</button>
|
||
|
|
<div>
|
||
|
|
<h2 className="text-2xl font-bold text-white">Enable Two-Factor Authentication</h2>
|
||
|
|
<p className="text-gray-400 text-sm">Protect your account with an extra layer of security.</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* STEP 1: CHOOSE METHOD */}
|
||
|
|
{step === 1 && (
|
||
|
|
<div className="grid gap-4">
|
||
|
|
<div
|
||
|
|
onClick={() => { setMethod('totp'); setStep(2); }}
|
||
|
|
className="p-6 border border-kodo-steel rounded-xl bg-kodo-ink hover:bg-white/5 cursor-pointer transition-all hover:border-kodo-cyan group"
|
||
|
|
>
|
||
|
|
<div className="flex items-center gap-4 mb-2">
|
||
|
|
<div className="w-12 h-12 rounded-full bg-kodo-cyan/10 flex items-center justify-center group-hover:bg-kodo-cyan/20">
|
||
|
|
<QrCode className="w-6 h-6 text-kodo-cyan" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<h3 className="text-lg font-bold text-white group-hover:text-kodo-cyan">Authenticator App</h3>
|
||
|
|
<p className="text-sm text-gray-400">Use Google Authenticator, Authy, or 1Password. (Recommended)</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div
|
||
|
|
onClick={() => { setMethod('sms'); setStep(2); }}
|
||
|
|
className="p-6 border border-kodo-steel rounded-xl bg-kodo-ink hover:bg-white/5 cursor-pointer transition-all hover:border-kodo-gold group"
|
||
|
|
>
|
||
|
|
<div className="flex items-center gap-4 mb-2">
|
||
|
|
<div className="w-12 h-12 rounded-full bg-kodo-gold/10 flex items-center justify-center group-hover:bg-kodo-gold/20">
|
||
|
|
<Smartphone className="w-6 h-6 text-kodo-gold" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<h3 className="text-lg font-bold text-white group-hover:text-kodo-gold">SMS / Text Message</h3>
|
||
|
|
<p className="text-sm text-gray-400">Receive a code via text message to your phone.</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* STEP 2: CONFIGURE & VERIFY */}
|
||
|
|
{step === 2 && method === 'totp' && (
|
||
|
|
<div className="space-y-8 bg-kodo-ink p-8 rounded-xl border border-kodo-steel">
|
||
|
|
<div className="text-center">
|
||
|
|
<div className="bg-white p-4 inline-block rounded-xl mb-4">
|
||
|
|
{/* Mock QR */}
|
||
|
|
<div className="w-48 h-48 bg-gray-900 flex items-center justify-center relative overflow-hidden">
|
||
|
|
<QrCode className="w-full h-full text-black opacity-20 absolute" />
|
||
|
|
<span className="relative font-bold text-black text-xs">MOCK QR CODE</span>
|
||
|
|
<div className="absolute inset-0 border-4 border-black/10"></div>
|
||
|
|
{/* Decorative pixel pattern simulated */}
|
||
|
|
<div className="absolute top-2 left-2 w-10 h-10 bg-black"></div>
|
||
|
|
<div className="absolute top-2 right-2 w-10 h-10 bg-black"></div>
|
||
|
|
<div className="absolute bottom-2 left-2 w-10 h-10 bg-black"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<p className="text-sm text-gray-300 mb-2">Scan this QR code with your authenticator app.</p>
|
||
|
|
<p className="text-xs text-gray-500 font-mono bg-black/30 py-1 px-2 rounded inline-block cursor-pointer hover:text-white" onClick={() => { navigator.clipboard.writeText("VEZA-SECRET-KEY-123"); addToast("Key copied"); }}>
|
||
|
|
KEY: VEZA-SECRET-KEY-123
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="border-t border-kodo-steel pt-6">
|
||
|
|
<h4 className="font-bold text-white mb-4">Verify Configuration</h4>
|
||
|
|
<div className="flex gap-3">
|
||
|
|
<Input
|
||
|
|
placeholder="Enter 6-digit code"
|
||
|
|
value={verificationCode}
|
||
|
|
onChange={(e) => setVerificationCode(e.target.value.replace(/\D/g,'').slice(0,6))}
|
||
|
|
className="font-mono text-center tracking-widest text-lg"
|
||
|
|
/>
|
||
|
|
<Button variant="primary" onClick={handleVerify} disabled={verificationCode.length !== 6}>
|
||
|
|
VERIFY
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{step === 2 && method === 'sms' && (
|
||
|
|
<div className="bg-kodo-ink p-8 rounded-xl border border-kodo-steel text-center">
|
||
|
|
<Smartphone className="w-16 h-16 text-kodo-gold mx-auto mb-4" />
|
||
|
|
<h3 className="text-xl font-bold text-white mb-2">SMS Setup</h3>
|
||
|
|
<p className="text-gray-400 mb-6">Enter your phone number to receive a verification code.</p>
|
||
|
|
<div className="flex gap-2 max-w-sm mx-auto">
|
||
|
|
<Input placeholder="+1 (555) 000-0000" />
|
||
|
|
<Button variant="primary" onClick={() => addToast("Code sent to your phone", "info")}>SEND</Button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="mt-8 border-t border-kodo-steel pt-6 text-left">
|
||
|
|
<h4 className="font-bold text-white mb-4">Enter Verification Code</h4>
|
||
|
|
<div className="flex gap-3">
|
||
|
|
<Input
|
||
|
|
placeholder="000000"
|
||
|
|
value={verificationCode}
|
||
|
|
onChange={(e) => setVerificationCode(e.target.value.replace(/\D/g,'').slice(0,6))}
|
||
|
|
className="font-mono text-center tracking-widest text-lg"
|
||
|
|
/>
|
||
|
|
<Button variant="primary" onClick={handleVerify} disabled={verificationCode.length !== 6}>
|
||
|
|
VERIFY
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* STEP 3: BACKUP CODES */}
|
||
|
|
{step === 3 && (
|
||
|
|
<div className="space-y-6 bg-kodo-ink p-8 rounded-xl border border-kodo-steel">
|
||
|
|
<div className="flex items-center gap-4 text-kodo-lime mb-2">
|
||
|
|
<CheckCircle className="w-8 h-8" />
|
||
|
|
<h3 className="text-xl font-bold">2FA Enabled Successfully</h3>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="bg-kodo-orange/10 border border-kodo-orange/30 p-4 rounded-lg flex gap-3">
|
||
|
|
<AlertTriangle className="w-6 h-6 text-kodo-orange flex-shrink-0" />
|
||
|
|
<div>
|
||
|
|
<h4 className="font-bold text-kodo-orange text-sm mb-1">Save these backup codes</h4>
|
||
|
|
<p className="text-xs text-gray-300">If you lose your device, these codes are the only way to access your account. Keep them safe.</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-2 gap-3 bg-black/40 p-4 rounded-lg font-mono text-sm text-gray-300 text-center border border-kodo-steel/50">
|
||
|
|
{backupCodes.map(code => (
|
||
|
|
<div key={code} className="py-1 tracking-wider">{code}</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex gap-3 pt-2">
|
||
|
|
<Button variant="secondary" className="flex-1" icon={<Copy className="w-4 h-4" />} onClick={copyCodes}>Copy All</Button>
|
||
|
|
<Button variant="secondary" className="flex-1" icon={<Download className="w-4 h-4" />} onClick={downloadCodes}>Download</Button>
|
||
|
|
<Button variant="primary" className="flex-1" onClick={onComplete}>Done</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|