/* passwords.jsx — раздел «Пароли»: список, показать/скрыть, копировать,
* генератор случайных паролей, индикатор надёжности. */
/* ----------------------------------------------------------- копирование с тостом */
function useCopied() {
const [copied, setCopied] = React.useState(null);
const copy = (text, key) => {
copyToClipboard(text).then(() => {
setCopied(key);
setTimeout(() => setCopied((c) => (c === key ? null : c)), 1400);
});
};
return [copied, copy];
}
/* ----------------------------------------------------------- индикатор силы */
function StrengthBar({ value }) {
const s = passwordStrength(value);
return (
{[0, 1, 2, 3].map((i) => (
))}
{s.label}
);
}
/* ----------------------------------------------------------- генератор (панель) */
function GeneratorPanel({ onUse }) {
const [length, setLength] = React.useState(16);
const [upper, setUpper] = React.useState(true);
const [digits, setDigits] = React.useState(true);
const [symbols, setSymbols] = React.useState(true);
const [value, setValue] = React.useState(() => generatePassword({ length: 16 }));
const regen = React.useCallback(() => {
setValue(generatePassword({ length, upper, digits, symbols }));
}, [length, upper, digits, symbols]);
React.useEffect(() => { regen(); }, [length, upper, digits, symbols]);
const [copied, copy] = useCopied();
const Toggle = ({ on, set, label }) => (
set(!on)} className="flex items-center gap-2 select-none">
{label}
);
return (
{value}
copy(value, "gen")} className="grid place-items-center w-8 h-8 rounded-lg text-[var(--muted)] hover:text-[var(--accent)] hover:bg-[var(--surface-2)] transition" title="Копировать">
{onUse && (
onUse(value)}>
Использовать этот пароль
)}
);
}
/* ----------------------------------------------------------- карточка пароля */
function PasswordCard({ item, onEdit, onToggleFav, flags = [], index = 0 }) {
const settings = Store.useSettings();
const [show, setShow] = React.useState(false);
const [copied, copy] = useCopied();
const initials = (item.title || "?").trim().slice(0, 1).toUpperCase();
// автоскрытие открытого пароля
React.useEffect(() => {
if (!show) return;
const secs = settings.autoHidePw || 0;
if (!secs) return;
const t = setTimeout(() => setShow(false), secs * 1000);
return () => clearTimeout(t);
}, [show, settings.autoHidePw]);
const flagLabel = { weak: "слабый", reused: "повтор", old: "давно не менялся" };
return (
{initials}
{item.title}
{item.url && (
)}
{item.login &&
{item.login}
}
{flags.length > 0 && (
{flags.map((fl) => (
{flagLabel[fl]}
))}
)}
onToggleFav(item)} className={`grid place-items-center w-8 h-8 rounded-lg transition shrink-0 ${item.favorite ? "str-text-amber" : "text-[var(--muted)] hover:text-[var(--text)]"}`} title="В избранное">
{show ? item.password : "••••••••••••"}
setShow((s) => !s)} className="grid place-items-center w-8 h-8 rounded-lg text-[var(--muted)] hover:text-[var(--accent)] transition" title={show ? "Скрыть" : "Показать"}>
copy(item.password, item.id)} className="grid place-items-center w-8 h-8 rounded-lg text-[var(--muted)] hover:text-[var(--accent)] transition" title="Копировать пароль">
{item.login && (
copy(item.login, item.id + "-l")} className="text-[12px] font-semibold text-[var(--muted)] hover:text-[var(--accent)] transition inline-flex items-center gap-1">
логин
)}
{item.note && · {item.note} }
onEdit(item)} className="ml-auto text-[12px] font-semibold text-[var(--muted)] hover:text-[var(--text)] transition inline-flex items-center gap-1">
изменить
);
}
/* ----------------------------------------------------------- модалка пароля */
function PasswordModal({ item, open, onClose, onSaved, onDeleted }) {
const empty = { title: "", login: "", password: "", url: "", note: "", favorite: false };
const [form, setForm] = React.useState(empty);
const [show, setShow] = React.useState(false);
const [gen, setGen] = React.useState(false);
const [busy, setBusy] = React.useState(false);
const [confirmDel, setConfirmDel] = React.useState(false);
const isEdit = item && item.id;
React.useEffect(() => {
if (open) {
setForm(item ? { ...empty, ...item } : empty);
setShow(false); setGen(!item); setConfirmDel(false);
}
}, [open, item]);
const set = (k, v) => setForm((f) => ({ ...f, [k]: v }));
const save = async () => {
if (!form.title.trim()) return;
setBusy(true);
if (isEdit) await api.updatePassword(item.id, form);
else await api.createPassword(form);
setBusy(false);
onSaved && onSaved();
onClose();
};
const del = async () => {
setBusy(true);
await api.deletePassword(item.id);
setBusy(false);
onDeleted && onDeleted();
onClose();
};
return (
{isEdit ? "Изменить пароль" : "Новый пароль"}
{isEdit && (!confirmDel ? (
setConfirmDel(true)} disabled={busy}> Удалить
) : (
Да, удалить
setConfirmDel(false)}>Отмена
))}
Отмена
{busy ? : } Сохранить
);
}
/* ----------------------------------------------------------- замок (PIN, zero-knowledge) */
const PIN_LEN = 4;
function VaultGate({ onUnlocked }) {
const [phase, setPhase] = React.useState("checking"); // checking | unset | locked
const [stage, setStage] = React.useState("enter"); // для unset: enter | confirm
const [pin, setPin] = React.useState("");
const [firstPin, setFirstPin] = React.useState("");
const [busy, setBusy] = React.useState(false);
const [err, setErr] = React.useState("");
React.useEffect(() => {
window.vault.status().then(setPhase).catch(() => setPhase("locked"));
}, []);
const finish = async (code) => {
setBusy(true); setErr("");
try {
if (phase === "unset") await window.vault.setup(code);
else await window.vault.unlock(code);
onUnlocked();
} catch (e) {
setErr(phase === "unset" ? "Не удалось сохранить PIN." : "Неверный PIN");
setPin(""); setBusy(false);
}
};
const press = (d) => {
if (busy) return;
setErr("");
setPin((prev) => {
if (prev.length >= PIN_LEN) return prev;
const v = prev + d;
if (v.length === PIN_LEN) {
if (phase === "unset") {
if (stage === "enter") {
setFirstPin(v); setStage("confirm");
setTimeout(() => setPin(""), 150);
} else if (v === firstPin) {
finish(v);
} else {
setErr("PIN не совпал, начни заново"); setStage("enter"); setFirstPin("");
setTimeout(() => setPin(""), 350);
}
} else {
finish(v);
}
}
return v;
});
};
const doReset = async () => {
if (!window.confirm("Сбросить PIN? Все сохранённые пароли будут удалены безвозвратно.")) return;
setBusy(true);
try {
await api.resetKeyInfo();
window.vault.lock();
setPhase("unset"); setStage("enter"); setFirstPin(""); setPin(""); setErr("");
} catch (e) { setErr("Не удалось сбросить."); }
finally { setBusy(false); }
};
if (phase === "checking") {
return
;
}
const title = phase === "locked" ? "Введи PIN" : stage === "confirm" ? "Повтори PIN" : "Придумай PIN";
const sub = phase === "locked"
? "Короткий код для доступа к паролям на этом устройстве."
: "4 цифры. Ими шифруются пароли прямо в браузере — сервер видит только шифр. Восстановить нельзя.";
return (
{title}
{sub}
{Array.from({ length: PIN_LEN }).map((_, i) => (
))}
{err &&
{err}
}
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map((n) => (
press(String(n))} disabled={busy}
className="h-14 rounded-xl bg-[var(--surface)] border border-[var(--border)] text-[20px] font-bold text-[var(--text)] hover:border-[var(--border-strong)] active:scale-95 transition mono">{n}
))}
press("0")} disabled={busy}
className="h-14 rounded-xl bg-[var(--surface)] border border-[var(--border)] text-[20px] font-bold text-[var(--text)] hover:border-[var(--border-strong)] active:scale-95 transition mono">0
setPin((p) => p.slice(0, -1))} disabled={busy}
className="h-14 rounded-xl text-[var(--muted)] grid place-items-center hover:bg-[var(--surface-2)] transition" aria-label="Стереть">
{phase === "locked" && (
Забыл PIN — сбросить (удалит пароли)
)}
);
}
/* ----------------------------------------------------------- баннер здоровья */
function VaultHealth({ health, active, onToggle }) {
if (!health) return null;
const issues = new Set([...health.weak, ...health.reused, ...health.old]).size;
const tone = health.score >= 80 ? "green" : health.score >= 50 ? "amber" : "red";
return (
{health.score}
Здоровье хранилища
{issues === 0 ? "Все пароли в порядке 🎉" : `${issues} ${pluralRu(issues, "пароль требует", "пароля требуют", "паролей требуют")} внимания`}
{active ? "скрыть" : "показать"}
);
}
/* ----------------------------------------------------------- экран «Пароли» */
function PasswordsScreen({ createSignal }) {
const [unlocked, setUnlocked] = React.useState(() => (window.vault ? window.vault.isUnlocked() : true));
const [items, setItems] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const [q, setQ] = React.useState("");
const [editing, setEditing] = React.useState(null);
const [modalOpen, setModalOpen] = React.useState(false);
const [health, setHealth] = React.useState(null);
const [showHealth, setShowHealth] = React.useState(false);
const [blurred, setBlurred] = React.useState(false);
// блюр при сворачивании приложения (безопасность)
React.useEffect(() => {
const onVis = () => setBlurred(document.hidden);
document.addEventListener("visibilitychange", onVis);
return () => document.removeEventListener("visibilitychange", onVis);
}, []);
const load = React.useCallback(async () => {
setLoading(true);
const data = await api.getPasswords({ q: q || undefined });
setItems(data);
setLoading(false);
api.getVaultHealth().then(setHealth);
}, [q]);
React.useEffect(() => { if (unlocked) { const t = setTimeout(load, 200); return () => clearTimeout(t); } }, [q, unlocked]);
const firstSignal = React.useRef(createSignal);
React.useEffect(() => {
if (createSignal !== firstSignal.current) { setEditing(null); setModalOpen(true); }
}, [createSignal]);
const openNew = () => { setEditing(null); setModalOpen(true); };
const openEdit = (item) => { setEditing(item); setModalOpen(true); };
const toggleFav = async (item) => { await api.updatePassword(item.id, { favorite: !item.favorite }); load(); };
const flagsFor = (id) => {
if (!health || !showHealth) return [];
const f = [];
if (health.weak.includes(id)) f.push("weak");
if (health.reused.includes(id)) f.push("reused");
if (health.old.includes(id)) f.push("old");
return f;
};
if (!unlocked) {
return (
setUnlocked(true)} />
);
}
return (
);
}
Object.assign(window, { PasswordsScreen, PasswordModal, PasswordCard, GeneratorPanel, StrengthBar });