feat: Implement 'Lock/Edit' mode with blur and confirmation dialog

This commit is contained in:
LC mac
2026-01-31 01:25:27 +08:00
parent 7564ddc7c9
commit 2a7ac1cce0
10 changed files with 158 additions and 52 deletions

View File

@@ -17,11 +17,11 @@ import { validateBip39Mnemonic } from './lib/bip39';
import { buildPlaintext, encryptToSeedPgp, decryptSeedPgp } from './lib/seedpgp';
import * as openpgp from 'openpgp';
import { SecurityWarnings } from './components/SecurityWarnings';
import { ReadOnly } from './components/ReadOnly';
import { getSessionKey, encryptJsonToBlob, destroySessionKey, EncryptedBlob } from './lib/sessionCrypto';
import Header from './components/Header';
import { StorageDetails } from './components/StorageDetails';
import { ClipboardDetails } from './components/ClipboardDetails';
import Footer from './components/Footer';
console.log("OpenPGP.js version:", openpgp.config.versionString);
@@ -64,6 +64,7 @@ function App() {
const [localItems, setLocalItems] = useState<StorageItem[]>([]);
const [sessionItems, setSessionItems] = useState<StorageItem[]>([]);
const [clipboardEvents, setClipboardEvents] = useState<ClipboardEvent[]>([]);
const [showLockConfirm, setShowLockConfirm] = useState(false);
const SENSITIVE_PATTERNS = ['key', 'mnemonic', 'seed', 'private', 'secret', 'pgp', 'password'];
@@ -100,21 +101,7 @@ function App() {
return () => clearInterval(interval);
}, []);
useEffect(() => {
// When entering read-only mode, clear sensitive data for security.
if (isReadOnly) {
setMnemonic('');
setBackupMessagePassword('');
setRestoreMessagePassword('');
setPublicKeyInput('');
setPrivateKeyInput('');
setPrivateKeyPassphrase('');
setQrPayload('');
setRestoreInput('');
setDecryptedRestoredMnemonic(null);
setError('');
}
}, [isReadOnly]);
// Cleanup session key on component unmount
useEffect(() => {
@@ -319,6 +306,21 @@ function App() {
setShowQRScanner(false);
};
const handleToggleLock = () => {
if (!isReadOnly) {
// About to lock - show confirmation
setShowLockConfirm(true);
} else {
// Unlocking - no confirmation needed
setIsReadOnly(false);
}
};
const confirmLock = () => {
setIsReadOnly(true);
setShowLockConfirm(false);
};
return (
<div className="min-h-screen bg-slate-800 text-slate-100">
@@ -334,6 +336,8 @@ function App() {
encryptedMnemonicCache={encryptedMnemonicCache}
handleLockAndClear={handleLockAndClear}
appVersion={__APP_VERSION__}
isLocked={isReadOnly}
onToggleLock={handleToggleLock}
/>
<main className="max-w-7xl mx-auto px-6 py-4">
<div className="p-6 md:p-8 space-y-6">
@@ -350,10 +354,10 @@ function App() {
{/* Info Banner */}
{recipientFpr && activeTab === 'backup' && (
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg flex items-start gap-3 text-blue-800 text-xs animate-in fade-in">
<div className="p-3 bg-teal-50 border border-teal-200 rounded-lg flex items-start gap-3 text-teal-800 text-xs animate-in fade-in">
<Info size={16} className="shrink-0 mt-0.5" />
<div>
<strong>Recipient Key:</strong> <code className="bg-blue-100 px-1.5 py-0.5 rounded font-mono">{recipientFpr}</code>
<strong>Recipient Key:</strong> <code className="bg-teal-100 px-1.5 py-0.5 rounded font-mono">{recipientFpr}</code>
</div>
</div>
)}
@@ -364,9 +368,11 @@ function App() {
{activeTab === 'backup' ? (
<>
<div className="space-y-2">
<label className="text-sm font-semibold text-slate-700">BIP39 Mnemonic</label>
<label className="text-sm font-semibold text-slate-200">BIP39 Mnemonic</label>
<textarea
className="w-full h-32 p-4 bg-slate-50 border border-slate-200 rounded-xl text-sm font-mono text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all resize-none"
className={`w-full h-32 p-4 bg-slate-50 border border-slate-200 rounded-xl text-sm font-mono text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-teal-500 transition-all resize-none ${
isReadOnly ? 'blur-sm select-none' : ''
}`}
data-sensitive="BIP39 Mnemonic"
placeholder="Enter your 12 or 24 word seed phrase..."
@@ -399,9 +405,11 @@ function App() {
</div>
<div className="space-y-2">
<label className="text-sm font-semibold text-slate-700">SEEDPGP1 Payload</label>
<label className="text-sm font-semibold text-slate-200">SEEDPGP1 Payload</label>
<textarea
className="w-full h-32 p-4 bg-slate-50 border border-slate-200 rounded-xl text-xs font-mono text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all resize-none"
className={`w-full h-32 p-4 bg-slate-50 border border-slate-200 rounded-xl text-xs font-mono text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-teal-500 transition-all resize-none ${
isReadOnly ? 'blur-sm select-none' : ''
}`}
placeholder="SEEDPGP1:0:ABCD:..."
value={restoreInput}
@@ -428,7 +436,9 @@ function App() {
<input
type="password"
data-sensitive="Message Password"
className="w-full pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all"
className={`w-full pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 transition-all ${
isReadOnly ? 'blur-sm select-none' : ''
}`}
placeholder="Unlock private key..."
value={privateKeyPassphrase}
onChange={(e) => setPrivateKeyPassphrase(e.target.value)}
@@ -443,7 +453,7 @@ function App() {
{/* Security Panel */}
<div className="space-y-2"> {/* Added space-y-2 wrapper */}
<label className="text-sm font-semibold text-slate-700 flex items-center gap-2">
<label className="text-sm font-semibold text-slate-200 flex items-center gap-2">
<Lock size={14} /> SECURITY OPTIONS
</label>
@@ -456,7 +466,9 @@ function App() {
<Lock className="absolute left-3 top-3 text-slate-400" size={16} />
<input
type="password"
className="w-full pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all"
className={`w-full pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 transition-all ${
isReadOnly ? 'blur-sm select-none' : ''
}`}
placeholder="Optional password..."
value={activeTab === 'backup' ? backupMessagePassword : restoreMessagePassword}
onChange={(e) => activeTab === 'backup' ? setBackupMessagePassword(e.target.value) : setRestoreMessagePassword(e.target.value)}
@@ -475,7 +487,7 @@ function App() {
checked={hasBip39Passphrase}
onChange={(e) => setHasBip39Passphrase(e.target.checked)}
disabled={isReadOnly}
className="rounded text-blue-600 focus:ring-2 focus:ring-blue-500 transition-all"
className="rounded text-teal-600 focus:ring-2 focus:ring-teal-500 transition-all"
/>
<span className="text-xs font-medium text-slate-700 group-hover:text-slate-900 transition-colors">
BIP39 25th word active
@@ -484,12 +496,6 @@ function App() {
</div>
)}
<ReadOnly
isReadOnly={isReadOnly}
onToggle={setIsReadOnly}
appVersion={__APP_VERSION__}
buildHash={__BUILD_HASH__}
/>
</div>
{/* Action Button */}
@@ -497,7 +503,7 @@ function App() {
<button
onClick={handleBackup}
disabled={!mnemonic || loading || isReadOnly}
className="w-full py-4 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-xl font-bold flex items-center justify-center gap-2 hover:from-blue-700 hover:to-blue-800 transition-all shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:from-blue-600 disabled:hover:to-blue-700"
className="w-full py-4 bg-gradient-to-r from-teal-500 to-cyan-600 text-white rounded-xl font-bold flex items-center justify-center gap-2 hover:from-teal-600 hover:to-cyan-700 transition-all shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:from-teal-500 disabled:hover:to-cyan-600"
>
{loading ? (
<RefreshCw className="animate-spin" size={20} />
@@ -526,7 +532,7 @@ function App() {
{/* QR Output */}
{qrPayload && activeTab === 'backup' && (
<div className="pt-6 border-t border-slate-200 space-y-6 animate-in fade-in slide-in-from-bottom-4">
<div className="flex justify-center">
<div className={isReadOnly ? 'blur-lg' : ''}>
<QrDisplay value={qrPayload} />
</div>
<div className="space-y-2">
@@ -549,7 +555,7 @@ function App() {
readOnly
value={qrPayload}
onFocus={(e) => e.currentTarget.select()}
className="w-full h-28 p-3 bg-slate-900 rounded-xl font-mono text-[10px] text-green-400 placeholder:text-slate-500 border border-slate-700 shadow-inner leading-relaxed resize-none focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full h-28 p-3 bg-slate-900 rounded-xl font-mono text-[10px] text-green-400 placeholder:text-slate-500 border border-slate-700 shadow-inner leading-relaxed resize-none focus:outline-none focus:ring-2 focus:ring-teal-500"
/>
<p className="text-[11px] text-slate-500">
Tip: click the box to select all, or use Copy.
@@ -575,7 +581,9 @@ function App() {
</div>
<div className="p-6 bg-white rounded-xl border-2 border-green-200 shadow-sm">
<p className="font-mono text-center text-lg text-slate-800 tracking-wide leading-relaxed break-words">
<p className={`font-mono text-center text-lg text-slate-800 tracking-wide leading-relaxed break-words ${
isReadOnly ? 'blur-md select-none' : ''
}`}>
{decryptedRestoredMnemonic}
</p>
</div>
@@ -584,6 +592,11 @@ function App() {
)}
</div>
</main>
<Footer
appVersion={__APP_VERSION__}
buildHash={__BUILD_HASH__}
buildTimestamp={__BUILD_TIMESTAMP__}
/>
{/* QR Scanner Modal */}
{showQRScanner && (
@@ -650,8 +663,45 @@ function App() {
</div>
</div>
)}
{/* Lock Confirmation Modal */}
{showLockConfirm && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50">
<div className="bg-slate-800 rounded-xl border border-slate-700 p-6 max-w-md w-full mx-4 shadow-2xl">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<Lock className="w-5 h-5 text-amber-500" />
Lock Sensitive Data?
</h3>
<div className="text-sm text-slate-300 space-y-3 mb-6">
<p>This will:</p>
<ul className="list-disc list-inside space-y-1 text-slate-400">
<li>Blur all sensitive data (mnemonics, keys, passwords)</li>
<li>Disable all inputs</li>
<li>Prevent clipboard operations</li>
</ul>
<p className="text-xs text-slate-500 mt-2">
Use this when showing the app to others or stepping away from your device.
</p>
</div>
<div className="flex gap-3">
<button
className="flex-1 py-2 bg-slate-700 hover:bg-slate-600 rounded-lg transition-colors"
onClick={() => setShowLockConfirm(false)}
>
Cancel
</button>
<button
className="flex-1 py-2 bg-amber-500 hover:bg-amber-600 text-white font-semibold rounded-lg transition-colors"
onClick={confirmLock}
>
Lock Data
</button>
</div>
</div>
</div>
)}
</div>
);
}
export default App;
export default App;