Add encrypted key support for chat and nodes

Introduces encrypted private key storage for nodes and users, updates chat message schema to support sender-side encryption, and adds supporting libraries and tests for cryptographic signing and identity unlock flows. Includes new database migrations, API route updates, and React components for identity unlock prompts.
This commit is contained in:
Christopher
2026-01-27 17:13:28 -08:00
parent 25f71e320b
commit 5903022f8a
46 changed files with 7457 additions and 113 deletions
+79 -3
View File
@@ -7,16 +7,18 @@ import { usePathname, useRouter } from 'next/navigation';
import { useAuth } from '@/lib/contexts/AuthContext';
import { HomeIcon, SearchIcon, BellIcon, UserIcon, ShieldIcon, SettingsIcon, BotIcon } from './Icons';
import { formatFullHandle } from '@/lib/utils/handle';
import { LogOut, Settings2 } from 'lucide-react';
import { LogOut, Settings2, Lock, Unlock } from 'lucide-react';
import { IdentityUnlockPrompt } from './IdentityUnlockPrompt';
export function Sidebar() {
const { user, isAdmin } = useAuth();
const { user, isAdmin, logout, isIdentityUnlocked } = useAuth();
const pathname = usePathname();
const router = useRouter();
const [customLogoUrl, setCustomLogoUrl] = useState<string | null | undefined>(undefined);
const [unreadCount, setUnreadCount] = useState(0);
const [unreadChatCount, setUnreadChatCount] = useState(0);
const [loggingOut, setLoggingOut] = useState(false);
const [showUnlockPrompt, setShowUnlockPrompt] = useState(false);
useEffect(() => {
fetch('/api/node')
@@ -75,7 +77,7 @@ export function Sidebar() {
setLoggingOut(true);
try {
await fetch('/api/auth/logout', { method: 'POST' });
await logout();
window.location.href = '/explore';
} catch (error) {
console.error('Logout failed:', error);
@@ -188,6 +190,72 @@ export function Sidebar() {
<div style={{ color: 'var(--foreground-tertiary)', fontSize: '13px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{formatFullHandle(user.handle)}</div>
</div>
</div>
{/* Identity Status Indicator */}
<div
onClick={() => {
if (!isIdentityUnlocked) {
setShowUnlockPrompt(true);
}
}}
title={isIdentityUnlocked ? 'Identity unlocked - you can perform actions' : 'Identity locked - click to unlock'}
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '10px 12px',
marginBottom: '12px',
borderRadius: '8px',
background: isIdentityUnlocked
? 'rgba(34, 197, 94, 0.1)'
: 'rgba(251, 191, 36, 0.1)',
border: isIdentityUnlocked
? '1px solid rgba(34, 197, 94, 0.2)'
: '1px solid rgba(251, 191, 36, 0.2)',
cursor: isIdentityUnlocked ? 'default' : 'pointer',
transition: 'all 0.2s',
}}
onMouseEnter={(e) => {
if (!isIdentityUnlocked) {
e.currentTarget.style.background = 'rgba(251, 191, 36, 0.15)';
}
}}
onMouseLeave={(e) => {
if (!isIdentityUnlocked) {
e.currentTarget.style.background = 'rgba(251, 191, 36, 0.1)';
}
}}
>
{isIdentityUnlocked ? (
<Unlock size={16} style={{ color: 'rgb(34, 197, 94)', flexShrink: 0 }} />
) : (
<Lock size={16} style={{ color: 'rgb(251, 191, 36)', flexShrink: 0 }} />
)}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
fontSize: '13px',
fontWeight: 500,
color: isIdentityUnlocked ? 'rgb(34, 197, 94)' : 'rgb(251, 191, 36)',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}>
{isIdentityUnlocked ? 'Identity Unlocked' : 'Identity Locked'}
</div>
<div style={{
fontSize: '11px',
color: 'var(--foreground-tertiary)',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}>
{isIdentityUnlocked
? 'You can perform actions'
: 'Click to unlock'}
</div>
</div>
</div>
<button
onClick={handleLogout}
disabled={loggingOut}
@@ -205,6 +273,14 @@ export function Sidebar() {
</button>
</div>
)}
{/* Identity Unlock Prompt Modal */}
{showUnlockPrompt && (
<IdentityUnlockPrompt
onUnlock={() => setShowUnlockPrompt(false)}
onCancel={() => setShowUnlockPrompt(false)}
/>
)}
</aside>
);
}