9b764053c4
Disable the previous auto-share of conversation keys to newly-registered devices: a stolen password / new device registered by an attacker no longer automatically grants history access. Backup-Restore (which restores the old device-id) still opens existing wraps as before. Phase 1 of the approval replacement: - New `lib/deviceApproval.ts`: realtime listener for `devices` INSERT, surfaces a pending list, persists approve/deny decisions in `chatapp.approvedDeviceIds` / `chatapp.dismissedDeviceIds`. Filters the initial fetch by created_at > own-device's created_at so a freshly installed client doesn't try to "approve" pre-existing devices. - New `components/DeviceApprovalBanner.tsx`: bottom-right Discord-style banner per pending request with Genehmigen / Ablehnen actions; reuses `wrapForOneDevice` from conversationKeySync to fan out conv-keys. - AppShell mounts both the listener and the banner. Plus dev userData isolation in main.ts: when running unpackaged, append `-Dev` to the userData path so `pnpm dev` runs side-by-side with the installed packaged build instead of colliding on the single-instance lock. Window title also distinguished as "ChatApp (Dev)". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
67 lines
2.3 KiB
TypeScript
67 lines
2.3 KiB
TypeScript
import { loadDevicePrivateKey } from '@chat-app/shared/auth';
|
|
import { useEffect } from 'react';
|
|
import { Outlet } from 'react-router-dom';
|
|
|
|
import { useAuth } from '../context/AuthContext';
|
|
import { startConversationKeySync } from '../lib/conversationKeySync';
|
|
import { startDeviceApprovalListener } from '../lib/deviceApproval';
|
|
import { ensureNotificationPermission } from '../lib/osNotify';
|
|
import { devLocalSecretStore } from '../lib/secretStore';
|
|
import { BackupPromptBanner } from './BackupPromptBanner';
|
|
import { CallUI } from './CallUI';
|
|
import { DeviceApprovalBanner } from './DeviceApprovalBanner';
|
|
import { Sidebar } from './Sidebar';
|
|
|
|
export function AppShell() {
|
|
const { session, device } = useAuth();
|
|
useEffect(() => {
|
|
// Prompt once per authenticated shell mount. Module-level guard prevents
|
|
// re-asking if the user already responded this session.
|
|
void ensureNotificationPermission();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!session?.user.id || !device?.id) return;
|
|
const userId = session.user.id;
|
|
const deviceId = device.id;
|
|
const stopKeySync = startConversationKeySync(userId, deviceId);
|
|
const stopApproval = startDeviceApprovalListener({
|
|
ownUserId: userId,
|
|
ownDeviceId: deviceId,
|
|
getPriv: () => loadDevicePrivateKey(devLocalSecretStore, userId, deviceId),
|
|
});
|
|
return () => {
|
|
stopKeySync();
|
|
stopApproval();
|
|
};
|
|
}, [session?.user.id, device?.id]);
|
|
|
|
return (
|
|
<div className="relative flex min-h-screen overflow-hidden bg-surface text-fg">
|
|
<ShellBackground />
|
|
<div className="relative z-10 flex min-h-screen w-full">
|
|
<Sidebar />
|
|
<main className="relative flex-1 overflow-hidden">
|
|
<div className="h-screen overflow-y-auto">
|
|
<Outlet />
|
|
</div>
|
|
</main>
|
|
</div>
|
|
<CallUI />
|
|
<BackupPromptBanner />
|
|
<DeviceApprovalBanner />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Subtle desktop texture for dark mode. The panels carry the depth; the
|
|
// background stays quiet so chat content remains the focus.
|
|
function ShellBackground() {
|
|
return (
|
|
<div aria-hidden="true" className="pointer-events-none absolute inset-0 hidden dark:block">
|
|
<div className="bg-grid absolute inset-0 opacity-[0.08]" />
|
|
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(30,31,34,0.94),rgba(17,18,20,0.98))]" />
|
|
</div>
|
|
);
|
|
}
|