Advertisement

← vibesdk

src/components/vault/VaultUnlockModal.tsx

6.4 KB Download file
import { useEffect, useState } from 'react';
import { useVault } from '@/hooks/use-vault';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
	AlertDialog,
	AlertDialogAction,
	AlertDialogCancel,
	AlertDialogContent,
	AlertDialogDescription,
	AlertDialogFooter,
	AlertDialogHeader,
	AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import {
	Dialog,
	DialogContent,
	DialogDescription,
	DialogHeader,
	DialogTitle,
} from '@/components/ui/dialog';
import { Fingerprint, KeyRound, AlertCircle, Loader2 } from 'lucide-react';

interface Props {
	open: boolean;
	onOpenChange: (open: boolean) => void;
	reason?: string;
}

type UnlockMethod = 'passkey' | 'password' | 'recovery';

type UnlockResult = { success: boolean; error?: string };

export function VaultUnlockModal({ open, onOpenChange, reason }: Props) {
	const { unlockWithPassword, unlockWithPasskey, unlockWithRecoveryCode, resetVault, state } = useVault();
	const hasPasskey = state.unlockMethod === 'webauthn-prf';

	const [method, setMethod] = useState<UnlockMethod>(hasPasskey ? 'passkey' : 'password');
	const [password, setPassword] = useState('');
	const [recoveryCode, setRecoveryCode] = useState('');
	const [error, setError] = useState<string | null>(null);
	const [isUnlocking, setIsUnlocking] = useState(false);
	const [showResetConfirm, setShowResetConfirm] = useState(false);

	useEffect(() => {
		if (!open) return;
		setError(null);
		setPassword('');
		setRecoveryCode('');
		setMethod(hasPasskey ? 'passkey' : 'password');
	}, [hasPasskey, open]);

	const handleUnlock = async () => {
		setError(null);
		setIsUnlocking(true);

		try {
			let result: UnlockResult = { success: false, error: 'Unlock failed' };

			switch (method) {
				case 'passkey':
					result = await unlockWithPasskey();
					break;
				case 'password':
					if (!password) {
						setError('Please enter your password');
						setIsUnlocking(false);
						return;
					}
					result = await unlockWithPassword(password);
					break;
				case 'recovery':
					if (!recoveryCode) {
						setError('Please enter a recovery code');
						setIsUnlocking(false);
						return;
					}
					result = await unlockWithRecoveryCode(recoveryCode);
					break;
			}

			if (result.success) {
				onOpenChange(false);
				setPassword('');
				setRecoveryCode('');
			} else {
				setError(result.error ?? 'Unlock failed');
			}
		} catch (err) {
			setError(err instanceof Error ? err.message : 'Unlock failed');
		} finally {
			setIsUnlocking(false);
		}
	};

	return (
		<>
			<Dialog open={open} onOpenChange={onOpenChange}>
				<DialogContent className="sm:max-w-md">
					<DialogHeader>
						<DialogTitle>Unlock Vault</DialogTitle>
						<DialogDescription>
							{reason ?? 'Your secrets are encrypted. Unlock your vault to access them.'}
						</DialogDescription>
					</DialogHeader>

					<div className="space-y-4 py-4">
						<div className="flex gap-2">
							{hasPasskey && (
								<Button
									variant={method === 'passkey' ? 'default' : 'outline'}
									onClick={() => setMethod('passkey')}
									className="flex-1"
								>
									<Fingerprint className="h-4 w-4 mr-2" />
									Passkey
								</Button>
							)}
							<Button
								variant={method === 'password' ? 'default' : 'outline'}
								onClick={() => setMethod('password')}
								className="flex-1"
							>
								<KeyRound className="h-4 w-4 mr-2" />
								Password
							</Button>
						</div>

						{method === 'passkey' && (
							<div className="text-center py-4">
								<Fingerprint className="h-12 w-12 mx-auto text-primary mb-3" />
								<p className="text-sm text-muted-foreground">
									Click unlock to authenticate with your passkey
								</p>
							</div>
						)}

						{method === 'password' && (
							<div className="space-y-2">
								<Label htmlFor="unlock-password">Vault Password</Label>
								<Input
									id="unlock-password"
									type="password"
									value={password}
									onChange={(e) => setPassword(e.target.value)}
									onKeyDown={(e) => e.key === 'Enter' && handleUnlock()}
									placeholder="Enter your vault password"
									autoFocus
								/>
							</div>
						)}

						{method === 'recovery' && (
							<div className="space-y-2">
								<Label htmlFor="recovery-code">Recovery Code</Label>
								<Input
									id="recovery-code"
									value={recoveryCode}
									onChange={(e) => setRecoveryCode(e.target.value.toUpperCase())}
									onKeyDown={(e) => e.key === 'Enter' && handleUnlock()}
									placeholder="XXXXX-XXXXX"
									className="font-mono"
									autoFocus
								/>
							</div>
						)}

						{error && (
							<div className="flex items-center gap-2 text-sm text-destructive">
								<AlertCircle className="h-4 w-4" />
								{error}
							</div>
						)}

						{error?.includes('Vault configuration invalid') && (
							<Button variant="destructive" onClick={() => setShowResetConfirm(true)} className="w-full">
								Reset Vault
							</Button>
						)}

						<Button onClick={handleUnlock} disabled={isUnlocking} className="w-full">
							{isUnlocking ? (
								<>
									<Loader2 className="h-4 w-4 mr-2 animate-spin" />
									Unlocking...
								</>
							) : (
								'Unlock'
							)}
						</Button>

						{method !== 'recovery' && (
							<button
								onClick={() => setMethod('recovery')}
								className="w-full text-sm text-muted-foreground hover:text-foreground transition-colors"
							>
								Use recovery code instead
							</button>
						)}
					</div>
				</DialogContent>
			</Dialog>

			<AlertDialog open={showResetConfirm} onOpenChange={setShowResetConfirm}>
				<AlertDialogContent>
					<AlertDialogHeader>
						<AlertDialogTitle>Reset vault?</AlertDialogTitle>
						<AlertDialogDescription>
							This deletes all vault secrets and configuration for this account.
						</AlertDialogDescription>
					</AlertDialogHeader>
					<AlertDialogFooter>
						<AlertDialogCancel>Cancel</AlertDialogCancel>
						<AlertDialogAction
							onClick={async () => {
								setShowResetConfirm(false);
								const ok = await resetVault();
								if (ok) onOpenChange(false);
							}}
						>
							Reset
						</AlertDialogAction>
					</AlertDialogFooter>
				</AlertDialogContent>
			</AlertDialog>
		</>
	);
}