From 2fd7551d36eb1e889936a58811601070bd7fc335 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Wed, 24 Jun 2026 03:55:39 +0530 Subject: [PATCH] fix(desktop):add auth cutover ipc --- surfsense_desktop/src/ipc/channels.ts | 3 +- surfsense_desktop/src/ipc/handlers.ts | 51 +++++++++++++++---- surfsense_desktop/src/modules/auth-cutover.ts | 30 +++++++++++ surfsense_desktop/src/preload.ts | 5 +- 4 files changed, 73 insertions(+), 16 deletions(-) create mode 100644 surfsense_desktop/src/modules/auth-cutover.ts diff --git a/surfsense_desktop/src/ipc/channels.ts b/surfsense_desktop/src/ipc/channels.ts index 43bdce312..436e0e064 100644 --- a/surfsense_desktop/src/ipc/channels.ts +++ b/surfsense_desktop/src/ipc/channels.ts @@ -40,13 +40,12 @@ export const IPC_CHANNELS = { READ_AGENT_LOCAL_FILE_TEXT: 'agent-filesystem:read-local-file-text', WRITE_AGENT_LOCAL_FILE_TEXT: 'agent-filesystem:write-local-file-text', // Auth token sync across windows - GET_AUTH_TOKENS: 'auth:get-tokens', - SET_AUTH_TOKENS: 'auth:set-tokens', GET_ACCESS_TOKEN: 'auth:get-access-token', REFRESH_ACCESS_TOKEN: 'auth:refresh-access-token', LOGOUT: 'auth:logout', AUTH_CHANGED: 'auth:changed', AUTH_START_GOOGLE: 'auth:start-google', + AUTH_LOGIN_PASSWORD: 'auth:login-password', // Keyboard shortcut configuration GET_SHORTCUTS: 'shortcuts:get', SET_SHORTCUTS: 'shortcuts:set', diff --git a/surfsense_desktop/src/ipc/handlers.ts b/surfsense_desktop/src/ipc/handlers.ts index 4ca98a9e6..ab4ba0d92 100644 --- a/surfsense_desktop/src/ipc/handlers.ts +++ b/surfsense_desktop/src/ipc/handlers.ts @@ -59,6 +59,11 @@ const REFRESH_TOKEN_KEY = 'surfsense_refresh_token'; let accessToken: string | null = null; let refreshInFlight: Promise | null = null; +type DesktopAuthResponse = { + access_token?: string; + refresh_token?: string | null; +}; + function getBackendUrl(): string { return (process.env.HOSTED_BACKEND_URL || process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || '').replace( /\/+$/, @@ -224,17 +229,6 @@ export function registerIpcHandlers(): void { } ); - ipcMain.handle(IPC_CHANNELS.SET_AUTH_TOKENS, async (_event, tokens: { bearer: string; refresh: string }) => { - await storeTokens(tokens); - }); - - ipcMain.handle(IPC_CHANNELS.GET_AUTH_TOKENS, async () => { - if (!accessToken) { - await refreshAccessToken(); - } - return accessToken ? { bearer: accessToken, refresh: '' } : null; - }); - ipcMain.handle(IPC_CHANNELS.GET_ACCESS_TOKEN, async () => { if (!accessToken) { await refreshAccessToken(); @@ -275,6 +269,41 @@ export function registerIpcHandlers(): void { return { ok: true }; }); + ipcMain.handle( + IPC_CHANNELS.AUTH_LOGIN_PASSWORD, + async (_event, payload: { email: string; password: string }) => { + const backendUrl = getBackendUrl(); + if (!backendUrl) { + throw new Error('Backend URL is not configured'); + } + + const response = await fetch(`${backendUrl}/auth/desktop/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + let detail = 'Password login failed'; + try { + const error = (await response.json()) as { detail?: string }; + detail = error.detail || detail; + } catch { + // Keep the generic error if the backend did not return JSON. + } + throw new Error(detail); + } + + const tokens = (await response.json()) as DesktopAuthResponse; + if (!tokens.access_token || !tokens.refresh_token) { + throw new Error('Password login did not return desktop tokens'); + } + + await storeTokens({ bearer: tokens.access_token, refresh: tokens.refresh_token }); + return { ok: true }; + } + ); + ipcMain.handle(IPC_CHANNELS.GET_SHORTCUTS, () => getShortcuts()); ipcMain.handle(IPC_CHANNELS.GET_AUTO_LAUNCH, () => getAutoLaunchState()); diff --git a/surfsense_desktop/src/modules/auth-cutover.ts b/surfsense_desktop/src/modules/auth-cutover.ts new file mode 100644 index 000000000..373865dbe --- /dev/null +++ b/surfsense_desktop/src/modules/auth-cutover.ts @@ -0,0 +1,30 @@ +import { app } from 'electron'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { secretStore } from './secret-store'; + +const CUTOVER_FLAG_FILE = 'auth-cutover-v1.json'; +const REFRESH_TOKEN_KEY = 'surfsense_refresh_token'; + +async function hasCompletedCutover(flagPath: string): Promise { + try { + const raw = await readFile(flagPath, 'utf8'); + return JSON.parse(raw)?.complete === true; + } catch { + return false; + } +} + +export async function purgeLegacyAuthCutover(): Promise { + const userDataPath = app.getPath('userData'); + const flagPath = path.join(userDataPath, CUTOVER_FLAG_FILE); + if (await hasCompletedCutover(flagPath)) return; + + await secretStore.clear(REFRESH_TOKEN_KEY); + await mkdir(userDataPath, { recursive: true }); + await writeFile( + flagPath, + JSON.stringify({ complete: true, completedAt: new Date().toISOString() }), + { mode: 0o600 } + ); +} diff --git a/surfsense_desktop/src/preload.ts b/surfsense_desktop/src/preload.ts index 548610f03..07f363a59 100644 --- a/surfsense_desktop/src/preload.ts +++ b/surfsense_desktop/src/preload.ts @@ -80,13 +80,12 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.invoke(IPC_CHANNELS.WRITE_AGENT_LOCAL_FILE_TEXT, virtualPath, content, searchSpaceId), // 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 }), getAccessToken: () => ipcRenderer.invoke(IPC_CHANNELS.GET_ACCESS_TOKEN), refreshAccessToken: () => ipcRenderer.invoke(IPC_CHANNELS.REFRESH_ACCESS_TOKEN), logout: () => ipcRenderer.invoke(IPC_CHANNELS.LOGOUT), startGoogleOAuth: () => ipcRenderer.invoke(IPC_CHANNELS.AUTH_START_GOOGLE), + loginPassword: (email: string, password: string) => + ipcRenderer.invoke(IPC_CHANNELS.AUTH_LOGIN_PASSWORD, { email, password }), onAuthChanged: (callback: (payload: { authed: boolean; accessToken: string | null }) => void) => { const listener = (_event: Electron.IpcRendererEvent, payload: { authed: boolean; accessToken: string | null }) => callback(payload);