81 lines
3.4 KiB
TypeScript
81 lines
3.4 KiB
TypeScript
|
|
import { useState } from "react";
|
||
|
|
import { useTranslation } from "react-i18next";
|
||
|
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||
|
|
import { Lock, ShieldCheck } from "lucide-react";
|
||
|
|
import { api } from "../lib/api";
|
||
|
|
import * as e2ee from "../lib/e2ee";
|
||
|
|
|
||
|
|
// Secure-messaging key gate: first-run setup (choose a passphrase) or unlock on this device.
|
||
|
|
// Self-contained — on success it updates the shared e2ee unlock state, so any observing view
|
||
|
|
// (the Messages page, a dock window) swaps out of the gate automatically.
|
||
|
|
export default function KeyGate({ meId, compact = false }: { meId: number; compact?: boolean }) {
|
||
|
|
const { t } = useTranslation();
|
||
|
|
const qc = useQueryClient();
|
||
|
|
const keyQ = useQuery({ queryKey: ["message-key"], queryFn: api.messageMyKey, staleTime: 60_000 });
|
||
|
|
const configured = keyQ.data?.configured ?? false;
|
||
|
|
|
||
|
|
const [pass, setPass] = useState("");
|
||
|
|
const [confirm, setConfirm] = useState("");
|
||
|
|
const [err, setErr] = useState<string | null>(null);
|
||
|
|
const [busy, setBusy] = useState(false);
|
||
|
|
|
||
|
|
async function submit() {
|
||
|
|
setErr(null);
|
||
|
|
if (pass.length < 6) return setErr(t("messages.passTooShort"));
|
||
|
|
if (!configured && pass !== confirm) return setErr(t("messages.passMismatch"));
|
||
|
|
setBusy(true);
|
||
|
|
try {
|
||
|
|
if (configured) {
|
||
|
|
await e2ee.unlock(meId, pass, keyQ.data!);
|
||
|
|
} else {
|
||
|
|
const payload = await e2ee.setup(meId, pass);
|
||
|
|
await api.setupMessageKey(payload);
|
||
|
|
}
|
||
|
|
qc.invalidateQueries({ queryKey: ["message-key"] });
|
||
|
|
} catch {
|
||
|
|
setErr(configured ? t("messages.wrongPass") : t("messages.setupFailed"));
|
||
|
|
} finally {
|
||
|
|
setBusy(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (keyQ.isLoading) return <div className="p-6 text-center text-muted text-sm">{t("common.loading")}</div>;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className={`glass rounded-2xl space-y-3 ${compact ? "p-3" : "p-4"}`}>
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<ShieldCheck className="w-5 h-5 text-accent shrink-0" />
|
||
|
|
<div className="font-semibold text-sm">{configured ? t("messages.unlockTitle") : t("messages.setupTitle")}</div>
|
||
|
|
</div>
|
||
|
|
{!compact && <p className="text-sm text-muted">{configured ? t("messages.unlockBody") : t("messages.setupBody")}</p>}
|
||
|
|
<input
|
||
|
|
type="password"
|
||
|
|
value={pass}
|
||
|
|
onChange={(e) => setPass(e.target.value)}
|
||
|
|
onKeyDown={(e) => e.key === "Enter" && configured && submit()}
|
||
|
|
placeholder={t("messages.passphrase")}
|
||
|
|
className="w-full bg-card border border-border rounded-xl px-3 py-2 text-sm outline-none focus:border-accent"
|
||
|
|
/>
|
||
|
|
{!configured && (
|
||
|
|
<input
|
||
|
|
type="password"
|
||
|
|
value={confirm}
|
||
|
|
onChange={(e) => setConfirm(e.target.value)}
|
||
|
|
placeholder={t("messages.passphraseConfirm")}
|
||
|
|
className="w-full bg-card border border-border rounded-xl px-3 py-2 text-sm outline-none focus:border-accent"
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
{err && <div className="text-sm text-red-400">{err}</div>}
|
||
|
|
<button
|
||
|
|
onClick={submit}
|
||
|
|
disabled={busy || !pass}
|
||
|
|
className="inline-flex items-center gap-2 px-4 py-2 rounded-xl font-semibold bg-accent text-accent-fg hover:opacity-90 transition disabled:opacity-40"
|
||
|
|
>
|
||
|
|
<Lock className="w-4 h-4" />
|
||
|
|
{configured ? t("messages.unlock") : t("messages.setUp")}
|
||
|
|
</button>
|
||
|
|
{!configured && !compact && <p className="text-xs text-muted">{t("messages.setupWarn")}</p>}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|