mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-22 23:31:12 +02:00
feat: implement auth token synchronization between Electron and web app
- Added IPC channels for getting and setting auth tokens in Electron. - Implemented functions to sync tokens from localStorage to Electron and vice versa. - Updated components to ensure tokens are retrieved from Electron when not available locally. - Enhanced user authentication flow by integrating token management across windows.
This commit is contained in:
parent
eb5799336c
commit
e920923fa4
8 changed files with 82 additions and 12 deletions
|
|
@ -58,7 +58,7 @@ win:
|
||||||
icon: assets/icon.ico
|
icon: assets/icon.ico
|
||||||
target:
|
target:
|
||||||
- target: nsis
|
- target: nsis
|
||||||
arch: [x64, arm64]
|
arch: [x64]
|
||||||
nsis:
|
nsis:
|
||||||
oneClick: false
|
oneClick: false
|
||||||
perMachine: false
|
perMachine: false
|
||||||
|
|
|
||||||
|
|
@ -32,4 +32,7 @@ export const IPC_CHANNELS = {
|
||||||
FOLDER_SYNC_ACK_EVENTS: 'folder-sync:ack-events',
|
FOLDER_SYNC_ACK_EVENTS: 'folder-sync:ack-events',
|
||||||
BROWSE_FILES: 'browse:files',
|
BROWSE_FILES: 'browse:files',
|
||||||
READ_LOCAL_FILES: 'browse:read-local-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;
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ import {
|
||||||
readLocalFiles,
|
readLocalFiles,
|
||||||
} from '../modules/folder-watcher';
|
} from '../modules/folder-watcher';
|
||||||
|
|
||||||
|
let authTokens: { bearer: string; refresh: string } | null = null;
|
||||||
|
|
||||||
export function registerIpcHandlers(): void {
|
export function registerIpcHandlers(): void {
|
||||||
ipcMain.on(IPC_CHANNELS.OPEN_EXTERNAL, (_event, url: string) => {
|
ipcMain.on(IPC_CHANNELS.OPEN_EXTERNAL, (_event, url: string) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -89,4 +91,12 @@ export function registerIpcHandlers(): void {
|
||||||
ipcMain.handle(IPC_CHANNELS.READ_LOCAL_FILES, (_event, paths: string[]) =>
|
ipcMain.handle(IPC_CHANNELS.READ_LOCAL_FILES, (_event, paths: string[]) =>
|
||||||
readLocalFiles(paths)
|
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;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,4 +68,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
// Browse files via native dialog
|
// Browse files via native dialog
|
||||||
browseFiles: () => ipcRenderer.invoke(IPC_CHANNELS.BROWSE_FILES),
|
browseFiles: () => ipcRenderer.invoke(IPC_CHANNELS.BROWSE_FILES),
|
||||||
readLocalFiles: (paths: string[]) => ipcRenderer.invoke(IPC_CHANNELS.READ_LOCAL_FILES, paths),
|
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 }),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { USER_QUERY_KEY } from "@/atoms/user/user-query.atoms";
|
import { USER_QUERY_KEY } from "@/atoms/user/user-query.atoms";
|
||||||
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
|
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";
|
import { queryClient } from "@/lib/query-client/client";
|
||||||
|
|
||||||
interface DashboardLayoutProps {
|
interface DashboardLayoutProps {
|
||||||
|
|
@ -17,15 +17,20 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||||
useGlobalLoadingEffect(isCheckingAuth);
|
useGlobalLoadingEffect(isCheckingAuth);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check if user is authenticated
|
async function checkAuth() {
|
||||||
const token = getBearerToken();
|
let token = getBearerToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
// Save current path and redirect to login
|
const synced = await ensureTokensFromElectron();
|
||||||
redirectToLogin();
|
if (synced) token = getBearerToken();
|
||||||
return;
|
}
|
||||||
|
if (!token) {
|
||||||
|
redirectToLogin();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queryClient.invalidateQueries({ queryKey: [...USER_QUERY_KEY] });
|
||||||
|
setIsCheckingAuth(false);
|
||||||
}
|
}
|
||||||
queryClient.invalidateQueries({ queryKey: [...USER_QUERY_KEY] });
|
checkAuth();
|
||||||
setIsCheckingAuth(false);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Return null while loading - the global provider handles the loading UI
|
// Return null while loading - the global provider handles the loading UI
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { getBearerToken } from "@/lib/auth-utils";
|
import { getBearerToken, ensureTokensFromElectron } from "@/lib/auth-utils";
|
||||||
|
|
||||||
type SSEEvent =
|
type SSEEvent =
|
||||||
| { type: "text-delta"; id: string; delta: string }
|
| { type: "text-delta"; id: string; delta: string }
|
||||||
|
|
@ -65,7 +65,11 @@ export default function SuggestionPage() {
|
||||||
setSuggestion("");
|
setSuggestion("");
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const token = getBearerToken();
|
let token = getBearerToken();
|
||||||
|
if (!token) {
|
||||||
|
await ensureTokensFromElectron();
|
||||||
|
token = getBearerToken();
|
||||||
|
}
|
||||||
if (!token) {
|
if (!token) {
|
||||||
setError(friendlyError("not authenticated"));
|
setError(friendlyError("not authenticated"));
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ export function getBearerToken(): string | null {
|
||||||
export function setBearerToken(token: string): void {
|
export function setBearerToken(token: string): void {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
localStorage.setItem(BEARER_TOKEN_KEY, token);
|
localStorage.setItem(BEARER_TOKEN_KEY, token);
|
||||||
|
syncTokensToElectron();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -111,6 +112,7 @@ export function getRefreshToken(): string | null {
|
||||||
export function setRefreshToken(token: string): void {
|
export function setRefreshToken(token: string): void {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
localStorage.setItem(REFRESH_TOKEN_KEY, token);
|
localStorage.setItem(REFRESH_TOKEN_KEY, token);
|
||||||
|
syncTokensToElectron();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -129,6 +131,44 @@ export function clearAllTokens(): void {
|
||||||
clearRefreshToken();
|
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<boolean> {
|
||||||
|
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.
|
* Logout the current user by revoking the refresh token and clearing localStorage.
|
||||||
* Returns true if logout was successful (or tokens were cleared), false otherwise.
|
* Returns true if logout was successful (or tokens were cleared), false otherwise.
|
||||||
|
|
|
||||||
3
surfsense_web/types/window.d.ts
vendored
3
surfsense_web/types/window.d.ts
vendored
|
|
@ -78,6 +78,9 @@ interface ElectronAPI {
|
||||||
// Browse files/folders via native dialogs
|
// Browse files/folders via native dialogs
|
||||||
browseFiles: () => Promise<string[] | null>;
|
browseFiles: () => Promise<string[] | null>;
|
||||||
readLocalFiles: (paths: string[]) => Promise<LocalFileData[]>;
|
readLocalFiles: (paths: string[]) => Promise<LocalFileData[]>;
|
||||||
|
// Auth token sync across windows
|
||||||
|
getAuthTokens: () => Promise<{ bearer: string; refresh: string } | null>;
|
||||||
|
setAuthTokens: (bearer: string, refresh: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue