mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-22 12:36:24 +08:00
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:
@@ -1,4 +1,4 @@
|
||||
import { Button } from '@affine/component';
|
||||
import { Button, notify } from '@affine/component';
|
||||
import {
|
||||
AuthContainer,
|
||||
AuthContent,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@affine/component/auth-components';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { ServersService } from '@affine/core/modules/cloud';
|
||||
import { UserFriendlyError } from '@affine/error';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import {
|
||||
@@ -35,12 +36,14 @@ export const AddSelfhostedStep = ({
|
||||
state: SignInState;
|
||||
changeState: Dispatch<SetStateAction<SignInState>>;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const serversService = useService(ServersService);
|
||||
const [baseURL, setBaseURL] = useState(state.initialServerBaseUrl ?? '');
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
|
||||
const t = useI18n();
|
||||
const [errorHint, setErrorHint] = useState(
|
||||
t['com.affine.auth.sign.add-selfhosted.error']()
|
||||
);
|
||||
|
||||
const urlValid = useMemo(() => {
|
||||
try {
|
||||
@@ -51,10 +54,14 @@ export const AddSelfhostedStep = ({
|
||||
}
|
||||
}, [baseURL]);
|
||||
|
||||
const onBaseURLChange = useCallback((value: string) => {
|
||||
setBaseURL(value);
|
||||
setError(false);
|
||||
}, []);
|
||||
const onBaseURLChange = useCallback(
|
||||
(value: string) => {
|
||||
setBaseURL(value);
|
||||
setError(false);
|
||||
setErrorHint(t['com.affine.auth.sign.add-selfhosted.error']());
|
||||
},
|
||||
[t]
|
||||
);
|
||||
|
||||
const onConnect = useAsyncCallback(async () => {
|
||||
setIsConnecting(true);
|
||||
@@ -69,11 +76,33 @@ export const AddSelfhostedStep = ({
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const userFriendlyError = UserFriendlyError.fromAny(err);
|
||||
setError(true);
|
||||
}
|
||||
if (userFriendlyError.is('TOO_MANY_REQUEST')) {
|
||||
setErrorHint(t['error.TOO_MANY_REQUEST']());
|
||||
} else if (
|
||||
userFriendlyError.is('NETWORK_ERROR') ||
|
||||
userFriendlyError.is('REQUEST_ABORTED')
|
||||
) {
|
||||
setErrorHint(t['error.NETWORK_ERROR']());
|
||||
} else {
|
||||
setErrorHint(t['com.affine.auth.sign.add-selfhosted.error']());
|
||||
}
|
||||
|
||||
setIsConnecting(false);
|
||||
}, [baseURL, changeState, serversService]);
|
||||
notify.error({
|
||||
title: t['com.affine.auth.toast.title.failed'](),
|
||||
message:
|
||||
userFriendlyError.is('REQUEST_ABORTED') ||
|
||||
userFriendlyError.is('NETWORK_ERROR')
|
||||
? t['error.NETWORK_ERROR']()
|
||||
: userFriendlyError.is('TOO_MANY_REQUEST')
|
||||
? t['error.TOO_MANY_REQUEST']()
|
||||
: t[`error.${userFriendlyError.name}`](userFriendlyError.data),
|
||||
});
|
||||
} finally {
|
||||
setIsConnecting(false);
|
||||
}
|
||||
}, [baseURL, changeState, serversService, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.initialServerBaseUrl) {
|
||||
@@ -101,7 +130,7 @@ export const AddSelfhostedStep = ({
|
||||
placeholder="https://your-server.com"
|
||||
error={!!error}
|
||||
disabled={isConnecting}
|
||||
errorHint={t['com.affine.auth.sign.add-selfhosted.error']()}
|
||||
errorHint={errorHint}
|
||||
onEnter={onConnect}
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from '@affine/core/modules/cloud';
|
||||
import type { AuthSessionStatus } from '@affine/core/modules/cloud/entities/session';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import { UserFriendlyError } from '@affine/error';
|
||||
import { ServerDeploymentType } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
@@ -46,6 +47,7 @@ export const SignInWithPasswordStep = ({
|
||||
|
||||
const [password, setPassword] = useState('');
|
||||
const [passwordError, setPasswordError] = useState(false);
|
||||
const [passwordErrorHint, setPasswordErrorHint] = useState('');
|
||||
const captchaService = useService(CaptchaService);
|
||||
const serverService = useService(ServerService);
|
||||
const isSelfhosted = useLiveData(
|
||||
@@ -74,6 +76,10 @@ export const SignInWithPasswordStep = ({
|
||||
onAuthenticated?.(loginStatus);
|
||||
}, [loginStatus, onAuthenticated, t]);
|
||||
|
||||
useEffect(() => {
|
||||
setPasswordErrorHint(t['com.affine.auth.password.error']());
|
||||
}, [t]);
|
||||
|
||||
const onSignIn = useAsyncCallback(async () => {
|
||||
if (isLoading || (!verifyToken && needCaptcha)) return;
|
||||
setIsLoading(true);
|
||||
@@ -88,7 +94,23 @@ export const SignInWithPasswordStep = ({
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setPasswordError(true);
|
||||
const error = UserFriendlyError.fromAny(err);
|
||||
|
||||
if (
|
||||
error.is('WRONG_SIGN_IN_CREDENTIALS') ||
|
||||
error.is('PASSWORD_REQUIRED')
|
||||
) {
|
||||
setPasswordError(true);
|
||||
setPasswordErrorHint(t['com.affine.auth.password.error']());
|
||||
} else {
|
||||
setPasswordError(false);
|
||||
notify.error({
|
||||
title: t['com.affine.auth.toast.title.failed'](),
|
||||
message: error.is('REQUEST_ABORTED')
|
||||
? t['error.NETWORK_ERROR']()
|
||||
: t[`error.${error.name}`](error.data),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -101,6 +123,7 @@ export const SignInWithPasswordStep = ({
|
||||
email,
|
||||
password,
|
||||
challenge,
|
||||
t,
|
||||
]);
|
||||
|
||||
const sendMagicLink = useCallback(() => {
|
||||
@@ -126,11 +149,15 @@ export const SignInWithPasswordStep = ({
|
||||
label={t['com.affine.auth.password']()}
|
||||
value={password}
|
||||
type="password"
|
||||
onChange={useCallback((value: string) => {
|
||||
onChange={(value: string) => {
|
||||
setPassword(value);
|
||||
}, [])}
|
||||
if (passwordError) {
|
||||
setPasswordError(false);
|
||||
setPasswordErrorHint(t['com.affine.auth.password.error']());
|
||||
}
|
||||
}}
|
||||
error={passwordError}
|
||||
errorHint={t['com.affine.auth.password.error']()}
|
||||
errorHint={passwordErrorHint}
|
||||
onEnter={onSignIn}
|
||||
/>
|
||||
{!isSelfhosted && (
|
||||
|
||||
Reference in New Issue
Block a user