mirror of
https://github.com/kccleoc/seedpgp-web.git
synced 2026-03-07 09:57:50 +08:00
feat: Implement 'Lock/Edit' mode with blur and confirmation dialog
This commit is contained in:
124
src/App.tsx
124
src/App.tsx
@@ -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;
|
||||
Reference in New Issue
Block a user