fix(core): desktop e2e (#15062)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-06-01 23:54:41 +08:00
committed by GitHub
parent 7123595831
commit 38110de134
7 changed files with 79 additions and 19 deletions

1
.gitignore vendored
View File

@@ -49,6 +49,7 @@ testem.log
tsconfig.tsbuildinfo tsconfig.tsbuildinfo
.context .context
/*.md /*.md
.codex
# System Files # System Files
.DS_Store .DS_Store

View File

@@ -24,6 +24,7 @@
".git", ".git",
".vscode", ".vscode",
".context", ".context",
".codex",
".yarnrc.yml", ".yarnrc.yml",
".docker", ".docker",
"**/.storybook", "**/.storybook",

View File

@@ -5,6 +5,7 @@
.git .git
.vscode .vscode
.context .context
.codex
.yarnrc.yml .yarnrc.yml
.docker .docker
**/.storybook **/.storybook

View File

@@ -1,3 +1,4 @@
import { notify } from '@affine/component';
import { configureElectronStateStorageImpls } from '@affine/core/desktop/storage'; import { configureElectronStateStorageImpls } from '@affine/core/desktop/storage';
import { configureCommonModules } from '@affine/core/modules'; import { configureCommonModules } from '@affine/core/modules';
import { configureAppTabsHeaderModule } from '@affine/core/modules/app-tabs-header'; 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 { configureBrowserWorkspaceFlavours } from '@affine/core/modules/workspace-engine';
import { Framework } from '@toeverything/infra'; 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() { export function setupModules() {
const framework = new Framework(); const framework = new Framework();
configureCommonModules(framework); configureCommonModules(framework);
@@ -75,26 +86,38 @@ export function setupModules() {
return { return {
async signInMagicLink(email, token, clientNonce) { async signInMagicLink(email, token, clientNonce) {
await apis.handler.auth.signInMagicLink( const result = await apis.handler.auth.signInMagicLink(
endpoint, endpoint,
email, email,
token, token,
clientNonce clientNonce
); );
notifySessionOnlySignIn(result.sessionOnly);
}, },
async signInOauth(code, state, _provider, clientNonce) { async signInOauth(code, state, _provider, clientNonce) {
return await apis.handler.auth.signInOauth( const result = await apis.handler.auth.signInOauth(
endpoint, endpoint,
code, code,
state, state,
clientNonce clientNonce
); );
notifySessionOnlySignIn(result.sessionOnly);
return result;
}, },
async signInPassword(credential) { 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) { async signInOpenAppSignInCode(code) {
await apis.handler.auth.signInOpenAppSignInCode(endpoint, code); const result = await apis.handler.auth.signInOpenAppSignInCode(
endpoint,
code
);
notifySessionOnlySignIn(result.sessionOnly);
}, },
async signOut() { async signOut() {
await apis.handler.auth.signOut(endpoint); await apis.handler.auth.signOut(endpoint);

View File

@@ -19,6 +19,16 @@ export interface SignInResponse {
redirectUri?: string; 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 { interface ExchangeResponse {
token?: string; token?: string;
} }
@@ -86,8 +96,9 @@ async function exchangeSession(endpoint: string, response: SignInResponse) {
throw new Error('Missing native auth token.'); throw new Error('Missing native auth token.');
} }
setNativeAuthToken(endpoint, body.token); const persistent = setNativeAuthToken(endpoint, body.token);
await clearAuthCookies(endpoint); await clearAuthCookies(endpoint);
return { persistent };
} }
export const authHandlers = { export const authHandlers = {
@@ -104,7 +115,8 @@ export const authHandlers = {
client_nonce: clientNonce, client_nonce: clientNonce,
}); });
const body = await readJson<SignInResponse>(response); const body = await readJson<SignInResponse>(response);
await exchangeSession(endpoint, body); const { persistent } = await exchangeSession(endpoint, body);
return { sessionOnly: !persistent };
}, },
signInOauth: async ( signInOauth: async (
@@ -120,8 +132,8 @@ export const authHandlers = {
client_nonce: clientNonce, client_nonce: clientNonce,
}); });
const body = await readJson<SignInResponse>(response); const body = await readJson<SignInResponse>(response);
await exchangeSession(endpoint, body); const { persistent } = await exchangeSession(endpoint, body);
return { redirectUri: body.redirectUri }; return { redirectUri: body.redirectUri, sessionOnly: !persistent };
}, },
signInPassword: async ( signInPassword: async (
@@ -152,16 +164,20 @@ export const authHandlers = {
password: credential.password, password: credential.password,
}), }),
}); });
const body = await readJson<SignInResponse>(response); const body = await readJson<PasswordSignInResponse>(response);
await exchangeSession(endpoint, body); const { persistent } = await exchangeSession(endpoint, body);
return body; return { ...body, sessionOnly: !persistent };
}, },
signInOpenAppSignInCode: async (_e, endpoint: string, code: string) => { signInOpenAppSignInCode: async (_e, endpoint: string, code: string) => {
const response = await fetchAuth(endpoint, '/api/auth/open-app/sign-in', { const response = await fetchAuth(endpoint, '/api/auth/open-app/sign-in', {
code, code,
}); });
await exchangeSession(endpoint, await readJson(response)); const { persistent } = await exchangeSession(
endpoint,
await readJson(response)
);
return { sessionOnly: !persistent };
}, },
signOut: async (_e, endpoint: string) => { signOut: async (_e, endpoint: string) => {

View File

@@ -11,6 +11,9 @@ type TokenRecord = {
token: string; 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<string, string> = {};
function normalizeEndpoint(endpoint: string) { function normalizeEndpoint(endpoint: string) {
return new URL(endpoint).origin; return new URL(endpoint).origin;
} }
@@ -51,19 +54,33 @@ function decryptToken(value: string): TokenRecord | null {
} }
export function setNativeAuthToken(endpoint: string, token: string) { export function setNativeAuthToken(endpoint: string, token: string) {
const normalizedEndpoint = normalizeEndpoint(endpoint);
if (!safeStorage.isEncryptionAvailable()) {
memoryTokenStore[normalizedEndpoint] = token;
return false;
}
const store = readStore(); const store = readStore();
store[normalizeEndpoint(endpoint)] = encryptToken({ token }); store[normalizedEndpoint] = encryptToken({ token });
writeStore(store); writeStore(store);
return true;
} }
export function deleteNativeAuthToken(endpoint: string) { export function deleteNativeAuthToken(endpoint: string) {
const normalizedEndpoint = normalizeEndpoint(endpoint);
delete memoryTokenStore[normalizedEndpoint];
const store = readStore(); const store = readStore();
delete store[normalizeEndpoint(endpoint)]; delete store[normalizedEndpoint];
writeStore(store); writeStore(store);
} }
export function getNativeAuthToken(endpoint: string) { 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; if (!encrypted) return null;
return decryptToken(encrypted)?.token ?? null; return decryptToken(encrypted)?.token ?? null;
} }

View File

@@ -42,10 +42,11 @@ async function enableEmojiDocIcon(page: Page) {
await confirmExperimentalPrompt(page); await confirmExperimentalPrompt(page);
const settingModal = page.locator('[data-testid=setting-modal-content]'); const settingModal = page.locator('[data-testid=setting-modal-content]');
const item = settingModal.locator('div').getByText('Emoji Doc Icon'); const button = settingModal.getByTestId('enable_emoji_doc_icon');
await item.waitFor({ state: 'attached' }); if (!(await button.isVisible().catch(() => false))) {
await expect(item).toBeVisible(); await page.keyboard.press('Escape');
const button = item.locator('label'); return;
}
const isChecked = await button.locator('input').isChecked(); const isChecked = await button.locator('input').isChecked();
if (!isChecked) { if (!isChecked) {
await button.click(); await button.click();