From bd52b3c05ac9986a1cf17d0ff11dea21809164af Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Sat, 30 Aug 2025 12:14:36 +0300 Subject: [PATCH] Fix crypto and message duplication --- frontend/src/auth/crypto.ts | 34 ++++++++++++++------------ frontend/src/ui/hooks/useChat.ts | 23 ++++++++++++----- frontend/src/ui/screen/LoginScreen.tsx | 9 ++++--- frontend/src/ui/state.ts | 18 ++++++++++---- 4 files changed, 55 insertions(+), 29 deletions(-) diff --git a/frontend/src/auth/crypto.ts b/frontend/src/auth/crypto.ts index 902cf7b..d416f7a 100644 --- a/frontend/src/auth/crypto.ts +++ b/frontend/src/auth/crypto.ts @@ -8,30 +8,33 @@ import type { BackupBlob, UploadPublicKeyRequest } from "../core/types"; let currentPublicKey: Uint8Array | null = null; let currentPrivateKey: Uint8Array | null = null; -async function fetchPublicKey(): Promise { - const res = await fetch(`${API_BASE_URL}/crypto/public-key`, { method: "GET", headers: getAuthHeaders(true) }); +async function fetchPublicKey(token?: string): Promise { + const headers = token ? { 'Authorization': `Bearer ${token}` } : getAuthHeaders(true); + const res = await fetch(`${API_BASE_URL}/crypto/public-key`, { method: "GET", headers }); if (!res.ok) return null; const data = await res.json(); if (!data?.publicKey) return null; return ub64(data.publicKey); } -async function uploadPublicKey(publicKey: Uint8Array): Promise { +async function uploadPublicKey(publicKey: Uint8Array, token?: string): Promise { const payload: UploadPublicKeyRequest = { publicKey: b64(publicKey) } + const headers = token ? { 'Authorization': `Bearer ${token}` } : getAuthHeaders(true); await fetch(`${API_BASE_URL}/crypto/public-key`, { method: "POST", - headers: getAuthHeaders(true), + headers, body: JSON.stringify(payload) }); } -async function fetchBackupBlob(): Promise { +async function fetchBackupBlob(token?: string): Promise { + const headers = token ? { 'Authorization': `Bearer ${token}` } : getAuthHeaders(true); const res = await fetch(`${API_BASE_URL}/crypto/backup`, { method: "GET", - headers: getAuthHeaders(true) + headers }); if (res.ok) { const response: BackupBlob = await res.json(); @@ -41,12 +44,13 @@ async function fetchBackupBlob(): Promise { } } -async function uploadBackupBlob(blobJson: string): Promise { +async function uploadBackupBlob(blobJson: string, token?: string): Promise { const payload: BackupBlob = { blob: blobJson } + const headers = token ? { 'Authorization': `Bearer ${token}` } : getAuthHeaders(true); await fetch(`${API_BASE_URL}/crypto/backup`, { method: "POST", - headers: getAuthHeaders(true), + headers, body: JSON.stringify(payload) }); } @@ -61,16 +65,16 @@ export function getCurrentKeys(): UserKeyPairMemory | null { return null; } -export async function ensureKeysOnLogin(password: string): Promise { +export async function ensureKeysOnLogin(password: string, token?: string): Promise { // Try to restore from backup - const blobJson = await fetchBackupBlob(); + const blobJson = await fetchBackupBlob(token); if (blobJson) { const blob = decodeBlob(blobJson); const bundle = await decryptBackupWithPassword(password, blob); currentPrivateKey = bundle.privateKey; // Ensure public key exists on server; if not, derive from private (not possible via libsafely), so keep previous // In our simple scheme, we rely on server having the public key or we reupload generated one on first setup - const serverPub = await fetchPublicKey(); + const serverPub = await fetchPublicKey(token); if (serverPub) { currentPublicKey = serverPub; } else { @@ -78,9 +82,9 @@ export async function ensureKeysOnLogin(password: string): Promise { - if (!user.authToken) return; + if (!user.authToken || messagesLoadedRef.current) return; try { const response = await fetch(`${API_BASE_URL}/get_messages`, { @@ -38,6 +40,7 @@ export function useChat() { }); } } + messagesLoadedRef.current = true; } catch (error) { console.error("Error loading messages:", error); } @@ -99,12 +102,20 @@ export function useChat() { return () => { websocket.removeEventListener("message", handleWebSocketMessage); }; - }, [addMessage, user.currentUser]); + }, [addMessage, updateMessage, removeMessage, user.currentUser]); - // Load messages when component mounts or chat changes + // Load messages only once when component mounts and user is authenticated useEffect(() => { - loadMessages(); - }, [loadMessages]); + if (user.authToken && !messagesLoadedRef.current) { + loadMessages(); + } + }, [user.authToken, loadMessages]); + + // Reset messages loaded flag and clear messages when chat changes + useEffect(() => { + messagesLoadedRef.current = false; + clearMessages(); // Clear messages when switching chats + }, [chat.currentChat, clearMessages]); return { messages: chat.messages, diff --git a/frontend/src/ui/screen/LoginScreen.tsx b/frontend/src/ui/screen/LoginScreen.tsx index 452b796..5a172fb 100644 --- a/frontend/src/ui/screen/LoginScreen.tsx +++ b/frontend/src/ui/screen/LoginScreen.tsx @@ -55,13 +55,16 @@ export default function LoginScreen() { if (response.ok) { const data: LoginResponse = await response.json(); - // Store the JWT token - setUser(data.token, data.user) + // Store the JWT token first + setUser(data.token, data.user); + + // Setup keys with the token we just received try { - await ensureKeysOnLogin(password); + await ensureKeysOnLogin(password, data.token); } catch (e) { console.error("Key setup failed:", e); } + setCurrentPage("chat"); // initializeProfile(); // Initialize profile after login } else { diff --git a/frontend/src/ui/state.ts b/frontend/src/ui/state.ts index 4e517a7..7d414fb 100644 --- a/frontend/src/ui/state.ts +++ b/frontend/src/ui/state.ts @@ -49,12 +49,20 @@ export const useAppState = create((set, get) => ({ dmUsers: [], activeDm: null }, - addMessage: (message: Message) => set((state) => ({ - chat: { - ...state.chat, - messages: [...state.chat.messages, message] + addMessage: (message: Message) => set((state) => { + // Check if message already exists to prevent duplicates + const messageExists = state.chat.messages.some(msg => msg.id === message.id); + if (messageExists) { + return state; // Return unchanged state if message already exists } - })), + + return { + chat: { + ...state.chat, + messages: [...state.chat.messages, message] + } + }; + }), updateMessage: (messageId: number, updatedMessage: Partial) => set((state) => ({ chat: { ...state.chat,