feat: improve selfhosted login (#14502)

fix #13397
fix #14011

#### PR Dependency Tree


* **PR #14502** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Centralized CORS policy with dynamic origin validation applied to
server and realtime connections
* Improved sign-in flows with contextual, localized error hints and
toast notifications
* Centralized network-error normalization and conditional OAuth provider
fetching

* **Bug Fixes**
* Better feedback for self-hosted connection failures and clearer
authentication error handling
* More robust handling of network-related failures with user-friendly
messages
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-02-23 21:23:01 +08:00
committed by GitHub
parent 6d805b302c
commit 91c5869053
7 changed files with 294 additions and 41 deletions
@@ -1,3 +1,4 @@
import { UserFriendlyError } from '@affine/error';
import {
gqlFetcherFactory,
type OauthProvidersQuery,
@@ -11,6 +12,45 @@ import { Store } from '@toeverything/infra';
export type ServerConfigType = ServerConfigQuery['serverConfig'] &
OauthProvidersQuery['serverConfig'];
const NETWORK_ERROR_PATTERNS = [
/failed to fetch/i,
/network request failed/i,
/fetch failed/i,
/load failed/i,
/networkerror/i,
/cors/i,
/certificate/i,
/ssl/i,
/err_[a-z_]+/i,
];
function mapServerConfigError(error: unknown) {
const userFriendlyError = UserFriendlyError.fromAny(error);
if (
userFriendlyError.is('NETWORK_ERROR') ||
userFriendlyError.is('REQUEST_ABORTED') ||
userFriendlyError.is('TOO_MANY_REQUEST')
) {
return userFriendlyError;
}
if (error instanceof Error) {
const detail = `${error.name}: ${error.message}`;
if (NETWORK_ERROR_PATTERNS.some(pattern => pattern.test(detail))) {
return new UserFriendlyError({
status: 504,
code: 'NETWORK_ERROR',
type: 'NETWORK_ERROR',
name: 'NETWORK_ERROR',
message: detail,
stacktrace: error.stack,
});
}
}
return userFriendlyError;
}
export class ServerConfigStore extends Store {
constructor() {
super();
@@ -20,19 +60,13 @@ export class ServerConfigStore extends Store {
serverBaseUrl: string,
abortSignal?: AbortSignal
): Promise<ServerConfigType> {
const gql = gqlFetcherFactory(`${serverBaseUrl}/graphql`, globalThis.fetch);
const serverConfigData = await gql({
query: serverConfigQuery,
context: {
signal: abortSignal,
headers: {
'x-affine-version': BUILD_CONFIG.appVersion,
},
},
});
if (serverConfigData.serverConfig.features.includes(ServerFeature.OAuth)) {
const oauthProvidersData = await gql({
query: oauthProvidersQuery,
try {
const gql = gqlFetcherFactory(
`${serverBaseUrl}/graphql`,
globalThis.fetch
);
const serverConfigData = await gql({
query: serverConfigQuery,
context: {
signal: abortSignal,
headers: {
@@ -40,11 +74,26 @@ export class ServerConfigStore extends Store {
},
},
});
return {
...serverConfigData.serverConfig,
...oauthProvidersData.serverConfig,
};
if (
serverConfigData.serverConfig.features.includes(ServerFeature.OAuth)
) {
const oauthProvidersData = await gql({
query: oauthProvidersQuery,
context: {
signal: abortSignal,
headers: {
'x-affine-version': BUILD_CONFIG.appVersion,
},
},
});
return {
...serverConfigData.serverConfig,
...oauthProvidersData.serverConfig,
};
}
return { ...serverConfigData.serverConfig, oauthProviders: [] };
} catch (error) {
throw mapServerConfigError(error);
}
return { ...serverConfigData.serverConfig, oauthProviders: [] };
}
}