fix(desktop):add auth cutover ipc

This commit is contained in:
Anish Sarkar 2026-06-24 03:55:39 +05:30
parent 766fa25ea0
commit 2fd7551d36
4 changed files with 73 additions and 16 deletions

View file

@ -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',

View file

@ -59,6 +59,11 @@ const REFRESH_TOKEN_KEY = 'surfsense_refresh_token';
let accessToken: string | null = null;
let refreshInFlight: Promise<string | null> | 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());

View file

@ -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<boolean> {
try {
const raw = await readFile(flagPath, 'utf8');
return JSON.parse(raw)?.complete === true;
} catch {
return false;
}
}
export async function purgeLegacyAuthCutover(): Promise<void> {
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 }
);
}

View file

@ -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);