import { FC, useEffect, useState } from 'react'; import { FaCheck, FaCopy, FaKey, FaTimes } from 'react-icons/fa'; import { LocalizeText } from '../../api'; import { useHousekeepingStore } from '../../hooks'; const COPY_CONFIRM_MS = 1600; /** * Password-reveal card — surfaces the plaintext password the emulator * returned from `HousekeepingResetUserPasswordEvent` so the operator * can read it once and copy it out-of-band to the user. * * Sensitive data, so: * - Renders OUTSIDE the auto-dismissing status banner (which truncates * long content and disappears after 4s). * - Stays put until the operator explicitly dismisses — they have to * acknowledge they've copied/communicated the secret. * - The plaintext lives in `useHousekeepingStore.passwordReveal` and * never flows through the generic success-toast / banner pipeline * (`useHousekeepingActions.resetUserPassword` intercepts it before * `wrap`'s default path). * - The clipboard write uses the modern `navigator.clipboard.writeText` * when available, with a `document.execCommand('copy')` fallback for * non-secure-context legacy paths so the button still works inside an * `http://` deployment. */ export const HousekeepingPasswordReveal: FC = () => { const { passwordReveal, clearPasswordReveal } = useHousekeepingStore(); const [ copyState, setCopyState ] = useState<'idle' | 'ok' | 'fail'>('idle'); // Reset the "copied!" visual whenever a new reveal lands so the // operator doesn't see a stale checkmark from a previous reset. useEffect(() => { setCopyState('idle'); }, [ passwordReveal?.password ]); // Auto-revert the copy-confirmation icon back to the copy icon // a short while after a successful copy. The plaintext itself // stays revealed until the operator explicitly dismisses. useEffect(() => { if(copyState === 'idle') return; const handle = window.setTimeout(() => setCopyState('idle'), COPY_CONFIRM_MS); return () => window.clearTimeout(handle); }, [ copyState ]); if(!passwordReveal) return null; const copyPassword = async () => { const text = passwordReveal.password; if(!text) return; // Modern path — requires a secure context (https / wss / localhost). if(typeof navigator !== 'undefined' && navigator.clipboard && window.isSecureContext) { try { await navigator.clipboard.writeText(text); setCopyState('ok'); return; } catch { // Fall through to the legacy path below — some browsers // still gate the modern API behind extra permissions even // in secure contexts. } } // Legacy fallback: stage a textarea, select, exec copy. Works // on plain-http deployments where `navigator.clipboard` is // refused. The textarea is positioned off-screen so the user // doesn't see a flash. try { const textarea = document.createElement('textarea'); textarea.value = text; textarea.setAttribute('readonly', ''); textarea.style.position = 'absolute'; textarea.style.left = '-9999px'; textarea.style.top = '0'; document.body.appendChild(textarea); textarea.select(); textarea.setSelectionRange(0, text.length); const ok = document.execCommand('copy'); document.body.removeChild(textarea); setCopyState(ok ? 'ok' : 'fail'); } catch { setCopyState('fail'); } }; const copyIcon = copyState === 'ok' ? : ; const copyLabel = copyState === 'ok' ? LocalizeText('housekeeping.password.copied') : copyState === 'fail' ? LocalizeText('housekeeping.password.copy_failed') : LocalizeText('housekeeping.password.copy'); return (
{ LocalizeText('housekeeping.password.title', [ 'username', 'id' ], [ passwordReveal.username || '—', String(passwordReveal.userId) ]) }
{/* Readonly input lets the operator triple-click + Ctrl+C as a manual fallback to the copy button. Monospace + tabular-nums keeps lookalikes (Il1, O0) visually distinct so they can read it aloud without typos. */} event.currentTarget.select() } aria-label={ LocalizeText('housekeeping.password.value_label') } />

{ LocalizeText('housekeeping.password.hint') }

); };