Password Manager experiment built on ATProto Spaces
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334import React, { useState, useEffect, useCallback } from 'react';import { X, RefreshCw, Copy, Check, CheckCheck, Wand2, Sliders} from 'lucide-react';import { useVault } from '../context/VaultContext.js';import { generatePassword, generatePassphrase } from '@2password/core';
export const PasswordGeneratorModal: React.FC = () => { const { isPasswordModalOpen, closePasswordModal, onPasswordGeneratedCallback } = useVault();
const [mode, setMode] = useState<'password' | 'passphrase'>('password'); const [length, setLength] = useState(20); const [uppercase, setUppercase] = useState(true); const [lowercase, setLowercase] = useState(true); const [numbers, setNumbers] = useState(true); const [special, setSpecial] = useState(true); const [avoidAmbiguous, setAvoidAmbiguous] = useState(true);
const [wordCount, setWordCount] = useState(4); const [separator, setSeparator] = useState('-');
const [generatedValue, setGeneratedValue] = useState(''); const [copied, setCopied] = useState(false);
const regenerate = useCallback(() => { if (mode === 'password') { const pwd = generatePassword({ length, uppercase, lowercase, numbers, special, avoidAmbiguous }); setGeneratedValue(pwd); } else { const phrase = generatePassphrase(wordCount, separator); setGeneratedValue(phrase); } setCopied(false); }, [mode, length, uppercase, lowercase, numbers, special, avoidAmbiguous, wordCount, separator]);
useEffect(() => { if (isPasswordModalOpen) { regenerate(); } }, [isPasswordModalOpen, regenerate]);
const handleCopy = async () => { if (!generatedValue) return; try { await navigator.clipboard.writeText(generatedValue); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { // ignore } };
const handleUsePassword = () => { if (onPasswordGeneratedCallback && generatedValue) { onPasswordGeneratedCallback(generatedValue); } closePasswordModal(); };
const getStrength = () => { if (mode === 'passphrase') { if (wordCount <= 3) return { score: 2, label: 'Fair', color: 'bg-amber-400', textColor: 'text-amber-400' }; if (wordCount === 4) return { score: 3, label: 'Good', color: 'bg-cyan-400', textColor: 'text-cyan-400' }; return { score: 4, label: 'Very Strong', color: 'bg-emerald-400', textColor: 'text-emerald-400' }; }
let score = 0; const poolCount = [uppercase, lowercase, numbers, special].filter(Boolean).length;
if (length >= 8) score++; if (length >= 14 && poolCount >= 2) score++; if (length >= 18 && poolCount >= 3) score++; if (length >= 24 && poolCount === 4) score++;
if (score <= 1) return { score: 1, label: 'Weak', color: 'bg-rose-500', textColor: 'text-rose-400' }; if (score === 2) return { score: 2, label: 'Fair', color: 'bg-amber-400', textColor: 'text-amber-400' }; if (score === 3) return { score: 3, label: 'Strong', color: 'bg-cyan-400', textColor: 'text-cyan-400' }; return { score: 4, label: 'Very Strong', color: 'bg-emerald-400', textColor: 'text-emerald-400' }; };
if (!isPasswordModalOpen) return null;
const strength = getStrength();
return ( <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/75 backdrop-blur-sm animate-in fade-in duration-150"> <div className="w-full max-w-lg rounded-2xl bg-slate-900 border border-slate-700/80 shadow-2xl overflow-hidden animate-in zoom-in-95 duration-150"> <div className="px-6 py-4 border-b border-slate-800 flex items-center justify-between"> <div className="flex items-center gap-2.5"> <div className="p-2 rounded-xl bg-purple-500/10 text-purple-400 border border-purple-500/20"> <Wand2 className="w-4 h-4" /> </div> <div> <h3 className="text-sm font-bold text-white">Generate Password</h3> <p className="text-[11px] text-slate-400">Cryptographically secure CSPRNG</p> </div> </div> <button type="button" onClick={closePasswordModal} className="p-1 rounded-lg text-slate-400 hover:text-slate-200 hover:bg-slate-800 cursor-pointer" > <X className="w-4 h-4" /> </button> </div>
<div className="p-6 space-y-5"> <div className="flex rounded-xl bg-slate-950 p-1 border border-slate-800"> <button type="button" onClick={() => setMode('password')} className={`flex-1 py-1.5 text-xs font-semibold rounded-lg transition-all cursor-pointer ${ mode === 'password' ? 'bg-slate-800 text-cyan-300 shadow-xs' : 'text-slate-400 hover:text-slate-200' }`} > Random Password </button> <button type="button" onClick={() => setMode('passphrase')} className={`flex-1 py-1.5 text-xs font-semibold rounded-lg transition-all cursor-pointer ${ mode === 'passphrase' ? 'bg-slate-800 text-cyan-300 shadow-xs' : 'text-slate-400 hover:text-slate-200' }`} > Diceware Passphrase </button> </div>
<div className="p-4 rounded-xl bg-slate-950 border border-slate-800/80 space-y-3"> <div className="flex items-center justify-between gap-2"> <div className="font-mono text-base tracking-wide text-white select-all break-all flex-1"> {generatedValue} </div> <div className="flex items-center gap-1 shrink-0"> <button type="button" onClick={regenerate} title="Generate new password" className="p-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white cursor-pointer transition-colors" > <RefreshCw className="w-4 h-4" /> </button> <button type="button" onClick={handleCopy} title="Copy to clipboard" className="p-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white cursor-pointer transition-colors" > {copied ? <Check className="w-4 h-4 text-emerald-400" /> : <Copy className="w-4 h-4" />} </button> </div> </div>
<div className="space-y-1 pt-1"> <div className="flex justify-between text-[11px]"> <span className="text-slate-400">Strength</span> <span className={`font-semibold ${strength.textColor}`}>{strength.label}</span> </div> <div className="grid grid-cols-4 gap-1.5 h-1.5"> {[1, 2, 3, 4].map((step) => ( <div key={step} className={`rounded-full transition-all ${ step <= strength.score ? strength.color : 'bg-slate-800' }`} /> ))} </div> </div> </div>
{mode === 'password' ? ( <div className="space-y-4 text-xs"> <div className="space-y-1.5"> <div className="flex justify-between text-slate-300 font-medium"> <span>Password Length</span> <span className="font-mono text-cyan-400 font-bold">{length} characters</span> </div> <input type="range" min="8" max="64" value={length} onChange={(e) => setLength(Number(e.target.value))} className="w-full accent-cyan-500 cursor-pointer" /> </div>
<div className="grid grid-cols-2 gap-2.5 pt-1"> <label className="flex items-center gap-2 p-2 rounded-lg bg-slate-950/60 border border-slate-800 cursor-pointer hover:bg-slate-950"> <input type="checkbox" checked={uppercase} onChange={(e) => setUppercase(e.target.checked)} className="rounded border-slate-700 text-cyan-500 focus:ring-0" /> <span className="text-slate-300">Uppercase (A-Z)</span> </label>
<label className="flex items-center gap-2 p-2 rounded-lg bg-slate-950/60 border border-slate-800 cursor-pointer hover:bg-slate-950"> <input type="checkbox" checked={lowercase} onChange={(e) => setLowercase(e.target.checked)} className="rounded border-slate-700 text-cyan-500 focus:ring-0" /> <span className="text-slate-300">Lowercase (a-z)</span> </label>
<label className="flex items-center gap-2 p-2 rounded-lg bg-slate-950/60 border border-slate-800 cursor-pointer hover:bg-slate-950"> <input type="checkbox" checked={numbers} onChange={(e) => setNumbers(e.target.checked)} className="rounded border-slate-700 text-cyan-500 focus:ring-0" /> <span className="text-slate-300">Numbers (0-9)</span> </label>
<label className="flex items-center gap-2 p-2 rounded-lg bg-slate-950/60 border border-slate-800 cursor-pointer hover:bg-slate-950"> <input type="checkbox" checked={special} onChange={(e) => setSpecial(e.target.checked)} className="rounded border-slate-700 text-cyan-500 focus:ring-0" /> <span className="text-slate-300">Special (!@#$%^&*)</span> </label>
<label className="col-span-2 flex items-center gap-2 p-2 rounded-lg bg-slate-950/60 border border-slate-800 cursor-pointer hover:bg-slate-950"> <input type="checkbox" checked={avoidAmbiguous} onChange={(e) => setAvoidAmbiguous(e.target.checked)} className="rounded border-slate-700 text-cyan-500 focus:ring-0" /> <span className="text-slate-300">Avoid Ambiguous Characters (l, 1, I, O, 0)</span> </label> </div> </div> ) : ( <div className="space-y-4 text-xs"> <div className="space-y-1.5"> <div className="flex justify-between text-slate-300 font-medium"> <span>Number of Words</span> <span className="font-mono text-cyan-400 font-bold">{wordCount} words</span> </div> <input type="range" min="3" max="10" value={wordCount} onChange={(e) => setWordCount(Number(e.target.value))} className="w-full accent-cyan-500 cursor-pointer" /> </div>
<div className="space-y-1"> <label className="text-slate-300 font-medium">Word Separator</label> <div className="flex gap-2"> {['-', '_', '.', ' ', '#'].map((sep) => ( <button key={sep} type="button" onClick={() => setSeparator(sep)} className={`w-10 py-1.5 rounded-lg border text-xs font-mono cursor-pointer transition-colors ${ separator === sep ? 'border-cyan-500 bg-cyan-500/10 text-cyan-300 font-bold' : 'border-slate-800 bg-slate-950 text-slate-400 hover:text-slate-200' }`} > {sep === ' ' ? 'space' : sep} </button> ))} </div> </div> </div> )} </div>
<div className="px-6 py-4 bg-slate-950/60 border-t border-slate-800 flex items-center justify-end gap-2.5"> <button type="button" onClick={closePasswordModal} className="px-4 py-2 rounded-xl text-xs font-semibold text-slate-400 hover:text-white hover:bg-slate-800 cursor-pointer transition-colors" > Close </button> {onPasswordGeneratedCallback ? ( <button type="button" onClick={handleUsePassword} className="px-4 py-2 rounded-xl bg-cyan-500 hover:bg-cyan-400 text-slate-950 font-bold text-xs cursor-pointer shadow-md shadow-cyan-500/20 transition-all flex items-center gap-1.5" > <CheckCheck className="w-4 h-4 stroke-[2.5]" /> <span>Use This Password</span> </button> ) : ( <button type="button" onClick={handleCopy} className="px-4 py-2 rounded-xl bg-cyan-500 hover:bg-cyan-400 text-slate-950 font-bold text-xs cursor-pointer shadow-md shadow-cyan-500/20 transition-all flex items-center gap-1.5" > {copied ? <Check className="w-4 h-4 stroke-[3]" /> : <Copy className="w-4 h-4 stroke-[2.5]" />} <span>{copied ? 'Copied!' : 'Copy Password'}</span> </button> )} </div> </div> </div> );};