diff --git a/surfsense_desktop/electron-builder.yml b/surfsense_desktop/electron-builder.yml index be5e07c63..4d6f0b283 100644 --- a/surfsense_desktop/electron-builder.yml +++ b/surfsense_desktop/electron-builder.yml @@ -58,7 +58,7 @@ win: icon: assets/icon.ico target: - target: nsis - arch: [x64, arm64] + arch: [x64] nsis: oneClick: false perMachine: false diff --git a/surfsense_desktop/src/ipc/channels.ts b/surfsense_desktop/src/ipc/channels.ts index 2a50de75f..d8a30347f 100644 --- a/surfsense_desktop/src/ipc/channels.ts +++ b/surfsense_desktop/src/ipc/channels.ts @@ -32,4 +32,7 @@ export const IPC_CHANNELS = { FOLDER_SYNC_ACK_EVENTS: 'folder-sync:ack-events', BROWSE_FILES: 'browse:files', READ_LOCAL_FILES: 'browse:read-local-files', + // Auth token sync across windows + GET_AUTH_TOKENS: 'auth:get-tokens', + SET_AUTH_TOKENS: 'auth:set-tokens', } as const; diff --git a/surfsense_desktop/src/ipc/handlers.ts b/surfsense_desktop/src/ipc/handlers.ts index de7cdb659..b36dcbdcd 100644 --- a/surfsense_desktop/src/ipc/handlers.ts +++ b/surfsense_desktop/src/ipc/handlers.ts @@ -21,6 +21,8 @@ import { readLocalFiles, } from '../modules/folder-watcher'; +let authTokens: { bearer: string; refresh: string } | null = null; + export function registerIpcHandlers(): void { ipcMain.on(IPC_CHANNELS.OPEN_EXTERNAL, (_event, url: string) => { try { @@ -89,4 +91,12 @@ export function registerIpcHandlers(): void { ipcMain.handle(IPC_CHANNELS.READ_LOCAL_FILES, (_event, paths: string[]) => readLocalFiles(paths) ); + + ipcMain.handle(IPC_CHANNELS.SET_AUTH_TOKENS, (_event, tokens: { bearer: string; refresh: string }) => { + authTokens = tokens; + }); + + ipcMain.handle(IPC_CHANNELS.GET_AUTH_TOKENS, () => { + return authTokens; + }); } diff --git a/surfsense_desktop/src/preload.ts b/surfsense_desktop/src/preload.ts index 6a9190693..2811c3b46 100644 --- a/surfsense_desktop/src/preload.ts +++ b/surfsense_desktop/src/preload.ts @@ -68,4 +68,9 @@ contextBridge.exposeInMainWorld('electronAPI', { // Browse files via native dialog browseFiles: () => ipcRenderer.invoke(IPC_CHANNELS.BROWSE_FILES), readLocalFiles: (paths: string[]) => ipcRenderer.invoke(IPC_CHANNELS.READ_LOCAL_FILES, paths), + + // Auth token sync across windows + getAuthTokens: () => ipcRenderer.invoke(IPC_CHANNELS.GET_AUTH_TOKENS), + setAuthTokens: (bearer: string, refresh: string) => + ipcRenderer.invoke(IPC_CHANNELS.SET_AUTH_TOKENS, { bearer, refresh }), }); diff --git a/surfsense_web/app/dashboard/layout.tsx b/surfsense_web/app/dashboard/layout.tsx index f727a2018..25bea5467 100644 --- a/surfsense_web/app/dashboard/layout.tsx +++ b/surfsense_web/app/dashboard/layout.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react"; import { USER_QUERY_KEY } from "@/atoms/user/user-query.atoms"; import { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; -import { getBearerToken, redirectToLogin } from "@/lib/auth-utils"; +import { getBearerToken, ensureTokensFromElectron, redirectToLogin } from "@/lib/auth-utils"; import { queryClient } from "@/lib/query-client/client"; interface DashboardLayoutProps { @@ -17,15 +17,20 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) { useGlobalLoadingEffect(isCheckingAuth); useEffect(() => { - // Check if user is authenticated - const token = getBearerToken(); - if (!token) { - // Save current path and redirect to login - redirectToLogin(); - return; + async function checkAuth() { + let token = getBearerToken(); + if (!token) { + const synced = await ensureTokensFromElectron(); + if (synced) token = getBearerToken(); + } + if (!token) { + redirectToLogin(); + return; + } + queryClient.invalidateQueries({ queryKey: [...USER_QUERY_KEY] }); + setIsCheckingAuth(false); } - queryClient.invalidateQueries({ queryKey: [...USER_QUERY_KEY] }); - setIsCheckingAuth(false); + checkAuth(); }, []); // Return null while loading - the global provider handles the loading UI diff --git a/surfsense_web/app/desktop/suggestion/page.tsx b/surfsense_web/app/desktop/suggestion/page.tsx index 03944867f..097047bb1 100644 --- a/surfsense_web/app/desktop/suggestion/page.tsx +++ b/surfsense_web/app/desktop/suggestion/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useCallback, useEffect, useRef, useState } from "react"; -import { getBearerToken } from "@/lib/auth-utils"; +import { getBearerToken, ensureTokensFromElectron } from "@/lib/auth-utils"; type SSEEvent = | { type: "text-delta"; id: string; delta: string } @@ -65,7 +65,11 @@ export default function SuggestionPage() { setSuggestion(""); setError(null); - const token = getBearerToken(); + let token = getBearerToken(); + if (!token) { + await ensureTokensFromElectron(); + token = getBearerToken(); + } if (!token) { setError(friendlyError("not authenticated")); setIsLoading(false); diff --git a/surfsense_web/lib/auth-utils.ts b/surfsense_web/lib/auth-utils.ts index c2a0d58a5..f7d1c5b09 100644 --- a/surfsense_web/lib/auth-utils.ts +++ b/surfsense_web/lib/auth-utils.ts @@ -87,6 +87,7 @@ export function getBearerToken(): string | null { export function setBearerToken(token: string): void { if (typeof window === "undefined") return; localStorage.setItem(BEARER_TOKEN_KEY, token); + syncTokensToElectron(); } /** @@ -111,6 +112,7 @@ export function getRefreshToken(): string | null { export function setRefreshToken(token: string): void { if (typeof window === "undefined") return; localStorage.setItem(REFRESH_TOKEN_KEY, token); + syncTokensToElectron(); } /** @@ -129,6 +131,44 @@ export function clearAllTokens(): void { clearRefreshToken(); } +/** + * Pushes the current localStorage tokens into the Electron main process + * so that other BrowserWindows (Quick Ask, Autocomplete) can access them. + */ +function syncTokensToElectron(): void { + if (typeof window === "undefined" || !window.electronAPI?.setAuthTokens) return; + const bearer = localStorage.getItem(BEARER_TOKEN_KEY) || ""; + const refresh = localStorage.getItem(REFRESH_TOKEN_KEY) || ""; + if (bearer) { + window.electronAPI.setAuthTokens(bearer, refresh); + } +} + +/** + * Attempts to pull auth tokens from the Electron main process into localStorage. + * Useful for popup windows (Quick Ask, Autocomplete) on platforms where + * localStorage is not reliably shared across BrowserWindow instances. + * Returns true if tokens were found and written to localStorage. + */ +export async function ensureTokensFromElectron(): Promise { + if (typeof window === "undefined" || !window.electronAPI?.getAuthTokens) return false; + if (getBearerToken()) return true; + + try { + const tokens = await window.electronAPI.getAuthTokens(); + if (tokens?.bearer) { + localStorage.setItem(BEARER_TOKEN_KEY, tokens.bearer); + if (tokens.refresh) { + localStorage.setItem(REFRESH_TOKEN_KEY, tokens.refresh); + } + return true; + } + } catch { + // IPC failure — fall through + } + return false; +} + /** * Logout the current user by revoking the refresh token and clearing localStorage. * Returns true if logout was successful (or tokens were cleared), false otherwise. diff --git a/surfsense_web/types/window.d.ts b/surfsense_web/types/window.d.ts index 6feb75463..5e45635a2 100644 --- a/surfsense_web/types/window.d.ts +++ b/surfsense_web/types/window.d.ts @@ -78,6 +78,9 @@ interface ElectronAPI { // Browse files/folders via native dialogs browseFiles: () => Promise; readLocalFiles: (paths: string[]) => Promise; + // Auth token sync across windows + getAuthTokens: () => Promise<{ bearer: string; refresh: string } | null>; + setAuthTokens: (bearer: string, refresh: string) => Promise; } declare global {