veza/apps/web/src/components/settings/security/TwoFactorSetup.tsx

282 lines
9.9 KiB
TypeScript
Raw Normal View History

import React, { useState } from 'react';
import { Button } from '../../ui/button';
import { Input } from '../../ui/input';
import {
Smartphone,
QrCode,
ArrowLeft,
Copy,
Download,
AlertTriangle,
CheckCircle,
} 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');
2026-01-07 18:39:21 +00:00
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-kodo-content-dim 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-kodo-content-dim 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">
2026-01-07 18:39:21 +00:00
<div
onClick={() => {
setMethod('totp');
setStep(2);
}}
aesthetic-improvements: replace secondary cyan hover states with steel - Button outline variant: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 - Header secondary nav: hover:text-kodo-cyan → hover:text-white, hover:bg-kodo-cyan/5 → hover:bg-white/5 - FileManagerView: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 (kept selected state cyan) - ProjectsManager: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50, hover:text-kodo-cyan → hover:text-white - GroupDetailView: hover:border-kodo-cyan/30 → hover:border-kodo-steel/50 - AIToolsView: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 - CloudFileBrowser: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 (kept selected state cyan) - ProfileView: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 - CourseCard: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 - TwoFactorSetup: hover:border-kodo-cyan → hover:border-kodo-steel/50 - GearView: hover:text-kodo-cyan → hover:text-white, hover:border-kodo-cyan → hover:border-kodo-steel/50 - ChatInput: hover:text-kodo-cyan → hover:text-white (3 instances) - ChatMessage: hover:text-kodo-cyan → hover:text-white (2 instances) - ChatRoom: hover:text-kodo-cyan → hover:text-white - AddToPlaylistModal: hover:border-kodo-cyan → hover:border-kodo-steel/50, hover:text-kodo-cyan → hover:text-white - Preserved focus rings (cyan) and active/selected states (cyan) as per audit - Action 11.3.1.2 in progress (first batch of ~15 files)
2026-01-16 09:51:30 +00:00
className="p-6 border border-kodo-steel rounded-xl bg-kodo-ink hover:bg-white/5 cursor-pointer transition-all hover:border-kodo-steel/50 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-kodo-content-dim">
Use Google Authenticator, Authy, or 1Password. (Recommended)
</p>
</div>
</div>
</div>
2026-01-07 18:39:21 +00:00
<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-kodo-content-dim">
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-kodo-ink flex items-center justify-center relative overflow-hidden">
2026-01-07 18:39:21 +00:00
<QrCode className="w-full h-full text-black opacity-20 absolute" />
<span className="relative font-bold text-black text-xs">
MOCK QR CODE
</span>
2026-01-07 18:39:21 +00:00
<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-kodo-text-main mb-2">
Scan this QR code with your authenticator app.
</p>
<p
className="text-xs text-kodo-content-dim 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">
2026-01-07 18:39:21 +00:00
<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">
2026-01-07 18:39:21 +00:00
<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-kodo-content-dim mb-6">
Enter your phone number to receive a verification code.
</p>
2026-01-07 18:39:21 +00:00
<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>
2026-01-07 18:39:21 +00:00
</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>
2026-01-07 18:39:21 +00:00
<div className="flex gap-3">
<Input
placeholder="000000"
value={verificationCode}
onChange={(e) =>
setVerificationCode(
e.target.value.replace(/\D/g, '').slice(0, 6),
)
}
2026-01-07 18:39:21 +00:00
className="font-mono text-center tracking-widest text-lg"
/>
<Button
variant="primary"
onClick={handleVerify}
disabled={verificationCode.length !== 6}
>
2026-01-07 18:39:21 +00:00
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>
2026-01-07 18:39:21 +00:00
<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-kodo-text-main">
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-kodo-text-main 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>
);
};