import { useEffect, useState } from 'react'; interface Props { rawKey: string; team: string; reason: 'created' | 'rotated'; onClose: () => void; } /** * One-time raw bearer reveal. The DB never stores the raw value — once * this dialog closes the operator can never see it again, so we: * - require an explicit "I've saved it" acknowledgement before close * - show a copy-to-clipboard button as the obvious primary action * - warn loudly in red * - trap ESC, browser back, and tab-close (beforeunload) until * acknowledged so a stray keypress can't lose the key (F10) * * The dialog is intentionally modal (overlay + focus trap via tabindex). */ export function GatewayKeyRawKeyDialog({ rawKey, team, reason, onClose }: Props) { const [copied, setCopied] = useState(false); const [acknowledged, setAcknowledged] = useState(false); // F10: while the raw key is on-screen and not acknowledged, block the // common dismissal paths that would otherwise silently lose it: // - ESC keypress (Escape closes most modals by convention) // - browser back / forward (popstate) // - tab close / refresh (beforeunload — best-effort browser warning) // We intentionally do NOT block the dialog's own Close button (gated // by the `acknowledged` checkbox) or the overlay click (which the // current design already ignores). useEffect(() => { if (acknowledged) return; const onKeydown = (e: KeyboardEvent): void => { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); } }; document.addEventListener('keydown', onKeydown, { capture: true }); const onBeforeUnload = (e: BeforeUnloadEvent): string => { e.preventDefault(); const msg = 'Gateway API key has not been saved. Closing this page will lose it forever.'; // Modern browsers ignore the returned string but require it set // for the warning dialog to appear. Setting both for cross-browser // safety (Chrome reads returnValue, some older Firefox reads return). (e as BeforeUnloadEvent & { returnValue: string }).returnValue = msg; return msg; }; window.addEventListener('beforeunload', onBeforeUnload); // Push a sentinel history entry so the next back-button press lands // here (where we re-push it). Best-effort: doesn't fully prevent // navigation in every browser, but turns a single back-tap into a // visible alert + re-block. let pushed = false; try { window.history.pushState({ aaoGatewayKeyTrap: true }, '', window.location.href); pushed = true; } catch { /* SSR / sandboxed iframes: skip */ } const onPopState = (e: PopStateEvent): void => { e.preventDefault?.(); try { window.history.pushState({ aaoGatewayKeyTrap: true }, '', window.location.href); } catch { /* ignore */ } alert( 'API key has not been saved. Copy it and tick "保存しました" before navigating away.', ); }; window.addEventListener('popstate', onPopState); return () => { document.removeEventListener('keydown', onKeydown, { capture: true }); window.removeEventListener('beforeunload', onBeforeUnload); window.removeEventListener('popstate', onPopState); // Drop the sentinel we pushed so the user's history isn't littered. if (pushed) { try { if (window.history.state && (window.history.state as { aaoGatewayKeyTrap?: boolean }).aaoGatewayKeyTrap) { window.history.back(); } } catch { /* ignore */ } } }; }, [acknowledged]); async function handleCopy() { try { await navigator.clipboard.writeText(rawKey); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { // Some browsers / contexts block clipboard access. The textarea // is selectable as a fallback. } } return (
team: {team}
⚠️ このキーは今後二度と表示されません
必ずパスワードマネージャや LLM クライアントの設定にコピー・保存してから閉じてください。 紛失した場合は Rotate で再発行する必要があります。