feat(messages): E2EE real-time messaging UI + system welcome
A Messages tab in the notification center (hidden for demo): set up secure messaging with a passphrase (key generated + wrapped in-browser via WebCrypto, stored non-extractable per device), unlock on other devices, then chat with end-to-end encrypted, live-delivered messages. The server-authored Siftlode welcome is readable before any key setup. - lib/e2ee.ts: ECDH P-256 + HKDF + AES-GCM, PBKDF2-wrapped key, IndexedDB. - lib/messagesSocket.ts: WebSocket client with backoff reconnect. - Messages.tsx: key gate, conversation list with decrypted previews, directory, thread with client-side decrypt + encrypt-on-send. Unread feeds the nav badge. EN/HU/DE strings.
This commit is contained in:
parent
002a79949b
commit
2f0ca68e8a
13 changed files with 943 additions and 6 deletions
|
|
@ -469,6 +469,48 @@ export interface AppNotification {
|
|||
created_at: string | null;
|
||||
}
|
||||
|
||||
export interface MessageUser {
|
||||
id: number;
|
||||
name: string;
|
||||
avatar_url: string | null;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id: number;
|
||||
kind: "user" | "system";
|
||||
sender_id: number | null;
|
||||
recipient_id: number;
|
||||
body: string | null; // plaintext, system messages only
|
||||
ciphertext: string | null; // base64, E2EE user messages only
|
||||
iv: string | null;
|
||||
read_at: string | null;
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
export interface Conversation {
|
||||
partner: MessageUser;
|
||||
last_message: Message;
|
||||
unread: number;
|
||||
}
|
||||
|
||||
// My own E2EE key bundle. When configured, the wrapped blob + params let a device unlock.
|
||||
export interface MyKeyBundle {
|
||||
configured: boolean;
|
||||
public_key?: string;
|
||||
wrapped_private_key?: string;
|
||||
salt?: string;
|
||||
wrap_iv?: string;
|
||||
key_check?: string;
|
||||
}
|
||||
|
||||
export interface KeySetup {
|
||||
public_key: string;
|
||||
wrapped_private_key: string;
|
||||
salt: string;
|
||||
wrap_iv: string;
|
||||
key_check: string;
|
||||
}
|
||||
|
||||
// Admin Configuration page: one DB-overridable setting (registry entry on the backend).
|
||||
export interface ConfigItem {
|
||||
key: string;
|
||||
|
|
@ -712,6 +754,23 @@ export const api = {
|
|||
req(`/api/me/notifications/${id}/dismiss`, { method: "POST" }),
|
||||
clearNotifications: () => req("/api/me/notifications/clear", { method: "POST" }),
|
||||
|
||||
// --- direct messages (end-to-end encrypted) ---
|
||||
messageMyKey: (): Promise<MyKeyBundle> => req("/api/messages/keys/me"),
|
||||
setupMessageKey: (k: KeySetup): Promise<{ configured: boolean }> =>
|
||||
req("/api/messages/keys", { method: "POST", body: JSON.stringify(k) }),
|
||||
messagePublicKey: (userId: number): Promise<{ user_id: number; public_key: string }> =>
|
||||
req(`/api/messages/keys/${userId}`),
|
||||
messageUsers: (): Promise<MessageUser[]> => req("/api/messages/users"),
|
||||
conversations: (): Promise<{ items: Conversation[] }> => req("/api/messages/conversations"),
|
||||
thread: (partnerId: number): Promise<{ partner: MessageUser; items: Message[] }> =>
|
||||
req(`/api/messages/conversations/${partnerId}`),
|
||||
sendMessage: (recipientId: number, ciphertext: string, iv: string): Promise<Message> =>
|
||||
req("/api/messages", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ recipient_id: recipientId, ciphertext, iv }),
|
||||
}),
|
||||
messageUnreadCount: (): Promise<{ count: number }> => req("/api/messages/unread_count"),
|
||||
|
||||
// --- user tags ---
|
||||
createTag: (t: { name: string; color?: string; category?: string }) =>
|
||||
req("/api/tags", { method: "POST", body: JSON.stringify(t) }),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue