diff --git a/app/common/constants.ts b/app/common/constants.ts index c54e8b585..0d28fb68f 100644 --- a/app/common/constants.ts +++ b/app/common/constants.ts @@ -37,6 +37,8 @@ export enum WebviewChannels { } export enum MainChannels { + OPEN_EXTERNAL_WINDOW = 'open-external-window', + // Code GET_CODE_BLOCK = 'get-code-block', GET_CODE_BLOCKS = 'get-code-blocks', @@ -61,6 +63,12 @@ export enum MainChannels { // Ast GET_TEMPLATE_NODE_AST = 'get-template-node-ast', GET_TEMPLATE_NODE_CHILD = 'get-template-node-child', + + // Auth + USER_SIGNED_IN = 'user-signed-in', + USER_SIGNED_OUT = 'user-signed-out', + GET_USER_METADATA = 'get-user-metadata', + SIGN_OUT = 'sign-out', } export enum Links { @@ -72,6 +80,7 @@ export enum Links { } export const APP_NAME = 'Onlook'; +export const APP_SCHEMA = 'onlook'; export const DefaultSettings = { SCALE: 0.6, diff --git a/app/common/models/settings.ts b/app/common/models/settings.ts index 76b72118e..ccc055a84 100644 --- a/app/common/models/settings.ts +++ b/app/common/models/settings.ts @@ -28,3 +28,19 @@ export interface ProjectSettings { frames?: FrameSettings[]; position?: RectPosition; } + +export interface UserMetadata { + id: string; + name?: string; + email?: string; + avatarUrl?: string; +} + +export interface AuthTokens { + accessToken: string; + refreshToken: string; + expiresAt: string; + expiresIn: string; + providerToken: string; + tokenType: string; +} diff --git a/app/src/lib/backend/index.ts b/app/common/supabase.ts similarity index 100% rename from app/src/lib/backend/index.ts rename to app/common/supabase.ts diff --git a/app/electron/main/analytics/index.ts b/app/electron/main/analytics/index.ts index 098e0f7bc..80066c3ec 100644 --- a/app/electron/main/analytics/index.ts +++ b/app/electron/main/analytics/index.ts @@ -17,7 +17,7 @@ class Analytics { } restoreSettings() { - const settings = PersistenStorage.USER_SETTINGS.read(); + const settings = PersistenStorage.USER_SETTINGS.read() || {}; const enable = settings.enableAnalytics; this.id = settings.id; if (!this.id) { @@ -33,7 +33,7 @@ class Analytics { } toggleSetting(enable: boolean) { - const settings = PersistenStorage.USER_SETTINGS.read(); + const settings = PersistenStorage.USER_SETTINGS.read() || {}; if (settings.enableAnalytics === enable) { return; } diff --git a/app/electron/main/auth/index.ts b/app/electron/main/auth/index.ts new file mode 100644 index 000000000..9cf46ab8a --- /dev/null +++ b/app/electron/main/auth/index.ts @@ -0,0 +1,75 @@ +import { User } from '@supabase/supabase-js'; +import { mainWindow } from '..'; +import { PersistenStorage } from '../storage'; +import { APP_SCHEMA, MainChannels } from '/common/constants'; +import { AuthTokens, UserMetadata } from '/common/models/settings'; +import supabase from '/common/supabase'; + +export async function handleAuthCallback(url: string) { + if (!url.startsWith(APP_SCHEMA + '://auth')) { + return; + } + + const authTokens = getToken(url); + PersistenStorage.AUTH_TOKENS.write(authTokens); + + if (!supabase) { + throw new Error('No backend connected'); + } + + const { + data: { user }, + error, + } = await supabase.auth.getUser(authTokens.accessToken); + + if (error) { + throw error; + } + + if (!user) { + throw new Error('No user found'); + } + + const userMetadata = getUserMetadata(user); + PersistenStorage.USER_METADATA.write(userMetadata); + + emitAuthEvent(); +} + +function emitAuthEvent() { + mainWindow?.webContents.send(MainChannels.USER_SIGNED_IN); +} + +function getToken(url: string): AuthTokens { + const fragmentParams = new URLSearchParams(url.split('#')[1]); + + const accessToken = fragmentParams.get('access_token'); + const refreshToken = fragmentParams.get('refresh_token'); + const expiresAt = fragmentParams.get('expires_at'); + const expiresIn = fragmentParams.get('expires_in'); + const providerToken = fragmentParams.get('provider_token'); + const tokenType = fragmentParams.get('token_type'); + + if (!accessToken || !refreshToken || !expiresAt || !expiresIn || !providerToken || !tokenType) { + throw new Error('Invalid token'); + } + + return { + accessToken, + refreshToken, + expiresAt, + expiresIn, + providerToken, + tokenType, + }; +} + +function getUserMetadata(user: User): UserMetadata { + const userMetadata: UserMetadata = { + id: user.id, + email: user.email, + name: user.user_metadata.full_name, + avatarUrl: user.user_metadata.avatar_url, + }; + return userMetadata; +} diff --git a/app/electron/main/code/index.ts b/app/electron/main/code/index.ts index 1fe80eb8d..568cccf54 100644 --- a/app/electron/main/code/index.ts +++ b/app/electron/main/code/index.ts @@ -69,7 +69,7 @@ export async function writeCode(codeDiffs: CodeDiff[]): Promise { } function getIdeFromUserSettings(): IDE { - const userSettings = PersistenStorage.USER_SETTINGS.read(); + const userSettings = PersistenStorage.USER_SETTINGS.read() || {}; return IDE.fromType(userSettings.ideType || IdeType.VS_CODE); } diff --git a/app/electron/main/events/auth.ts b/app/electron/main/events/auth.ts new file mode 100644 index 000000000..1707fe77d --- /dev/null +++ b/app/electron/main/events/auth.ts @@ -0,0 +1,13 @@ +import { ipcMain } from 'electron'; +import { mainWindow } from '..'; +import { PersistenStorage } from '../storage'; +import { MainChannels } from '/common/constants'; + +export function listenForAuthMessages() { + ipcMain.handle(MainChannels.SIGN_OUT, (e: Electron.IpcMainInvokeEvent, args) => { + PersistenStorage.USER_METADATA.clear(); + PersistenStorage.AUTH_TOKENS.clear(); + + mainWindow?.webContents.send(MainChannels.USER_SIGNED_OUT); + }); +} diff --git a/app/electron/main/events/code.ts b/app/electron/main/events/code.ts index 463876650..53a2b5df0 100644 --- a/app/electron/main/events/code.ts +++ b/app/electron/main/events/code.ts @@ -1,11 +1,11 @@ import { ipcMain } from 'electron'; import { openInIde, pickDirectory, readCodeBlock, readCodeBlocks, writeCode } from '../code/'; +import { extractComponentsFromDirectory } from '../code/components'; import { getCodeDiffs } from '../code/diff'; import { getTemplateNodeChild } from '../code/templateNode'; import { MainChannels } from '/common/constants'; import { CodeDiff, CodeDiffRequest } from '/common/models/code'; import { TemplateNode } from '/common/models/element/templateNode'; -import { extractComponentsFromDirectory } from '../code/components'; export function listenForCodeMessages() { ipcMain.handle(MainChannels.VIEW_SOURCE_CODE, (e: Electron.IpcMainInvokeEvent, args) => { diff --git a/app/electron/main/events/index.ts b/app/electron/main/events/index.ts index 1154cf732..62d4e86ea 100644 --- a/app/electron/main/events/index.ts +++ b/app/electron/main/events/index.ts @@ -1,11 +1,25 @@ +import { ipcMain, shell } from 'electron'; import { listenForAnalyticsMessages } from './analytics'; +import { listenForAuthMessages } from './auth'; import { listenForCodeMessages } from './code'; -import { listenForSettingMessages } from './settings'; +import { listenForStorageMessages } from './storage'; import { listenForTunnelMessages } from './tunnel'; +import { MainChannels } from '/common/constants'; export function listenForIpcMessages() { + listenForGeneralMessages(); listenForTunnelMessages(); listenForAnalyticsMessages(); listenForCodeMessages(); - listenForSettingMessages(); + listenForStorageMessages(); + listenForAuthMessages(); +} + +function listenForGeneralMessages() { + ipcMain.handle( + MainChannels.OPEN_EXTERNAL_WINDOW, + (e: Electron.IpcMainInvokeEvent, args: string) => { + return shell.openExternal(args); + }, + ); } diff --git a/app/electron/main/events/settings.ts b/app/electron/main/events/settings.ts deleted file mode 100644 index 0f502432b..000000000 --- a/app/electron/main/events/settings.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ipcMain } from 'electron'; -import { PersistenStorage } from '../storage'; -import { MainChannels } from '/common/constants'; - -export function listenForSettingMessages() { - ipcMain.handle(MainChannels.GET_USER_SETTINGS, (e: Electron.IpcMainInvokeEvent) => { - return PersistenStorage.USER_SETTINGS.read(); - }); - - ipcMain.handle(MainChannels.UPDATE_USER_SETTINGS, (e: Electron.IpcMainInvokeEvent, args) => { - PersistenStorage.USER_SETTINGS.update(args); - }); - - ipcMain.handle(MainChannels.GET_PROJECT_SETTINGS, (e: Electron.IpcMainInvokeEvent) => { - return PersistenStorage.PROJECT_SETTINGS.read(); - }); - - ipcMain.handle(MainChannels.UPDATE_PROJECT_SETTINGS, (e: Electron.IpcMainInvokeEvent, args) => { - PersistenStorage.PROJECT_SETTINGS.update(args); - }); -} diff --git a/app/electron/main/events/storage.ts b/app/electron/main/events/storage.ts new file mode 100644 index 000000000..277d6a7bf --- /dev/null +++ b/app/electron/main/events/storage.ts @@ -0,0 +1,32 @@ +import { ipcMain } from 'electron'; +import { PersistenStorage } from '../storage'; +import { MainChannels } from '/common/constants'; +import { ProjectSettings, UserSettings } from '/common/models/settings'; + +export function listenForStorageMessages() { + ipcMain.handle(MainChannels.GET_USER_SETTINGS, (e: Electron.IpcMainInvokeEvent) => { + return PersistenStorage.USER_SETTINGS.read(); + }); + + ipcMain.handle( + MainChannels.UPDATE_USER_SETTINGS, + (e: Electron.IpcMainInvokeEvent, args: UserSettings) => { + PersistenStorage.USER_SETTINGS.update(args); + }, + ); + + ipcMain.handle(MainChannels.GET_PROJECT_SETTINGS, (e: Electron.IpcMainInvokeEvent) => { + return PersistenStorage.PROJECT_SETTINGS.read(); + }); + + ipcMain.handle( + MainChannels.UPDATE_PROJECT_SETTINGS, + (e: Electron.IpcMainInvokeEvent, args: ProjectSettings) => { + PersistenStorage.PROJECT_SETTINGS.update(args); + }, + ); + + ipcMain.handle(MainChannels.GET_USER_METADATA, (e: Electron.IpcMainInvokeEvent) => { + return PersistenStorage.USER_METADATA.read(); + }); +} diff --git a/app/electron/main/index.ts b/app/electron/main/index.ts index afab09561..dedb68c89 100644 --- a/app/electron/main/index.ts +++ b/app/electron/main/index.ts @@ -4,117 +4,138 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { sendAnalytics } from './analytics'; +import { handleAuthCallback } from './auth'; import { listenForIpcMessages } from './events'; import AutoUpdateManager from './update'; -import { APP_NAME } from '/common/constants'; +import { APP_NAME, APP_SCHEMA } from '/common/constants'; const require = createRequire(import.meta.url); const __dirname = path.dirname(fileURLToPath(import.meta.url)); -process.env.APP_ROOT = path.join(__dirname, '../..'); -process.env.WEBVIEW_PRELOAD_PATH = path.join(__dirname, '../preload/webview.js'); -process.env.APP_VERSION = app.getVersion(); - -export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron'); -export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist'); -export const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL; -process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL - ? path.join(process.env.APP_ROOT, 'public') - : RENDERER_DIST; - -// Disable GPU Acceleration for Windows 7 -if (os.release().startsWith('6.1')) { - app.disableHardwareAcceleration(); -} - -// Set application name for Windows 10+ notifications -if (process.platform === 'win32') { - app.setAppUserModelId(app.getName()); -} - -if (!app.requestSingleInstanceLock()) { - app.quit(); - process.exit(0); -} - -let win: BrowserWindow | null = null; -const preload = path.join(__dirname, '../preload/index.js'); -const indexHtml = path.join(RENDERER_DIST, 'index.html'); - -function loadWindowContent(win: BrowserWindow) { - // Load URL or file based on the environment - if (VITE_DEV_SERVER_URL) { - win.loadURL(VITE_DEV_SERVER_URL); +// Constants +const MAIN_DIST = path.join(__dirname, '../../dist-electron'); +const RENDERER_DIST = path.join(__dirname, '../../dist'); +const PRELOAD_PATH = path.join(__dirname, '../preload/index.js'); +const INDEX_HTML = path.join(RENDERER_DIST, 'index.html'); +const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL; + +// Environment setup +const setupEnvironment = () => { + process.env.APP_ROOT = path.join(__dirname, '../..'); + process.env.WEBVIEW_PRELOAD_PATH = path.join(__dirname, '../preload/webview.js'); + process.env.APP_VERSION = app.getVersion(); + process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL + ? path.join(process.env.APP_ROOT, 'public') + : RENDERER_DIST; +}; + +// Platform-specific configurations +const configurePlatformSpecifics = () => { + if (os.release().startsWith('6.1')) { + app.disableHardwareAcceleration(); + } + + if (process.platform === 'win32') { + app.setAppUserModelId(app.getName()); + } +}; + +// Protocol setup +const setupProtocol = () => { + if (process.defaultApp && process.argv.length >= 2) { + app.setAsDefaultProtocolClient(APP_SCHEMA, process.execPath, [ + path.resolve(process.argv[1]), + ]); } else { - win.loadFile(indexHtml); + app.setAsDefaultProtocolClient(APP_SCHEMA); } -} +}; -function createWindow() { - win = new BrowserWindow({ +// Window management +export let mainWindow: BrowserWindow | null = null; + +const createWindow = () => { + mainWindow = new BrowserWindow({ title: APP_NAME, icon: path.join(process.env.VITE_PUBLIC, 'favicon.ico'), titleBarStyle: 'hiddenInset', webPreferences: { - preload, + preload: PRELOAD_PATH, webviewTag: true, }, }); - return win; -} + return mainWindow; +}; + +const loadWindowContent = (win: BrowserWindow) => { + VITE_DEV_SERVER_URL ? win.loadURL(VITE_DEV_SERVER_URL) : win.loadFile(INDEX_HTML); +}; -function initMainWindow() { +const initMainWindow = () => { const win = createWindow(); win.maximize(); loadWindowContent(win); - // Ensure links open externally win.webContents.setWindowOpenHandler(({ url }) => { if (url.startsWith('https:')) { shell.openExternal(url); } return { action: 'deny' }; }); -} +}; -function listenForAppEvents() { +// Event listeners +const setupAppEventListeners = () => { app.whenReady().then(initMainWindow); app.on('ready', () => { - const updateManager = new AutoUpdateManager(); + new AutoUpdateManager(); sendAnalytics('start app'); }); app.on('window-all-closed', () => { - win = null; + mainWindow = null; if (process.platform !== 'darwin') { app.quit(); } }); app.on('second-instance', () => { - if (win) { - // Focus on the main window if the user tried to open another - if (win.isMinimized()) { - win.restore(); + if (mainWindow) { + if (mainWindow.isMinimized()) { + mainWindow.restore(); } - win.focus(); + mainWindow.focus(); } }); app.on('activate', () => { - const allWindows = BrowserWindow.getAllWindows(); - if (allWindows.length) { - allWindows[0].focus(); - } else { - initMainWindow(); - } + BrowserWindow.getAllWindows().length + ? BrowserWindow.getAllWindows()[0].focus() + : initMainWindow(); }); - app.on('quit', () => { - sendAnalytics('quit app'); + app.on('open-url', (event, url) => { + event.preventDefault(); + handleAuthCallback(url); }); -} -listenForAppEvents(); -listenForIpcMessages(); + app.on('quit', () => sendAnalytics('quit app')); +}; + +// Main function +const main = () => { + setupEnvironment(); + configurePlatformSpecifics(); + + if (!app.requestSingleInstanceLock()) { + app.quit(); + process.exit(0); + } + + setupProtocol(); + setupAppEventListeners(); + listenForIpcMessages(); +}; + +main(); diff --git a/app/electron/main/storage/index.ts b/app/electron/main/storage/index.ts index 756914f50..3c218d79e 100644 --- a/app/electron/main/storage/index.ts +++ b/app/electron/main/storage/index.ts @@ -1,33 +1,105 @@ -import { app } from 'electron'; +import { UserMetadata } from '@supabase/supabase-js'; +import { app, safeStorage } from 'electron'; import { existsSync, readFileSync, writeFileSync } from 'fs'; -import { ProjectSettings, UserSettings } from '/common/models/settings'; +import { AuthTokens, ProjectSettings, UserSettings } from '/common/models/settings'; export class PersistenStorage { public readonly FILE_PATH: string; static readonly USER_SETTINGS = new PersistenStorage('user-settings'); static readonly PROJECT_SETTINGS = new PersistenStorage('project-settings'); + static readonly USER_METADATA = new PersistenStorage('user-metadata'); + static readonly AUTH_TOKENS = new PersistenStorage('auth-tokens', true); - private constructor(public readonly fileName: string) { + private constructor( + public readonly fileName: string, + public readonly encrypted = false, + ) { const APP_PATH = app.getPath('userData'); this.FILE_PATH = `${APP_PATH}/${fileName}.json`; } - read(): T { - if (!existsSync(this.FILE_PATH)) { - return {} as T; + read(): T | null { + try { + return this.encrypted ? this.readEncrypted() : this.readUnencrypted(); + } catch (e) { + console.error(`Error reading file ${this.FILE_PATH}: `, e); + return null; } - - const content = readFileSync(this.FILE_PATH, 'utf8'); - return JSON.parse(content || '') as T; } write(value: T) { + try { + this.encrypted ? this.writeEncrypted(value) : this.writeUnencrypted(value); + } catch (e) { + console.error(`Error writing file ${this.FILE_PATH}: `, e); + return null; + } + } + + update(value: T) { + try { + this.encrypted ? this.updateEncrypted(value) : this.updateUnencrypted(value); + } catch (e) { + console.error(`Error updating file ${this.FILE_PATH}: `, e); + return null; + } + } + + clear() { + try { + writeFileSync(this.FILE_PATH, ''); + } catch (e) { + console.error(`Error clearing file ${this.FILE_PATH}: `, e); + return null; + } + } + + private readUnencrypted(): T | null { + if (!existsSync(this.FILE_PATH)) { + return null; + } + const data = readFileSync(this.FILE_PATH, 'utf8'); + return data ? (JSON.parse(data) as T) : null; + } + + private writeUnencrypted(value: T) { const data = JSON.stringify(value); writeFileSync(this.FILE_PATH, data); } - update(value: T) { - const existingValue = this.read(); - this.write({ ...existingValue, ...value }); + private updateUnencrypted(value: T) { + const existingValue = this.readUnencrypted(); + this.writeUnencrypted({ ...(existingValue ?? {}), ...value }); + } + + private readEncrypted(): T | null { + if (!existsSync(this.FILE_PATH)) { + return null; + } + const base64EncryptedData = readFileSync(this.FILE_PATH, 'utf8'); + const encryptedBuffer = Buffer.from(base64EncryptedData, 'base64'); + const data = safeStorage.decryptString(encryptedBuffer); + return data ? (JSON.parse(data) as T) : null; + } + + private writeEncrypted(value: T) { + const base64EncryptedData = this.encryptToBase64(value); + writeFileSync(this.FILE_PATH, base64EncryptedData); + } + + private updateEncrypted(value: T) { + const existingValue = this.readEncrypted(); + if (!existingValue) { + this.writeEncrypted(value); + return; + } + this.writeEncrypted({ ...(existingValue ?? {}), ...value }); + } + + private encryptToBase64(value: T): string { + const data = JSON.stringify(value); + const encryptedData = safeStorage.encryptString(data); + const base64EncryptedData = encryptedData.toString('base64'); + return base64EncryptedData; } } diff --git a/app/src/App.tsx b/app/src/App.tsx index 8cd0ecc9f..dee813143 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -1,9 +1,15 @@ import { TooltipProvider } from '@/components/ui/tooltip'; +import { createContext, useContext } from 'react'; import Announcement from './components/Announcement'; import AppBar from './components/AppBar'; import { ThemeProvider } from './components/theme-provider'; import { Toaster } from './components/ui/toaster'; +import { AuthManager } from './lib/auth'; import ProjectEditor from './routes/project'; + +const AuthContext = createContext(new AuthManager()); +export const useAuthManager = () => useContext(AuthContext); + function App() { return ( diff --git a/app/src/components/Announcement/auth.tsx b/app/src/components/Announcement/auth.tsx new file mode 100644 index 000000000..15f502da1 --- /dev/null +++ b/app/src/components/Announcement/auth.tsx @@ -0,0 +1,37 @@ +import { useAuthManager } from '@/App'; +import { observer } from 'mobx-react-lite'; +import { useEffect, useState } from 'react'; +import { Button } from '../ui/button'; + +const Auth = observer(() => { + const authManager = useAuthManager(); + const [userMetadata, setUserMetadata] = useState(authManager.userMetadata); + + useEffect(() => { + setUserMetadata(authManager.userMetadata); + }, [authManager.userMetadata]); + + return ( +
+ {userMetadata ? ( + <> +
Hello, {userMetadata.name}
+ + + ) : ( + <> + + + + )} +
+ ); +}); + +export default Auth; diff --git a/app/src/components/Announcement/index.tsx b/app/src/components/Announcement/index.tsx index b08fcfeb5..8c4b7d3f6 100644 --- a/app/src/components/Announcement/index.tsx +++ b/app/src/components/Announcement/index.tsx @@ -1,6 +1,5 @@ import mountains from '@/assets/mountains.png'; import wordLogo from '@/assets/word-logo.svg'; -import supabase from '@/lib/backend'; import { DialogTitle } from '@radix-ui/react-dialog'; import { BoxIcon, @@ -17,6 +16,7 @@ import { Toggle } from '../ui/toggle'; import { toast } from '../ui/use-toast'; import { Links, MainChannels } from '/common/constants'; import { UserSettings } from '/common/models/settings'; +import supabase from '/common/supabase'; function Announcement() { const [checked, setChecked] = useState(true); diff --git a/app/src/lib/auth/index.ts b/app/src/lib/auth/index.ts new file mode 100644 index 000000000..79bee6e24 --- /dev/null +++ b/app/src/lib/auth/index.ts @@ -0,0 +1,63 @@ +import { makeAutoObservable } from 'mobx'; +import { APP_SCHEMA, MainChannels } from '/common/constants'; +import { UserMetadata } from '/common/models/settings'; +import supabase from '/common/supabase'; + +export class AuthManager { + authenticated = false; + userMetadata: UserMetadata | null = null; + + constructor() { + makeAutoObservable(this); + this.fetchUserMetadata(); + this.listenForAuthEvents(); + } + + async fetchUserMetadata() { + this.userMetadata = await window.api.invoke(MainChannels.GET_USER_METADATA); + } + + listenForAuthEvents() { + window.api.on(MainChannels.USER_SIGNED_IN, async (e, args) => { + this.authenticated = true; + this.fetchUserMetadata(); + }); + + window.api.on(MainChannels.USER_SIGNED_OUT, async (e, args) => { + this.authenticated = false; + this.userMetadata = null; + }); + } + + async signIn(provider: 'github' | 'google') { + if (!supabase) { + throw new Error('No backend connected'); + } + + supabase.auth.signOut(); + + const { data, error } = await supabase.auth.signInWithOAuth({ + provider, + options: { + skipBrowserRedirect: true, + redirectTo: APP_SCHEMA + '://auth', + }, + }); + + if (error) { + console.error('Authentication error:', error); + return; + } + + window.api.invoke(MainChannels.OPEN_EXTERNAL_WINDOW, data.url); + } + + signOut() { + if (!supabase) { + throw new Error('No backend connected'); + } + + supabase.auth.signOut(); + window.api.invoke(MainChannels.SIGN_OUT); + } +}