UNAUTHORIZED gmail error (#12262)
# Gmail OAuth authentication flow issues ### TLDR This error is not an error and therefore should be treated as a simple redirect with a snackbar. ### More details Fixing incomplete OAuth token exchange processes and improving error handling for empty Gmail inboxes. The changes include modifications to OAuth guards, to ensure that if a user clicks "cancel" instead of completing the authentication workflow if fails ## Before: Redirection from `/settings/accounts` to `app.twenty.com` with an `UNAUTHORIZED` error ## After : <img width="948" alt="Screenshot 2025-05-26 at 18 04 37" src="https://github.com/user-attachments/assets/62c8721e-c2b3-4e3d-ad0b-e4059dfb7a98" /> Fixes https://github.com/twentyhq/twenty/issues/11895 --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -8,6 +8,7 @@ import { ChromeExtensionSidecarProvider } from '@/chrome-extension-sidecar/compo
|
||||
import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider';
|
||||
import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect';
|
||||
import { MainContextStoreProvider } from '@/context-store/components/MainContextStoreProvider';
|
||||
import { ErrorMessageEffect } from '@/error-handler/components/ErrorMessageEffect';
|
||||
import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect';
|
||||
import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider';
|
||||
import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect';
|
||||
@ -49,6 +50,7 @@ export const AppRouterProviders = () => {
|
||||
<PrefetchDataProvider>
|
||||
<UserThemeProviderEffect />
|
||||
<SnackBarProvider>
|
||||
<ErrorMessageEffect />
|
||||
<DialogManagerScope dialogManagerScopeId="dialog-manager">
|
||||
<DialogManager>
|
||||
<StrictMode>
|
||||
|
||||
@ -5,8 +5,6 @@ import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||
import { useVerifyLogin } from '@/auth/hooks/useVerifyLogin';
|
||||
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||
@ -14,9 +12,7 @@ import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||
export const VerifyLoginTokenEffect = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const loginToken = searchParams.get('loginToken');
|
||||
const errorMessage = searchParams.get('errorMessage');
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const isLogged = useIsLogged();
|
||||
const navigate = useNavigateApp();
|
||||
const { verifyLoginToken } = useVerifyLogin();
|
||||
@ -26,13 +22,6 @@ export const VerifyLoginTokenEffect = () => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDefined(errorMessage)) {
|
||||
enqueueSnackBar(errorMessage, {
|
||||
dedupeKey: 'get-auth-tokens-from-login-token-failed-dedupe-key',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
|
||||
if (!clientConfigLoaded) return;
|
||||
|
||||
if (isDefined(loginToken)) {
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const ErrorMessageEffect = () => {
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const errorMessage = searchParams.get('errorMessage');
|
||||
|
||||
useEffect(() => {
|
||||
if (isDefined(errorMessage)) {
|
||||
enqueueSnackBar(errorMessage, {
|
||||
dedupeKey: 'error-message-dedupe-key',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
const newSearchParams = new URLSearchParams(searchParams);
|
||||
newSearchParams.delete('errorMessage');
|
||||
setSearchParams(newSearchParams);
|
||||
}
|
||||
}, [enqueueSnackBar, errorMessage, searchParams, setSearchParams]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -1,5 +1,3 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { ConnectedAccount } from '@/accounts/types/ConnectedAccount';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
@ -14,6 +12,7 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
|
||||
@ -20,6 +20,7 @@ import { ValidatePasswordResetTokenInput } from 'src/engine/core-modules/auth/dt
|
||||
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
|
||||
import { ApiKeyService } from 'src/engine/core-modules/auth/services/api-key.service';
|
||||
// import { OAuthService } from 'src/engine/core-modules/auth/services/oauth.service';
|
||||
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
@ -53,7 +54,6 @@ import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
|
||||
import { GetAuthTokensFromLoginTokenInput } from './dto/get-auth-tokens-from-login-token.input';
|
||||
import { GetLoginTokenFromCredentialsInput } from './dto/get-login-token-from-credentials.input';
|
||||
|
||||
@ -118,15 +118,16 @@ export class GoogleAPIsAuthController {
|
||||
})
|
||||
.toString(),
|
||||
);
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
return res.redirect(
|
||||
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions(
|
||||
err,
|
||||
workspace ?? {
|
||||
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions({
|
||||
error,
|
||||
workspace: workspace ?? {
|
||||
subdomain: this.twentyConfigService.get('DEFAULT_SUBDOMAIN'),
|
||||
customDomain: null,
|
||||
},
|
||||
),
|
||||
pathname: '/verify',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,14 +118,16 @@ export class GoogleAuthController {
|
||||
billingCheckoutSessionState,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
return res.redirect(
|
||||
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions(
|
||||
err,
|
||||
this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
|
||||
currentWorkspace,
|
||||
),
|
||||
),
|
||||
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions({
|
||||
error,
|
||||
workspace:
|
||||
this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
|
||||
currentWorkspace,
|
||||
),
|
||||
pathname: '/verify',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,15 +125,16 @@ export class MicrosoftAPIsAuthController {
|
||||
})
|
||||
.toString(),
|
||||
);
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
return res.redirect(
|
||||
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions(
|
||||
err,
|
||||
workspace ?? {
|
||||
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions({
|
||||
error,
|
||||
workspace: workspace ?? {
|
||||
subdomain: this.twentyConfigService.get('DEFAULT_SUBDOMAIN'),
|
||||
customDomain: null,
|
||||
},
|
||||
),
|
||||
pathname: '/verify',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,14 +119,16 @@ export class MicrosoftAuthController {
|
||||
billingCheckoutSessionState,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
return res.redirect(
|
||||
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions(
|
||||
err,
|
||||
this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
|
||||
currentWorkspace,
|
||||
),
|
||||
),
|
||||
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions({
|
||||
error,
|
||||
workspace:
|
||||
this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
|
||||
currentWorkspace,
|
||||
),
|
||||
pathname: '/verify',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,25 +19,25 @@ import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { AuthOAuthExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-oauth-exception.filter';
|
||||
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
|
||||
import { EnterpriseFeaturesEnabledGuard } from 'src/engine/core-modules/auth/guards/enterprise-features-enabled.guard';
|
||||
import { OIDCAuthGuard } from 'src/engine/core-modules/auth/guards/oidc-auth.guard';
|
||||
import { SAMLAuthGuard } from 'src/engine/core-modules/auth/guards/saml-auth.guard';
|
||||
import { EnterpriseFeaturesEnabledGuard } from 'src/engine/core-modules/auth/guards/enterprise-features-enabled.guard';
|
||||
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
|
||||
import { OIDCRequest } from 'src/engine/core-modules/auth/strategies/oidc.auth.strategy';
|
||||
import { SAMLRequest } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
||||
import {
|
||||
IdentityProviderType,
|
||||
WorkspaceSSOIdentityProvider,
|
||||
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { AuthOAuthExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-oauth-exception.filter';
|
||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
import { SAMLRequest } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy';
|
||||
import { OIDCRequest } from 'src/engine/core-modules/auth/strategies/oidc.auth.strategy';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
|
||||
@Controller('auth')
|
||||
export class SSOAuthController {
|
||||
@ -157,14 +157,16 @@ export class SSOAuthController {
|
||||
workspace: currentWorkspace,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
return res.redirect(
|
||||
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions(
|
||||
err,
|
||||
this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
|
||||
workspaceIdentityProvider?.workspace,
|
||||
),
|
||||
),
|
||||
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions({
|
||||
error,
|
||||
workspace:
|
||||
this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
|
||||
workspaceIdentityProvider?.workspace,
|
||||
),
|
||||
pathname: '/verify',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,10 +14,18 @@ export class AuthRestApiExceptionFilter implements ExceptionFilter {
|
||||
private readonly httpExceptionHandlerService: HttpExceptionHandlerService,
|
||||
) {}
|
||||
|
||||
catch(exception: AuthException, host: ArgumentsHost) {
|
||||
catch(exception: AuthException | Error, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
|
||||
if (!(exception instanceof AuthException)) {
|
||||
return this.httpExceptionHandlerService.handleError(
|
||||
exception,
|
||||
response,
|
||||
500,
|
||||
);
|
||||
}
|
||||
|
||||
switch (exception.code) {
|
||||
case AuthExceptionCode.USER_NOT_FOUND:
|
||||
case AuthExceptionCode.CLIENT_NOT_FOUND:
|
||||
@ -43,6 +51,7 @@ export class AuthRestApiExceptionFilter implements ExceptionFilter {
|
||||
case AuthExceptionCode.GOOGLE_API_AUTH_DISABLED:
|
||||
case AuthExceptionCode.MICROSOFT_API_AUTH_DISABLED:
|
||||
case AuthExceptionCode.SIGNUP_DISABLED:
|
||||
case AuthExceptionCode.WORKSPACE_NOT_FOUND:
|
||||
return this.httpExceptionHandlerService.handleError(
|
||||
exception,
|
||||
response,
|
||||
|
||||
@ -1,14 +1,20 @@
|
||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { GoogleAPIsOauthExchangeCodeForTokenStrategy } from 'src/engine/core-modules/auth/strategies/google-apis-oauth-exchange-code-for-token.auth.strategy';
|
||||
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
|
||||
import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@Injectable()
|
||||
export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard(
|
||||
@ -17,6 +23,10 @@ export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard(
|
||||
constructor(
|
||||
private readonly guardRedirectService: GuardRedirectService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
private readonly transientTokenService: TransientTokenService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@ -47,6 +57,30 @@ export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard(
|
||||
|
||||
return (await super.canActivate(context)) as boolean;
|
||||
} catch (err) {
|
||||
if (err.status === 401) {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const state = JSON.parse(request.query.state);
|
||||
|
||||
const workspace = await this.getWorkspaceFromTransientToken(
|
||||
state.transientToken,
|
||||
);
|
||||
|
||||
const redirectErrorUrl =
|
||||
this.domainManagerService.computeRedirectErrorUrl(
|
||||
'We cannot connect to your Google account, please try again with more permissions, or a valid account',
|
||||
{
|
||||
subdomain: workspace.subdomain,
|
||||
customDomain: workspace.customDomain,
|
||||
isCustomDomainEnabled: workspace.isCustomDomainEnabled ?? false,
|
||||
},
|
||||
'/settings/accounts',
|
||||
);
|
||||
|
||||
context.switchToHttp().getResponse().redirect(redirectErrorUrl);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
this.guardRedirectService.dispatchErrorFromGuard(
|
||||
context,
|
||||
err,
|
||||
@ -58,4 +92,24 @@ export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard(
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async getWorkspaceFromTransientToken(
|
||||
transientToken: string,
|
||||
): Promise<Workspace> {
|
||||
const { workspaceId } =
|
||||
await this.transientTokenService.verifyTransientToken(transientToken);
|
||||
|
||||
const workspace = await this.workspaceRepository.findOneBy({
|
||||
id: workspaceId,
|
||||
});
|
||||
|
||||
if (!workspace) {
|
||||
throw new AuthException(
|
||||
`Error extracting workspace from transientToken for Google APIs connect for ${workspaceId}`,
|
||||
AuthExceptionCode.WORKSPACE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return workspace;
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,10 +128,11 @@ export class DomainManagerService {
|
||||
computeRedirectErrorUrl(
|
||||
errorMessage: string,
|
||||
workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType,
|
||||
pathname: string,
|
||||
) {
|
||||
const url = this.buildWorkspaceURL({
|
||||
workspace,
|
||||
pathname: '/verify',
|
||||
pathname,
|
||||
searchParams: { errorMessage },
|
||||
});
|
||||
|
||||
|
||||
@ -2,7 +2,10 @@ import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
|
||||
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
@ -25,15 +28,19 @@ export class GuardRedirectService {
|
||||
customDomain: string | null;
|
||||
isCustomDomainEnabled?: boolean;
|
||||
},
|
||||
pathname = '/verify',
|
||||
) {
|
||||
if ('contextType' in context && context.contextType === 'graphql') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
context
|
||||
.switchToHttp()
|
||||
.getResponse()
|
||||
.redirect(this.getRedirectErrorUrlAndCaptureExceptions(error, workspace));
|
||||
context.switchToHttp().getResponse().redirect(
|
||||
this.getRedirectErrorUrlAndCaptureExceptions({
|
||||
error,
|
||||
workspace,
|
||||
pathname,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
getSubdomainAndCustomDomainFromContext(context: ExecutionContext) {
|
||||
@ -58,7 +65,11 @@ export class GuardRedirectService {
|
||||
}
|
||||
|
||||
private captureException(err: Error | CustomException, workspaceId?: string) {
|
||||
if (err instanceof AuthException) return;
|
||||
if (
|
||||
err instanceof AuthException &&
|
||||
err.code !== AuthExceptionCode.INTERNAL_SERVER_ERROR
|
||||
)
|
||||
return;
|
||||
|
||||
this.exceptionHandlerService.captureExceptions([err], {
|
||||
workspace: {
|
||||
@ -67,24 +78,30 @@ export class GuardRedirectService {
|
||||
});
|
||||
}
|
||||
|
||||
getRedirectErrorUrlAndCaptureExceptions(
|
||||
err: Error | CustomException,
|
||||
getRedirectErrorUrlAndCaptureExceptions({
|
||||
error,
|
||||
workspace,
|
||||
pathname,
|
||||
}: {
|
||||
error: Error | AuthException;
|
||||
workspace: {
|
||||
id?: string;
|
||||
subdomain: string;
|
||||
customDomain: string | null;
|
||||
isCustomDomainEnabled?: boolean;
|
||||
},
|
||||
) {
|
||||
this.captureException(err, workspace.id);
|
||||
};
|
||||
pathname: string;
|
||||
}) {
|
||||
this.captureException(error, workspace.id);
|
||||
|
||||
return this.domainManagerService.computeRedirectErrorUrl(
|
||||
err instanceof AuthException ? err.message : 'Unknown error',
|
||||
error instanceof AuthException ? error.message : 'Unknown error',
|
||||
{
|
||||
subdomain: workspace.subdomain,
|
||||
customDomain: workspace.customDomain,
|
||||
isCustomDomainEnabled: workspace.isCustomDomainEnabled ?? false,
|
||||
},
|
||||
pathname,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user