diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 1e5d95382..a0ddeb962 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -60,6 +60,7 @@ export const TEST_SETTINGS_FILE_KEY = 'playwright-test-settings' export const DEFAULT_HOST = 'https://api.zoo.dev' export const SETTINGS_FILE_NAME = 'settings.toml' +export const TOKEN_FILE_NAME = 'token.txt' export const PROJECT_SETTINGS_FILE_NAME = 'project.toml' export const COOKIE_NAME = '__Secure-next-auth.session-token' diff --git a/src/lib/desktop.ts b/src/lib/desktop.ts index e5a1cf6d2..e38113438 100644 --- a/src/lib/desktop.ts +++ b/src/lib/desktop.ts @@ -13,6 +13,7 @@ import { PROJECT_FOLDER, PROJECT_SETTINGS_FILE_NAME, SETTINGS_FILE_NAME, + TOKEN_FILE_NAME, } from './constants' import { DeepPartial } from './types' import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration' @@ -396,6 +397,23 @@ const getAppSettingsFilePath = async () => { } return window.electron.path.join(fullPath, SETTINGS_FILE_NAME) } +const getTokenFilePath = async () => { + const isTestEnv = window.electron.process.env.IS_PLAYWRIGHT === 'true' + const testSettingsPath = window.electron.process.env.TEST_SETTINGS_FILE_KEY + const appConfig = await window.electron.getPath('appData') + const fullPath = isTestEnv + ? testSettingsPath + : window.electron.path.join(appConfig, getAppFolderName()) + try { + await window.electron.stat(fullPath) + } catch (e) { + // File/path doesn't exist + if (e === 'ENOENT') { + await window.electron.mkdir(fullPath, { recursive: true }) + } + } + return window.electron.path.join(fullPath, TOKEN_FILE_NAME) +} const getProjectSettingsFilePath = async (projectPath: string) => { try { @@ -475,6 +493,34 @@ export const writeAppSettingsFile = async (tomlStr: string) => { return window.electron.writeFile(appSettingsFilePath, tomlStr) } +export const readTokenFile = async () => { + let settingsPath = await getTokenFilePath() + + if (window.electron.exists(settingsPath)) { + const token: string = await window.electron.readFile(settingsPath) + if (!token) return '' + + return token + } + return '' +} + +export const writeTokenFile = async (token: string) => { + const tokenFilePath = await getTokenFilePath() + if (err(token)) return Promise.reject(token) + return window.electron.writeFile(tokenFilePath, token) +} + +let appStateStore: Project | undefined = undefined + +export const getState = async (): Promise => { + return Promise.resolve(appStateStore) +} + +export const setState = async (state: Project | undefined): Promise => { + appStateStore = state +} + export const getUser = async ( token: string, hostname: string diff --git a/src/machines/authMachine.ts b/src/machines/authMachine.ts index 887dc1a05..cae5b4b54 100644 --- a/src/machines/authMachine.ts +++ b/src/machines/authMachine.ts @@ -8,7 +8,11 @@ import { VITE_KC_SKIP_AUTH, DEV, } from 'env' -import { getUser as getUserDesktop } from 'lib/desktop' +import { + getUser as getUserDesktop, + readTokenFile, + writeTokenFile, +} from 'lib/desktop' import { COOKIE_NAME } from 'lib/constants' const SKIP_AUTH = VITE_KC_SKIP_AUTH === 'true' && DEV @@ -53,6 +57,7 @@ const persistedToken = export const authMachine = createMachine( { + /** @xstate-layout N4IgpgJg5mDOIC5QEECuAXAFgOgMabFwGsBJAMwBkB7KGCEgOwGIIqGxsBLBgNyqI75CRALQAbGnRHcA2gAYAuolAAHKrE7pObZSAAeiAIwBmAEzYA7ABYAbAFZTcgBzGbN44adWANCACeiKbGdthypk4AnBFyVs6uQXYAvom+aFh4BMTk1LSQjExgAE6FVIXYKmIAhuhkpQC2GcLikpDSDPJKSCBqGlo6XQYIrk7YETYWctYRxmMWFk6+AUPj2I5OdjZyrnZOFmbJqRg4Ern0zDkABFQYHbo9mtoMuoOGFhHYxlZOhvbOsUGGRaIL4WbBONzWQxWYwWOx2H4HEBpY4tCAAeQwTEuskUd3UD36oEGIlMNlCuzk8Js0TcVisgP8iG2lmcGysb0mW3ByRSIAYVAgcF0yLxvUez0QIms5ImVJpNjpDKWxmw9PGdLh4Te00+iORjSylFRjFFBKeA0QThGQWcexMwWhniBCGiqrepisUVMdlszgieqO2BOdBNXXufXNRKMHtGVuphlJkXs4Wdriso2CCasdgipOidID6WDkAx6FNEYlCAT5jmcjrckMdj2b3GzpsjbBMVMWezDbGPMSQA */ id: 'Auth', initial: 'checkIfLoggedIn', states: { @@ -85,6 +90,9 @@ export const authMachine = createMachine( on: { 'Log out': { target: 'loggedOut', + actions: () => { + if (isDesktop()) writeTokenFile('') + }, }, }, }, @@ -96,7 +104,6 @@ export const authMachine = createMachine( actions: assign({ token: (_, event) => { const token = event.token || '' - localStorage.setItem(TOKEN_PERSIST_KEY, token) return token }, }), @@ -120,11 +127,7 @@ export const authMachine = createMachine( ) async function getUser(context: UserContext) { - const token = VITE_KC_DEV_TOKEN - ? VITE_KC_DEV_TOKEN - : context.token && context.token !== '' - ? context.token - : getCookie(COOKIE_NAME) || localStorage?.getItem(TOKEN_PERSIST_KEY) + const token = await getAndSyncStoredToken(context) const url = withBaseURL('/user') const headers: { [key: string]: string } = { 'Content-Type': 'application/json', @@ -189,3 +192,26 @@ function getCookie(cname: string): string | null { } return null } + +async function getAndSyncStoredToken(context: UserContext): Promise { + // dev mode + if (VITE_KC_DEV_TOKEN) return VITE_KC_DEV_TOKEN + + const token = + context.token && context.token !== '' + ? context.token + : getCookie(COOKIE_NAME) || localStorage?.getItem(TOKEN_PERSIST_KEY) || '' + if (token) { + // has just logged in, update storage + localStorage.setItem(TOKEN_PERSIST_KEY, token) + isDesktop() && writeTokenFile(token) + return token + } + if (!isDesktop()) return '' + const fileToken = isDesktop() ? await readTokenFile() : '' + // prefer other above, but file will ensure login persists after app updates + if (!fileToken) return '' + // has token in file, update localStorage + localStorage.setItem(TOKEN_PERSIST_KEY, fileToken) + return fileToken +}