Added recovery keys
This commit is contained in:
@@ -17,6 +17,7 @@ const THEME_PREVIEWS = {
|
||||
|
||||
const TABS = [
|
||||
{ id: 'account', label: 'My Account', section: 'USER SETTINGS' },
|
||||
{ id: 'security', label: 'Security', section: 'USER SETTINGS' },
|
||||
{ id: 'appearance', label: 'Appearance', section: 'USER SETTINGS' },
|
||||
{ id: 'voice', label: 'Voice & Video', section: 'APP SETTINGS' },
|
||||
{ id: 'keybinds', label: 'Keybinds', section: 'APP SETTINGS' },
|
||||
@@ -111,6 +112,7 @@ const UserSettings = ({ onClose, userId, username, onLogout }) => {
|
||||
<div style={{ flex: 1, display: 'flex', justifyContent: 'flex-start', overflowY: 'auto' }}>
|
||||
<div style={{ flex: 1, maxWidth: '740px', padding: '60px 40px 80px', position: 'relative' }}>
|
||||
{activeTab === 'account' && <MyAccountTab userId={userId} username={username} />}
|
||||
{activeTab === 'security' && <SecurityTab />}
|
||||
{activeTab === 'appearance' && <AppearanceTab />}
|
||||
{activeTab === 'voice' && <VoiceVideoTab />}
|
||||
{activeTab === 'keybinds' && <KeybindsTab />}
|
||||
@@ -550,6 +552,173 @@ const MyAccountTab = ({ userId, username }) => {
|
||||
);
|
||||
};
|
||||
|
||||
/* =========================================
|
||||
SECURITY TAB
|
||||
========================================= */
|
||||
const SecurityTab = () => {
|
||||
const [masterKey, setMasterKey] = useState(null);
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
const [confirmed, setConfirmed] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const mk = sessionStorage.getItem('masterKey');
|
||||
setMasterKey(mk);
|
||||
}, []);
|
||||
|
||||
const formatKey = (hex) => {
|
||||
if (!hex) return '';
|
||||
return hex.match(/.{1,4}/g).join(' ');
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
if (!masterKey) return;
|
||||
navigator.clipboard.writeText(masterKey).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
if (!masterKey) return;
|
||||
const content = [
|
||||
'=== RECOVERY KEY ===',
|
||||
'',
|
||||
'This is your Recovery Key for your encrypted account.',
|
||||
'Store it in a safe place. If you lose your password, this key is the ONLY way to recover your account.',
|
||||
'',
|
||||
'DO NOT share this key with anyone.',
|
||||
'',
|
||||
`Recovery Key: ${masterKey}`,
|
||||
'',
|
||||
`Exported: ${new Date().toISOString()}`,
|
||||
].join('\n');
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'recovery-key.txt';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const labelStyle = {
|
||||
display: 'block', color: 'var(--header-secondary)', fontSize: '12px',
|
||||
fontWeight: '700', textTransform: 'uppercase', marginBottom: '8px',
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ color: 'var(--header-primary)', margin: '0 0 20px', fontSize: '20px' }}>Security</h2>
|
||||
|
||||
<div style={{ backgroundColor: 'var(--bg-secondary)', borderRadius: '8px', padding: '20px' }}>
|
||||
<h3 style={{ color: 'var(--header-primary)', margin: '0 0 8px', fontSize: '16px', fontWeight: '600' }}>
|
||||
Recovery Key
|
||||
</h3>
|
||||
<p style={{ color: 'var(--text-muted)', fontSize: '14px', margin: '0 0 16px', lineHeight: '1.4' }}>
|
||||
Your Recovery Key allows you to reset your password without losing access to your encrypted messages.
|
||||
Store it somewhere safe — if you forget your password, this is the <strong style={{ color: 'var(--text-normal)' }}>only way</strong> to recover your account.
|
||||
</p>
|
||||
|
||||
{!masterKey ? (
|
||||
<div style={{
|
||||
backgroundColor: 'var(--bg-tertiary)', borderRadius: '4px', padding: '16px',
|
||||
color: 'var(--text-muted)', fontSize: '14px',
|
||||
}}>
|
||||
Recovery Key is not available in this session. Please log out and log back in to access it.
|
||||
</div>
|
||||
) : !revealed ? (
|
||||
<div>
|
||||
{/* Warning box */}
|
||||
<div style={{
|
||||
backgroundColor: 'rgba(250, 166, 26, 0.1)', border: '1px solid rgba(250, 166, 26, 0.4)',
|
||||
borderRadius: '4px', padding: '12px', marginBottom: '16px',
|
||||
}}>
|
||||
<div style={{ color: '#faa61a', fontSize: '14px', fontWeight: '600', marginBottom: '4px' }}>
|
||||
Warning
|
||||
</div>
|
||||
<div style={{ color: 'var(--text-normal)', fontSize: '13px', lineHeight: '1.4' }}>
|
||||
Anyone with your Recovery Key can reset your password and take control of your account.
|
||||
Only reveal it in a private, secure environment.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label style={{
|
||||
display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer',
|
||||
color: 'var(--text-normal)', fontSize: '14px', marginBottom: '16px',
|
||||
}}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={confirmed}
|
||||
onChange={(e) => setConfirmed(e.target.checked)}
|
||||
style={{ width: '16px', height: '16px', accentColor: 'var(--brand-experiment)' }}
|
||||
/>
|
||||
I understand and want to reveal my Recovery Key
|
||||
</label>
|
||||
|
||||
<button
|
||||
onClick={() => setRevealed(true)}
|
||||
disabled={!confirmed}
|
||||
style={{
|
||||
backgroundColor: 'var(--brand-experiment)', color: 'white', border: 'none',
|
||||
borderRadius: '4px', padding: '10px 20px', cursor: confirmed ? 'pointer' : 'not-allowed',
|
||||
fontSize: '14px', fontWeight: '500', opacity: confirmed ? 1 : 0.5,
|
||||
}}
|
||||
>
|
||||
Reveal Recovery Key
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<label style={labelStyle}>Your Recovery Key</label>
|
||||
<div style={{
|
||||
backgroundColor: 'var(--bg-tertiary)', borderRadius: '4px', padding: '16px',
|
||||
fontFamily: 'Consolas, "Courier New", monospace', fontSize: '15px',
|
||||
color: 'var(--text-normal)', wordBreak: 'break-all', lineHeight: '1.6',
|
||||
letterSpacing: '1px', marginBottom: '12px', userSelect: 'all',
|
||||
}}>
|
||||
{formatKey(masterKey)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
style={{
|
||||
backgroundColor: 'var(--brand-experiment)', color: 'white', border: 'none',
|
||||
borderRadius: '4px', padding: '8px 16px', cursor: 'pointer',
|
||||
fontSize: '14px', fontWeight: '500',
|
||||
}}
|
||||
>
|
||||
{copied ? 'Copied!' : 'Copy'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-tertiary)', color: 'var(--text-normal)', border: 'none',
|
||||
borderRadius: '4px', padding: '8px 16px', cursor: 'pointer',
|
||||
fontSize: '14px', fontWeight: '500',
|
||||
}}
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setRevealed(false); setConfirmed(false); }}
|
||||
style={{
|
||||
backgroundColor: 'transparent', color: 'var(--text-muted)', border: '1px solid var(--border-subtle)',
|
||||
borderRadius: '4px', padding: '8px 16px', cursor: 'pointer',
|
||||
fontSize: '14px', fontWeight: '500',
|
||||
}}
|
||||
>
|
||||
Hide
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/* =========================================
|
||||
APPEARANCE TAB
|
||||
========================================= */
|
||||
|
||||
Reference in New Issue
Block a user