From 38110de134f4a15ee0bbe37be82100922f9b5a5d Mon Sep 17 00:00:00 2001 From: DarkSky <25152247+darkskygit@users.noreply.github.com> Date: Mon, 1 Jun 2026 23:54:41 +0800 Subject: [PATCH] fix(core): desktop e2e (#15062) ## Summary by CodeRabbit * **Bug Fixes** * Sign-in flows now reliably propagate richer authentication results (user data and session type), improving persistence and reducing intermittent sign-in issues. * Native token handling gains a fallback for environments without encrypted storage, improving session reliability. * **New Features** * User-visible warning when sign-in is session-only because encrypted storage is unavailable. * **Chores** * Tooling ignore patterns updated to exclude .codex. --- .gitignore | 1 + .oxlintrc.json | 1 + .prettierignore | 1 + .../src/app/effects/modules.ts | 31 +++++++++++++++--- .../apps/electron/src/main/auth/handlers.ts | 32 ++++++++++++++----- .../electron/src/main/auth/native-token.ts | 23 +++++++++++-- tests/affine-local/e2e/links.spec.ts | 9 +++--- 7 files changed, 79 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index 08d069b6f2..f33dd2ae9d 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ testem.log tsconfig.tsbuildinfo .context /*.md +.codex # System Files .DS_Store diff --git a/.oxlintrc.json b/.oxlintrc.json index e4b90bdf09..3b9bd6f53e 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -24,6 +24,7 @@ ".git", ".vscode", ".context", + ".codex", ".yarnrc.yml", ".docker", "**/.storybook", diff --git a/.prettierignore b/.prettierignore index 7a0c924898..7007de099c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,6 +5,7 @@ .git .vscode .context +.codex .yarnrc.yml .docker **/.storybook diff --git a/packages/frontend/apps/electron-renderer/src/app/effects/modules.ts b/packages/frontend/apps/electron-renderer/src/app/effects/modules.ts index 3822d13731..b0b004ce96 100644 --- a/packages/frontend/apps/electron-renderer/src/app/effects/modules.ts +++ b/packages/frontend/apps/electron-renderer/src/app/effects/modules.ts @@ -1,3 +1,4 @@ +import { notify } from '@affine/component'; import { configureElectronStateStorageImpls } from '@affine/core/desktop/storage'; import { configureCommonModules } from '@affine/core/modules'; import { configureAppTabsHeaderModule } from '@affine/core/modules/app-tabs-header'; @@ -25,6 +26,16 @@ import { configureDesktopWorkbenchModule } from '@affine/core/modules/workbench' import { configureBrowserWorkspaceFlavours } from '@affine/core/modules/workspace-engine'; import { Framework } from '@toeverything/infra'; +function notifySessionOnlySignIn(sessionOnly?: boolean) { + if (!sessionOnly) return; + + notify.warning({ + title: 'Sign-in is only valid for this session', + message: + 'Encrypted storage is unavailable, so you will need to sign in again after restarting AFFiNE.', + }); +} + export function setupModules() { const framework = new Framework(); configureCommonModules(framework); @@ -75,26 +86,38 @@ export function setupModules() { return { async signInMagicLink(email, token, clientNonce) { - await apis.handler.auth.signInMagicLink( + const result = await apis.handler.auth.signInMagicLink( endpoint, email, token, clientNonce ); + notifySessionOnlySignIn(result.sessionOnly); }, async signInOauth(code, state, _provider, clientNonce) { - return await apis.handler.auth.signInOauth( + const result = await apis.handler.auth.signInOauth( endpoint, code, state, clientNonce ); + notifySessionOnlySignIn(result.sessionOnly); + return result; }, async signInPassword(credential) { - await apis.handler.auth.signInPassword(endpoint, credential); + const result = await apis.handler.auth.signInPassword( + endpoint, + credential + ); + notifySessionOnlySignIn(result.sessionOnly); + return result; }, async signInOpenAppSignInCode(code) { - await apis.handler.auth.signInOpenAppSignInCode(endpoint, code); + const result = await apis.handler.auth.signInOpenAppSignInCode( + endpoint, + code + ); + notifySessionOnlySignIn(result.sessionOnly); }, async signOut() { await apis.handler.auth.signOut(endpoint); diff --git a/packages/frontend/apps/electron/src/main/auth/handlers.ts b/packages/frontend/apps/electron/src/main/auth/handlers.ts index 895f3465ae..e8a7cc0e15 100644 --- a/packages/frontend/apps/electron/src/main/auth/handlers.ts +++ b/packages/frontend/apps/electron/src/main/auth/handlers.ts @@ -19,6 +19,16 @@ export interface SignInResponse { redirectUri?: string; } +export interface PasswordSignInResponse extends SignInResponse { + id: string; + email: string; + name: string; + hasPassword: boolean | null; + avatarUrl: string | null; + emailVerified: boolean; + sessionOnly?: boolean; +} + interface ExchangeResponse { token?: string; } @@ -86,8 +96,9 @@ async function exchangeSession(endpoint: string, response: SignInResponse) { throw new Error('Missing native auth token.'); } - setNativeAuthToken(endpoint, body.token); + const persistent = setNativeAuthToken(endpoint, body.token); await clearAuthCookies(endpoint); + return { persistent }; } export const authHandlers = { @@ -104,7 +115,8 @@ export const authHandlers = { client_nonce: clientNonce, }); const body = await readJson(response); - await exchangeSession(endpoint, body); + const { persistent } = await exchangeSession(endpoint, body); + return { sessionOnly: !persistent }; }, signInOauth: async ( @@ -120,8 +132,8 @@ export const authHandlers = { client_nonce: clientNonce, }); const body = await readJson(response); - await exchangeSession(endpoint, body); - return { redirectUri: body.redirectUri }; + const { persistent } = await exchangeSession(endpoint, body); + return { redirectUri: body.redirectUri, sessionOnly: !persistent }; }, signInPassword: async ( @@ -152,16 +164,20 @@ export const authHandlers = { password: credential.password, }), }); - const body = await readJson(response); - await exchangeSession(endpoint, body); - return body; + const body = await readJson(response); + const { persistent } = await exchangeSession(endpoint, body); + return { ...body, sessionOnly: !persistent }; }, signInOpenAppSignInCode: async (_e, endpoint: string, code: string) => { const response = await fetchAuth(endpoint, '/api/auth/open-app/sign-in', { code, }); - await exchangeSession(endpoint, await readJson(response)); + const { persistent } = await exchangeSession( + endpoint, + await readJson(response) + ); + return { sessionOnly: !persistent }; }, signOut: async (_e, endpoint: string) => { diff --git a/packages/frontend/apps/electron/src/main/auth/native-token.ts b/packages/frontend/apps/electron/src/main/auth/native-token.ts index 69120dfc5c..6a6b5c39cb 100644 --- a/packages/frontend/apps/electron/src/main/auth/native-token.ts +++ b/packages/frontend/apps/electron/src/main/auth/native-token.ts @@ -11,6 +11,9 @@ type TokenRecord = { token: string; }; +// safeStorage may not be available in some environments (e.g. Linux without a keyring), so we fall back to an in-memory store in that case +const memoryTokenStore: Record = {}; + function normalizeEndpoint(endpoint: string) { return new URL(endpoint).origin; } @@ -51,19 +54,33 @@ function decryptToken(value: string): TokenRecord | null { } export function setNativeAuthToken(endpoint: string, token: string) { + const normalizedEndpoint = normalizeEndpoint(endpoint); + if (!safeStorage.isEncryptionAvailable()) { + memoryTokenStore[normalizedEndpoint] = token; + return false; + } + const store = readStore(); - store[normalizeEndpoint(endpoint)] = encryptToken({ token }); + store[normalizedEndpoint] = encryptToken({ token }); writeStore(store); + return true; } export function deleteNativeAuthToken(endpoint: string) { + const normalizedEndpoint = normalizeEndpoint(endpoint); + delete memoryTokenStore[normalizedEndpoint]; + const store = readStore(); - delete store[normalizeEndpoint(endpoint)]; + delete store[normalizedEndpoint]; writeStore(store); } export function getNativeAuthToken(endpoint: string) { - const encrypted = readStore()[normalizeEndpoint(endpoint)]; + const normalizedEndpoint = normalizeEndpoint(endpoint); + const memoryToken = memoryTokenStore[normalizedEndpoint]; + if (memoryToken) return memoryToken; + + const encrypted = readStore()[normalizedEndpoint]; if (!encrypted) return null; return decryptToken(encrypted)?.token ?? null; } diff --git a/tests/affine-local/e2e/links.spec.ts b/tests/affine-local/e2e/links.spec.ts index a6134fefba..1104ed6095 100644 --- a/tests/affine-local/e2e/links.spec.ts +++ b/tests/affine-local/e2e/links.spec.ts @@ -42,10 +42,11 @@ async function enableEmojiDocIcon(page: Page) { await confirmExperimentalPrompt(page); const settingModal = page.locator('[data-testid=setting-modal-content]'); - const item = settingModal.locator('div').getByText('Emoji Doc Icon'); - await item.waitFor({ state: 'attached' }); - await expect(item).toBeVisible(); - const button = item.locator('label'); + const button = settingModal.getByTestId('enable_emoji_doc_icon'); + if (!(await button.isVisible().catch(() => false))) { + await page.keyboard.press('Escape'); + return; + } const isChecked = await button.locator('input').isChecked(); if (!isChecked) { await button.click();