From 926ebe7ffa98899f8c1c742bf173e54bcb5e7146 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Mon, 27 Jul 2026 23:26:23 +0200 Subject: [PATCH] =?UTF-8?q?refactor(api):=20extract=20the=20messaging=20do?= =?UTF-8?q?main=20to=20api/messaging.ts=20(R7=20S3b=C2=B74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The E2EE direct-message types (MessageUser/Message/Conversation/MyKeyBundle/ KeySetup) + the ~9 messaging methods move into a self-contained api/messaging.ts exporting a `messagingApi` slice (imports only core's req). api.ts spreads the slice and re-exports the types. api.ts 882 -> 819 lines; no runtime change. Gate green: typecheck, eslint, prettier, 74 vitest. --- frontend/src/lib/api.ts | 69 ++--------------------------- frontend/src/lib/api/messaging.ts | 72 +++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 66 deletions(-) create mode 100644 frontend/src/lib/api/messaging.ts diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 9d8a06d..79619ba 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -24,6 +24,8 @@ import { downloadsApi } from "./api/downloads"; export * from "./api/downloads"; import { plexApi } from "./api/plex"; export * from "./api/plex"; +import { messagingApi } from "./api/messaging"; +export * from "./api/messaging"; export interface Me { id: number; @@ -426,48 +428,6 @@ 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; @@ -846,30 +806,7 @@ export const api = { reorderViews: (ids: number[]): Promise<{ ok: boolean }> => req("/api/saved-views/reorder", { method: "POST", body: JSON.stringify({ ids }) }), - // --- direct messages (end-to-end encrypted) --- - messageMyKey: (): Promise => req("/api/messages/keys/me"), - setupMessageKey: (k: KeySetup): Promise<{ configured: boolean }> => - req("/api/messages/keys", { method: "POST", body: JSON.stringify(k) }), - // Forgotten-passphrase reset: drops the server key (and the now-undecryptable history) so a new - // passphrase can be set. Password accounts must pass their current password; errors shown inline. - resetMessageKey: (currentPassword?: string): Promise<{ configured: boolean }> => - req( - "/api/messages/keys/reset", - { method: "POST", body: JSON.stringify({ current_password: currentPassword ?? null }) }, - { quiet: true }, - ), - messagePublicKey: (userId: number): Promise<{ user_id: number; public_key: string }> => - req(`/api/messages/keys/${userId}`), - messageUsers: (): Promise => 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 => - req("/api/messages", { - method: "POST", - body: JSON.stringify({ recipient_id: recipientId, ciphertext, iv }), - }), - messageUnreadCount: (): Promise<{ count: number }> => req("/api/messages/unread_count"), + ...messagingApi, // --- user tags --- createTag: (t: { name: string; color?: string; category?: string }) => diff --git a/frontend/src/lib/api/messaging.ts b/frontend/src/lib/api/messaging.ts new file mode 100644 index 0000000..95f9b0b --- /dev/null +++ b/frontend/src/lib/api/messaging.ts @@ -0,0 +1,72 @@ +// Direct-messaging domain: the E2EE key bundle setup/reset, the conversation list, threads, and +// send/unread. Self-contained — types reference only each other, methods only touch core's req. +// (The WebCrypto layer that encrypts/decrypts message bodies lives in e2ee.ts, not here.) +import { req } from "./core"; + +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; +} + +export const messagingApi = { + messageMyKey: (): Promise => req("/api/messages/keys/me"), + setupMessageKey: (k: KeySetup): Promise<{ configured: boolean }> => + req("/api/messages/keys", { method: "POST", body: JSON.stringify(k) }), + // Forgotten-passphrase reset: drops the server key (and the now-undecryptable history) so a new + // passphrase can be set. Password accounts must pass their current password; errors shown inline. + resetMessageKey: (currentPassword?: string): Promise<{ configured: boolean }> => + req( + "/api/messages/keys/reset", + { method: "POST", body: JSON.stringify({ current_password: currentPassword ?? null }) }, + { quiet: true }, + ), + messagePublicKey: (userId: number): Promise<{ user_id: number; public_key: string }> => + req(`/api/messages/keys/${userId}`), + messageUsers: (): Promise => 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 => + req("/api/messages", { + method: "POST", + body: JSON.stringify({ recipient_id: recipientId, ciphertext, iv }), + }), + messageUnreadCount: (): Promise<{ count: number }> => req("/api/messages/unread_count"), +};