Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48b2b7b0ff | ||
|
|
94d5348595 | ||
|
|
86423104f7 | ||
|
|
e4a073be56 |
@@ -7,7 +7,7 @@ import {
|
||||
import { setContext } from '@apollo/client/link/context';
|
||||
import { ErrorLink } from '@apollo/client/link/error';
|
||||
import { RetryLink } from '@apollo/client/link/retry';
|
||||
import { from, switchMap } from 'rxjs';
|
||||
import { EMPTY, from, switchMap } from 'rxjs';
|
||||
import { RestLink } from 'apollo-link-rest';
|
||||
import UploadHttpLink from 'apollo-upload-client/UploadHttpLink.mjs';
|
||||
|
||||
@@ -41,7 +41,7 @@ const logger = loggerLink(() => 'Twenty');
|
||||
// Shared across all ApolloFactory instances so concurrent
|
||||
// UNAUTHENTICATED errors from /graphql and /metadata clients
|
||||
// deduplicate into a single renewal request.
|
||||
let renewalPromise: Promise<void> | null = null;
|
||||
let renewalPromise: Promise<boolean> | null = null;
|
||||
|
||||
const TOKEN_RENEWAL_MAX_RETRIES = 3;
|
||||
const TOKEN_RENEWAL_RETRY_DELAY_MS = 1000;
|
||||
@@ -181,21 +181,32 @@ export class ApolloFactory implements ApolloManager {
|
||||
operation: ApolloLink.Operation,
|
||||
forward: ApolloLink.ForwardFunction,
|
||||
) => {
|
||||
if (!getTokenPair()) {
|
||||
onUnauthenticatedError?.();
|
||||
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
if (!renewalPromise) {
|
||||
renewalPromise = attemptTokenRenewal()
|
||||
.then(() => true)
|
||||
.catch(() => {
|
||||
// oxlint-disable-next-line no-console
|
||||
console.log(
|
||||
'Failed to renew token after retries, triggering unauthenticated error',
|
||||
);
|
||||
onUnauthenticatedError?.();
|
||||
|
||||
return false;
|
||||
})
|
||||
.finally(() => {
|
||||
renewalPromise = null;
|
||||
});
|
||||
}
|
||||
|
||||
return from(renewalPromise).pipe(switchMap(() => forward(operation)));
|
||||
return from(renewalPromise).pipe(
|
||||
switchMap((succeeded) => (succeeded ? forward(operation) : EMPTY)),
|
||||
);
|
||||
};
|
||||
|
||||
const sendToSentry = ({
|
||||
|
||||
@@ -9,7 +9,7 @@ import { PageChangeEffect } from '@/app/effect-components/PageChangeEffect';
|
||||
import { SignOutOnOtherTabSignOutEffect } from '@/auth/effect-components/SignOutOnOtherTabSignOutEffect';
|
||||
import { AuthProvider } from '@/auth/components/AuthProvider';
|
||||
import { CaptchaProvider } from '@/captcha/components/CaptchaProvider';
|
||||
import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider';
|
||||
import { ClientConfigErrorGate } from '@/client-config/components/ClientConfigErrorGate';
|
||||
import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect';
|
||||
import { MainContextStoreProvider } from '@/context-store/components/MainContextStoreProvider';
|
||||
import { ErrorMessageEffect } from '@/error-handler/components/ErrorMessageEffect';
|
||||
@@ -47,7 +47,7 @@ export const AppRouterProviders = () => {
|
||||
<MinimalMetadataLoadEffect />
|
||||
<IsMinimalMetadataReadyEffect />
|
||||
<WorkspaceProviderEffect />
|
||||
<ClientConfigProvider>
|
||||
<ClientConfigErrorGate>
|
||||
<CaptchaProvider>
|
||||
<MinimalMetadataGater>
|
||||
<AuthProvider>
|
||||
@@ -86,7 +86,7 @@ export const AppRouterProviders = () => {
|
||||
</AuthProvider>
|
||||
</MinimalMetadataGater>
|
||||
</CaptchaProvider>
|
||||
</ClientConfigProvider>
|
||||
</ClientConfigErrorGate>
|
||||
</BaseThemeProvider>
|
||||
</ApolloProvider>
|
||||
);
|
||||
|
||||
@@ -42,7 +42,9 @@ export const VerifyEmailEffect = () => {
|
||||
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
|
||||
const { verifyLoginToken } = useVerifyLogin();
|
||||
const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace();
|
||||
const clientConfigApiStatus = useAtomStateValue(clientConfigApiStatusState);
|
||||
const { isLoaded: isClientConfigLoaded } = useAtomStateValue(
|
||||
clientConfigApiStatusState,
|
||||
);
|
||||
|
||||
const { t } = useLingui();
|
||||
useEffect(() => {
|
||||
@@ -113,7 +115,7 @@ export const VerifyEmailEffect = () => {
|
||||
}
|
||||
};
|
||||
|
||||
if (!clientConfigApiStatus.isLoadedOnce) {
|
||||
if (!isClientConfigLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -121,7 +123,7 @@ export const VerifyEmailEffect = () => {
|
||||
|
||||
// Verify email only needs to run once at mount
|
||||
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [clientConfigApiStatus.isLoadedOnce]);
|
||||
}, [isClientConfigLoaded]);
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
|
||||
import { useVerifyLogin } from '@/auth/hooks/useVerifyLogin';
|
||||
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { tokenPairState } from '@/auth/states/tokenPairState';
|
||||
import { useInvalidateMetadataStore } from '@/metadata-store/hooks/useInvalidateMetadataStore';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||
|
||||
export const VerifyLoginTokenEffect = () => {
|
||||
@@ -15,25 +16,20 @@ export const VerifyLoginTokenEffect = () => {
|
||||
|
||||
const hasAccessTokenPair = useHasAccessTokenPair();
|
||||
const navigate = useNavigateApp();
|
||||
const setTokenPair = useSetAtomState(tokenPairState);
|
||||
const { verifyLoginToken } = useVerifyLogin();
|
||||
|
||||
const { isSaved: clientConfigLoaded } = useAtomStateValue(
|
||||
clientConfigApiStatusState,
|
||||
);
|
||||
const { resetMetadataStore } = useInvalidateMetadataStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!clientConfigLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDefined(loginToken)) {
|
||||
resetMetadataStore();
|
||||
setTokenPair(null);
|
||||
verifyLoginToken(loginToken);
|
||||
} else if (!hasAccessTokenPair) {
|
||||
navigate(AppPath.SignInUp);
|
||||
}
|
||||
// Verify only needs to run once at mount
|
||||
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [clientConfigLoaded]);
|
||||
}, []);
|
||||
|
||||
return <></>;
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -341,6 +341,8 @@ export const useAuth = () => {
|
||||
handleSetLoginToken(loginToken);
|
||||
navigate(AppPath.SignInUp);
|
||||
setSignInUpStep(SignInUpStep.TwoFactorAuthenticationProvision);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -351,7 +353,11 @@ export const useAuth = () => {
|
||||
handleSetLoginToken(loginToken);
|
||||
navigate(AppPath.SignInUp);
|
||||
setSignInUpStep(SignInUpStep.TwoFactorAuthenticationVerification);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { tokenPairState } from '@/auth/states/tokenPairState';
|
||||
import { isWorkspaceSpecificAccessToken } from '@/auth/utils/isWorkspaceSpecificAccessToken';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
export const useHasWorkspaceSpecificToken = (): boolean => {
|
||||
const tokenPair = useAtomStateValue(tokenPairState);
|
||||
|
||||
if (!tokenPair?.accessOrWorkspaceAgnosticToken?.token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isWorkspaceSpecificAccessToken(
|
||||
tokenPair.accessOrWorkspaceAgnosticToken.token,
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
export const isWorkspaceSpecificAccessToken = (
|
||||
tokenString: string,
|
||||
): boolean => {
|
||||
try {
|
||||
const base64Url = tokenString.split('.')[1];
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const payload = JSON.parse(atob(base64));
|
||||
|
||||
return payload.type === 'ACCESS';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
+4
-3
@@ -2,14 +2,15 @@ import { clientConfigApiStatusState } from '@/client-config/states/clientConfigA
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { AppFullScreenErrorFallback } from '@/error-handler/components/AppFullScreenErrorFallback';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
export const ClientConfigErrorGate: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { isErrored, error } = useAtomStateValue(clientConfigApiStatusState);
|
||||
const { error } = useAtomStateValue(clientConfigApiStatusState);
|
||||
const { t } = useLingui();
|
||||
|
||||
return isErrored && error instanceof Error ? (
|
||||
return isDefined(error) ? (
|
||||
<AppFullScreenErrorFallback
|
||||
error={error}
|
||||
resetErrorBoundary={() => {
|
||||
+5
-33
@@ -1,45 +1,17 @@
|
||||
import { useClientConfig } from '@/client-config/hooks/useClientConfig';
|
||||
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useEffect } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const ClientConfigProviderEffect = () => {
|
||||
const [clientConfigApiStatus, setClientConfigApiStatus] = useAtomState(
|
||||
clientConfigApiStatusState,
|
||||
);
|
||||
|
||||
const { data, loading, error, fetchClientConfig } = useClientConfig();
|
||||
const { isLoaded } = useAtomStateValue(clientConfigApiStatusState);
|
||||
const { fetchClientConfig } = useClientConfig();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!clientConfigApiStatus.isLoadedOnce &&
|
||||
!clientConfigApiStatus.isLoading
|
||||
) {
|
||||
if (!isLoaded) {
|
||||
fetchClientConfig();
|
||||
}
|
||||
}, [
|
||||
clientConfigApiStatus.isLoadedOnce,
|
||||
clientConfigApiStatus.isLoading,
|
||||
fetchClientConfig,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) return;
|
||||
|
||||
if (error instanceof Error) {
|
||||
setClientConfigApiStatus((currentStatus) => ({
|
||||
...currentStatus,
|
||||
isErrored: true,
|
||||
error,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDefined(data?.clientConfig)) {
|
||||
return;
|
||||
}
|
||||
}, [data?.clientConfig, error, loading, setClientConfigApiStatus]);
|
||||
}, [isLoaded, fetchClientConfig]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
@@ -9,10 +9,10 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
export const useCaptcha = () => {
|
||||
const captcha = useAtomStateValue(captchaState);
|
||||
const captchaToken = useAtomStateValue(captchaTokenState);
|
||||
const clientConfigApiStatus = useAtomStateValue(clientConfigApiStatusState);
|
||||
const { isLoaded: isClientConfigLoaded } = useAtomStateValue(
|
||||
clientConfigApiStatusState,
|
||||
);
|
||||
const isCaptchaScriptLoaded = useAtomStateValue(isCaptchaScriptLoadedState);
|
||||
|
||||
const isClientConfigLoaded = clientConfigApiStatus.isLoadedOnce;
|
||||
const isSiteKeyDefined = isDefined(captcha?.siteKey);
|
||||
const isTokenAvailable = isDefined(captchaToken);
|
||||
|
||||
|
||||
@@ -23,24 +23,14 @@ import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWork
|
||||
import { labPublicFeatureFlagsState } from '@/client-config/states/labPublicFeatureFlagsState';
|
||||
import { sentryConfigState } from '@/client-config/states/sentryConfigState';
|
||||
import { supportChatState } from '@/client-config/states/supportChatState';
|
||||
import { type ClientConfig } from '@/client-config/types/ClientConfig';
|
||||
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
|
||||
import { useCallback } from 'react';
|
||||
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
||||
import { getClientConfig } from '@/client-config/utils/getClientConfig';
|
||||
import { allowRequestsToTwentyIconsState } from '@/client-config/states/allowRequestsToTwentyIcons';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
|
||||
type UseClientConfigResult = {
|
||||
data: { clientConfig: ClientConfig } | undefined;
|
||||
loading: boolean;
|
||||
error: Error | undefined;
|
||||
fetchClientConfig: () => Promise<void>;
|
||||
refetch: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const useClientConfig = (): UseClientConfigResult => {
|
||||
export const useClientConfig = () => {
|
||||
const setIsAnalyticsEnabled = useSetAtomState(isAnalyticsEnabledState);
|
||||
const setDomainConfiguration = useSetAtomState(domainConfigurationState);
|
||||
const setAuthProviders = useSetAtomState(authProvidersState);
|
||||
@@ -60,9 +50,7 @@ export const useClientConfig = (): UseClientConfigResult => {
|
||||
const setSupportChat = useSetAtomState(supportChatState);
|
||||
|
||||
const setSentryConfig = useSetAtomState(sentryConfigState);
|
||||
const [clientConfigApiStatus, setClientConfigApiStatus] = useAtomState(
|
||||
clientConfigApiStatusState,
|
||||
);
|
||||
const setClientConfigApiStatus = useSetAtomState(clientConfigApiStatusState);
|
||||
|
||||
const setCaptcha = useSetAtomState(captchaState);
|
||||
|
||||
@@ -120,26 +108,9 @@ export const useClientConfig = (): UseClientConfigResult => {
|
||||
const setAppVersion = useSetAtomState(appVersionState);
|
||||
|
||||
const fetchClientConfig = useCallback(async () => {
|
||||
setClientConfigApiStatus((prev) => ({
|
||||
...prev,
|
||||
isLoading: true,
|
||||
}));
|
||||
|
||||
try {
|
||||
const clientConfig = await getClientConfig();
|
||||
setClientConfigApiStatus((prev) => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
isLoadedOnce: true,
|
||||
isErrored: false,
|
||||
error: undefined,
|
||||
data: { clientConfig },
|
||||
}));
|
||||
setClientConfigApiStatus((currentStatus) => ({
|
||||
...currentStatus,
|
||||
isErrored: false,
|
||||
error: undefined,
|
||||
}));
|
||||
|
||||
setAppVersion(clientConfig.appVersion);
|
||||
setAuthProviders({
|
||||
google: clientConfig.authProviders.google,
|
||||
@@ -155,18 +126,15 @@ export const useClientConfig = (): UseClientConfigResult => {
|
||||
setIsEmailVerificationRequired(clientConfig.isEmailVerificationRequired);
|
||||
setBilling(clientConfig.billing);
|
||||
setSupportChat(clientConfig.support);
|
||||
|
||||
setSentryConfig({
|
||||
dsn: clientConfig?.sentry?.dsn,
|
||||
release: clientConfig?.sentry?.release,
|
||||
environment: clientConfig?.sentry?.environment,
|
||||
});
|
||||
|
||||
setCaptcha({
|
||||
provider: clientConfig?.captcha?.provider,
|
||||
siteKey: clientConfig?.captcha?.siteKey,
|
||||
});
|
||||
|
||||
setApiConfig(clientConfig?.api);
|
||||
setDomainConfiguration({
|
||||
defaultSubdomain: clientConfig?.defaultSubdomain,
|
||||
@@ -182,11 +150,6 @@ export const useClientConfig = (): UseClientConfigResult => {
|
||||
setIsConfigVariablesInDbEnabled(
|
||||
clientConfig?.isConfigVariablesInDbEnabled,
|
||||
);
|
||||
setClientConfigApiStatus((currentStatus) => ({
|
||||
...currentStatus,
|
||||
isSaved: true,
|
||||
}));
|
||||
|
||||
setCalendarBookingPageId(clientConfig?.calendarBookingPageId ?? null);
|
||||
setIsImapSmtpCaldavEnabled(clientConfig?.isImapSmtpCaldavEnabled);
|
||||
setIsEmailingDomainsEnabled(clientConfig?.isEmailingDomainsEnabled);
|
||||
@@ -195,16 +158,12 @@ export const useClientConfig = (): UseClientConfigResult => {
|
||||
clientConfig?.isCloudflareIntegrationEnabled,
|
||||
);
|
||||
setIsClickHouseConfigured(clientConfig?.isClickHouseConfigured ?? false);
|
||||
|
||||
setClientConfigApiStatus({ isLoaded: true, error: undefined });
|
||||
} catch (err) {
|
||||
const error =
|
||||
err instanceof Error ? err : new Error('Failed to fetch client config');
|
||||
setClientConfigApiStatus((prev) => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
isLoadedOnce: true,
|
||||
isErrored: true,
|
||||
error,
|
||||
}));
|
||||
setClientConfigApiStatus({ isLoaded: true, error });
|
||||
}
|
||||
}, [
|
||||
setAiModels,
|
||||
@@ -237,11 +196,5 @@ export const useClientConfig = (): UseClientConfigResult => {
|
||||
setAllowRequestsToTwentyIcons,
|
||||
]);
|
||||
|
||||
return {
|
||||
data: clientConfigApiStatus.data,
|
||||
loading: clientConfigApiStatus.isLoading || false,
|
||||
error: clientConfigApiStatus.error,
|
||||
fetchClientConfig,
|
||||
refetch: fetchClientConfig,
|
||||
};
|
||||
return { fetchClientConfig };
|
||||
};
|
||||
|
||||
+2
-11
@@ -1,24 +1,15 @@
|
||||
import { type ClientConfig } from '@/client-config/types/ClientConfig';
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
|
||||
type ClientConfigApiStatus = {
|
||||
isLoadedOnce: boolean;
|
||||
isLoading: boolean;
|
||||
isErrored: boolean;
|
||||
isSaved: boolean;
|
||||
isLoaded: boolean;
|
||||
error?: Error;
|
||||
data?: { clientConfig: ClientConfig };
|
||||
};
|
||||
|
||||
export const clientConfigApiStatusState =
|
||||
createAtomState<ClientConfigApiStatus>({
|
||||
key: 'clientConfigApiStatus',
|
||||
defaultValue: {
|
||||
isLoadedOnce: false,
|
||||
isLoading: false,
|
||||
isErrored: false,
|
||||
isSaved: false,
|
||||
isLoaded: false,
|
||||
error: undefined,
|
||||
data: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
+4
-2
@@ -29,7 +29,9 @@ export const useGetPublicWorkspaceDataByDomain = () => {
|
||||
const workspacePublicData = useAtomStateValue(workspacePublicDataState);
|
||||
const { redirectToDefaultDomain } = useRedirectToDefaultDomain();
|
||||
const setWorkspacePublicData = useSetAtomState(workspacePublicDataState);
|
||||
const clientConfigApiStatus = useAtomStateValue(clientConfigApiStatusState);
|
||||
const { isLoaded: isClientConfigLoaded } = useAtomStateValue(
|
||||
clientConfigApiStatusState,
|
||||
);
|
||||
|
||||
const { loading, data, error } = useQuery(
|
||||
GetPublicWorkspaceDataByDomainDocument,
|
||||
@@ -38,7 +40,7 @@ export const useGetPublicWorkspaceDataByDomain = () => {
|
||||
origin,
|
||||
},
|
||||
skip:
|
||||
!clientConfigApiStatus.isSaved ||
|
||||
!isClientConfigLoaded ||
|
||||
(isMultiWorkspaceEnabled && isDefaultDomain) ||
|
||||
isDefined(workspacePublicData),
|
||||
},
|
||||
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
|
||||
import { useHasWorkspaceSpecificToken } from '@/auth/hooks/useHasWorkspaceSpecificToken';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
|
||||
import { useLoadMinimalMetadata } from '@/metadata-store/hooks/useLoadMinimalMetadata';
|
||||
@@ -12,10 +12,10 @@ import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
|
||||
type LoadedState = 'none' | 'mocked' | 'real';
|
||||
|
||||
const computeDesiredLoadState = (
|
||||
hasAccessTokenPair: boolean,
|
||||
hasWorkspaceSpecificToken: boolean,
|
||||
isActiveWorkspace: boolean,
|
||||
): LoadedState => {
|
||||
if (hasAccessTokenPair && isActiveWorkspace) {
|
||||
if (hasWorkspaceSpecificToken && isActiveWorkspace) {
|
||||
return 'real';
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ const computeDesiredLoadState = (
|
||||
};
|
||||
|
||||
export const MinimalMetadataLoadEffect = () => {
|
||||
const hasAccessTokenPair = useHasAccessTokenPair();
|
||||
const hasWorkspaceSpecificToken = useHasWorkspaceSpecificToken();
|
||||
const isCurrentUserLoaded = useAtomStateValue(isCurrentUserLoadedState);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const metadataLoadedVersion = useAtomStateValue(metadataLoadedVersionState);
|
||||
@@ -39,7 +39,7 @@ export const MinimalMetadataLoadEffect = () => {
|
||||
const isActiveWorkspace = isWorkspaceActiveOrSuspended(currentWorkspace);
|
||||
|
||||
const desiredLoadState = computeDesiredLoadState(
|
||||
hasAccessTokenPair,
|
||||
hasWorkspaceSpecificToken,
|
||||
isActiveWorkspace,
|
||||
);
|
||||
|
||||
@@ -76,7 +76,7 @@ export const MinimalMetadataLoadEffect = () => {
|
||||
performLoad();
|
||||
}, [
|
||||
isCurrentUserLoaded,
|
||||
hasAccessTokenPair,
|
||||
hasWorkspaceSpecificToken,
|
||||
isActiveWorkspace,
|
||||
desiredLoadState,
|
||||
lastMetadataLoadData,
|
||||
|
||||
+18
-151
@@ -1,182 +1,49 @@
|
||||
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
|
||||
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
||||
import { currentWorkspaceDeletedMembersState } from '@/auth/states/currentWorkspaceDeletedMembersState';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
|
||||
import { useInitializeFormatPreferences } from '@/localization/hooks/useInitializeFormatPreferences';
|
||||
import { getDateFnsLocale } from '@/ui/field/display/utils/getDateFnsLocale';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { type ColorScheme } from '@/workspace-member/types/WorkspaceMember';
|
||||
import { enUS } from 'date-fns/locale';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { type ObjectPermissions } from 'twenty-shared/types';
|
||||
import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser';
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation, useSearchParams } from 'react-router-dom';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import {
|
||||
type WorkspaceMember,
|
||||
GetCurrentUserDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
|
||||
export const UserMetadataProviderInitialEffect = () => {
|
||||
const hasAccessTokenPair = useHasAccessTokenPair();
|
||||
const currentUser = useAtomStateValue(currentUserState);
|
||||
const store = useStore();
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const location = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const setCurrentUser = useSetAtomState(currentUserState);
|
||||
const setCurrentWorkspace = useSetAtomState(currentWorkspaceState);
|
||||
const setCurrentUserWorkspace = useSetAtomState(currentUserWorkspaceState);
|
||||
const setAvailableWorkspaces = useSetAtomState(availableWorkspacesState);
|
||||
const setCurrentWorkspaceMember = useSetAtomState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
const setCurrentWorkspaceMembers = useSetAtomState(
|
||||
currentWorkspaceMembersState,
|
||||
);
|
||||
const setCurrentWorkspaceDeletedMembers = useSetAtomState(
|
||||
currentWorkspaceDeletedMembersState,
|
||||
);
|
||||
const setIsCurrentUserLoaded = useSetAtomState(isCurrentUserLoadedState);
|
||||
const { loadCurrentUser } = useLoadCurrentUser();
|
||||
|
||||
const { initializeFormatPreferences } = useInitializeFormatPreferences();
|
||||
|
||||
const updateLocaleCatalog = useCallback(
|
||||
async (newLocale: keyof typeof APP_LOCALES) => {
|
||||
const localeValue = store.get(dateLocaleState.atom);
|
||||
if (localeValue.locale !== newLocale) {
|
||||
getDateFnsLocale(newLocale).then((localeCatalog) => {
|
||||
const newValue = {
|
||||
locale: newLocale,
|
||||
localeCatalog: localeCatalog || enUS,
|
||||
};
|
||||
store.set(dateLocaleState.atom, newValue);
|
||||
});
|
||||
}
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
const shouldSkipUserQuery = !hasAccessTokenPair || isDefined(currentUser);
|
||||
|
||||
const { data: userQueryData, loading: userQueryLoading } = useQuery(
|
||||
GetCurrentUserDocument,
|
||||
{
|
||||
skip: shouldSkipUserQuery,
|
||||
},
|
||||
);
|
||||
const isLoginFlowInProgress =
|
||||
isMatchingLocation(location, AppPath.Verify) &&
|
||||
searchParams.has('loginToken');
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized) {
|
||||
if (isLoginFlowInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasAccessTokenPair) {
|
||||
setIsCurrentUserLoaded(true);
|
||||
setIsInitialized(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (userQueryLoading || !isDefined(userQueryData?.currentUser)) {
|
||||
if (isDefined(currentUser)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentUser(userQueryData.currentUser);
|
||||
|
||||
if (isDefined(userQueryData.currentUser.currentWorkspace)) {
|
||||
setCurrentWorkspace({
|
||||
...userQueryData.currentUser.currentWorkspace,
|
||||
defaultRole:
|
||||
userQueryData.currentUser.currentWorkspace.defaultRole ?? null,
|
||||
workspaceCustomApplication:
|
||||
userQueryData.currentUser.currentWorkspace
|
||||
.workspaceCustomApplication ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(userQueryData.currentUser.currentUserWorkspace)) {
|
||||
setCurrentUserWorkspace({
|
||||
permissionFlags:
|
||||
userQueryData.currentUser.currentUserWorkspace.permissionFlags ?? [],
|
||||
twoFactorAuthenticationMethodSummary:
|
||||
userQueryData.currentUser.currentUserWorkspace
|
||||
.twoFactorAuthenticationMethodSummary ?? [],
|
||||
objectsPermissions:
|
||||
(userQueryData.currentUser.currentUserWorkspace
|
||||
.objectsPermissions as Array<
|
||||
ObjectPermissions & { objectMetadataId: string }
|
||||
>) ?? [],
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
workspaceMember,
|
||||
workspaceMembers,
|
||||
deletedWorkspaceMembers,
|
||||
availableWorkspaces,
|
||||
} = userQueryData.currentUser;
|
||||
|
||||
const affectDefaultValuesOnEmptyWorkspaceMemberFields = (
|
||||
workspaceMember: WorkspaceMember,
|
||||
) => {
|
||||
return {
|
||||
...workspaceMember,
|
||||
colorScheme: (workspaceMember.colorScheme as ColorScheme) ?? 'System',
|
||||
locale:
|
||||
(workspaceMember.locale as keyof typeof APP_LOCALES) ?? SOURCE_LOCALE,
|
||||
};
|
||||
};
|
||||
|
||||
if (isDefined(workspaceMember)) {
|
||||
const updatedWorkspaceMember =
|
||||
affectDefaultValuesOnEmptyWorkspaceMemberFields(workspaceMember);
|
||||
setCurrentWorkspaceMember(updatedWorkspaceMember);
|
||||
|
||||
updateLocaleCatalog(updatedWorkspaceMember.locale);
|
||||
|
||||
initializeFormatPreferences(updatedWorkspaceMember);
|
||||
|
||||
dynamicActivate(
|
||||
(workspaceMember.locale as keyof typeof APP_LOCALES) ?? SOURCE_LOCALE,
|
||||
);
|
||||
}
|
||||
|
||||
if (isDefined(workspaceMembers)) {
|
||||
setCurrentWorkspaceMembers(workspaceMembers);
|
||||
}
|
||||
|
||||
if (isDefined(deletedWorkspaceMembers)) {
|
||||
setCurrentWorkspaceDeletedMembers(deletedWorkspaceMembers);
|
||||
}
|
||||
|
||||
if (isDefined(availableWorkspaces)) {
|
||||
setAvailableWorkspaces(availableWorkspaces);
|
||||
}
|
||||
|
||||
setIsCurrentUserLoaded(true);
|
||||
setIsInitialized(true);
|
||||
loadCurrentUser();
|
||||
}, [
|
||||
isInitialized,
|
||||
hasAccessTokenPair,
|
||||
userQueryLoading,
|
||||
userQueryData?.currentUser,
|
||||
setCurrentUser,
|
||||
setCurrentUserWorkspace,
|
||||
setCurrentWorkspaceMembers,
|
||||
setAvailableWorkspaces,
|
||||
setCurrentWorkspace,
|
||||
setCurrentWorkspaceMember,
|
||||
initializeFormatPreferences,
|
||||
setCurrentWorkspaceDeletedMembers,
|
||||
updateLocaleCatalog,
|
||||
isLoginFlowInProgress,
|
||||
currentUser,
|
||||
loadCurrentUser,
|
||||
setIsCurrentUserLoaded,
|
||||
]);
|
||||
|
||||
|
||||
+15
-1
@@ -19,5 +19,19 @@ export const useInvalidateMetadataStore = () => {
|
||||
store.set(metadataLoadedVersionState.atom, (prev) => prev + 1);
|
||||
}, [store]);
|
||||
|
||||
return { invalidateMetadataStore };
|
||||
// Resets statuses to 'empty' so isMinimalMetadataReady becomes false
|
||||
// and loadMinimalMetadata writes fresh server data. Use during login
|
||||
// flows where stale localStorage metadata must not gate rendering.
|
||||
const resetMetadataStore = useCallback(() => {
|
||||
for (const key of ALL_METADATA_ENTITY_KEYS) {
|
||||
store.set(metadataStoreState.atomFamily(key), (prev) => ({
|
||||
...prev,
|
||||
status: 'empty',
|
||||
currentCollectionHash: undefined,
|
||||
}));
|
||||
}
|
||||
store.set(metadataLoadedVersionState.atom, (prev) => prev + 1);
|
||||
}, [store]);
|
||||
|
||||
return { invalidateMetadataStore, resetMetadataStore };
|
||||
};
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ type UsageBreakdownItem = {
|
||||
|
||||
export const SettingsAdminAI = () => {
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const { refetch: refetchClientConfig } = useClientConfig();
|
||||
const { fetchClientConfig: refetchClientConfig } = useClientConfig();
|
||||
const { formatUsageValue } = useUsageValueFormatter();
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const billing = useAtomStateValue(billingState);
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ import {
|
||||
export const useConfigVariableActions = (variableName: string) => {
|
||||
const { t } = useLingui();
|
||||
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
|
||||
const { refetch: refetchClientConfig } = useClientConfig();
|
||||
const { fetchClientConfig: refetchClientConfig } = useClientConfig();
|
||||
|
||||
const [updateDatabaseConfigVariable] = useMutation(
|
||||
UpdateDatabaseConfigVariableDocument,
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { ApolloCoreClientContext } from '@/object-metadata/contexts/ApolloCoreClientContext';
|
||||
import {
|
||||
ApolloClient,
|
||||
ApolloLink,
|
||||
InMemoryCache,
|
||||
Observable,
|
||||
} from '@apollo/client';
|
||||
import { ApolloProvider } from '@apollo/client/react';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
const noOpLink = new ApolloLink(
|
||||
() =>
|
||||
new Observable((observer) => {
|
||||
observer.next({ data: {} });
|
||||
observer.complete();
|
||||
}),
|
||||
);
|
||||
|
||||
const noOpClient = new ApolloClient({
|
||||
link: noOpLink,
|
||||
cache: new InMemoryCache(),
|
||||
});
|
||||
|
||||
export const NoOpApolloCoreProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) => (
|
||||
<ApolloProvider client={noOpClient}>
|
||||
<ApolloCoreClientContext.Provider value={noOpClient}>
|
||||
{children}
|
||||
</ApolloCoreClientContext.Provider>
|
||||
</ApolloProvider>
|
||||
);
|
||||
@@ -11,6 +11,7 @@ import { MobileNavigationBar } from '@/navigation/components/MobileNavigationBar
|
||||
import { PageDragDropProvider } from '@/navigation-menu-item/display/dnd/providers/PageDragDropProvider';
|
||||
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
|
||||
import { OBJECT_SETTINGS_WIDTH } from '@/settings/data-model/constants/ObjectSettings';
|
||||
import { NoOpApolloCoreProvider } from '@/sign-in-background-mock/components/NoOpApolloCoreProvider';
|
||||
import { SignInAppNavigationDrawerMock } from '@/sign-in-background-mock/components/SignInAppNavigationDrawerMock';
|
||||
import { Suspense, lazy, useContext } from 'react';
|
||||
|
||||
@@ -105,9 +106,11 @@ export const DefaultLayout = () => {
|
||||
{showAuthModal ? (
|
||||
<>
|
||||
<StyledMainContainer>
|
||||
<Suspense fallback={null}>
|
||||
<SignInBackgroundMockPage />
|
||||
</Suspense>
|
||||
<NoOpApolloCoreProvider>
|
||||
<Suspense fallback={null}>
|
||||
<SignInBackgroundMockPage />
|
||||
</Suspense>
|
||||
</NoOpApolloCoreProvider>
|
||||
</StyledMainContainer>
|
||||
<AnimatePresence mode="wait">
|
||||
<LayoutGroup>
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
||||
import { currentWorkspaceDeletedMembersState } from '@/auth/states/currentWorkspaceDeletedMembersState';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
|
||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
|
||||
import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain';
|
||||
import { useInitializeFormatPreferences } from '@/localization/hooks/useInitializeFormatPreferences';
|
||||
import { getDateFnsLocale } from '@/ui/field/display/utils/getDateFnsLocale';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { workspaceAuthBypassProvidersState } from '@/workspace/states/workspaceAuthBypassProvidersState';
|
||||
import { useApolloClient } from '@apollo/client/react';
|
||||
import { enUS } from 'date-fns/locale';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { SOURCE_LOCALE, type APP_LOCALES } from 'twenty-shared/translations';
|
||||
import { type ObjectPermissions } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type ColorScheme } from 'twenty-ui/input';
|
||||
import { useApolloClient } from '@apollo/client/react';
|
||||
import { GetCurrentUserDocument } from '~/generated-metadata/graphql';
|
||||
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
|
||||
|
||||
@@ -33,7 +39,11 @@ export const useLoadCurrentUser = () => {
|
||||
const setCurrentWorkspaceMembers = useSetAtomState(
|
||||
currentWorkspaceMembersState,
|
||||
);
|
||||
const setCurrentWorkspaceDeletedMembers = useSetAtomState(
|
||||
currentWorkspaceDeletedMembersState,
|
||||
);
|
||||
const setCurrentWorkspace = useSetAtomState(currentWorkspaceState);
|
||||
const setIsCurrentUserLoaded = useSetAtomState(isCurrentUserLoadedState);
|
||||
const { initializeFormatPreferences } = useInitializeFormatPreferences();
|
||||
const setWorkspaceAuthBypassProviders = useSetAtomState(
|
||||
workspaceAuthBypassProvidersState,
|
||||
@@ -43,6 +53,7 @@ export const useLoadCurrentUser = () => {
|
||||
const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace();
|
||||
|
||||
const client = useApolloClient();
|
||||
const store = useStore();
|
||||
|
||||
const loadCurrentUser = useCallback(async () => {
|
||||
const currentUserResult = await client.query({
|
||||
@@ -84,25 +95,42 @@ export const useLoadCurrentUser = () => {
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(user.deletedWorkspaceMembers)) {
|
||||
setCurrentWorkspaceDeletedMembers(user.deletedWorkspaceMembers);
|
||||
}
|
||||
|
||||
if (isDefined(user.workspaceMember)) {
|
||||
const memberLocale =
|
||||
(user.workspaceMember.locale as keyof typeof APP_LOCALES) ??
|
||||
SOURCE_LOCALE;
|
||||
|
||||
workspaceMember = {
|
||||
...user.workspaceMember,
|
||||
colorScheme: user.workspaceMember?.colorScheme as ColorScheme,
|
||||
locale: user.workspaceMember?.locale ?? SOURCE_LOCALE,
|
||||
colorScheme:
|
||||
(user.workspaceMember.colorScheme as ColorScheme) ?? 'System',
|
||||
locale: memberLocale,
|
||||
};
|
||||
|
||||
setCurrentWorkspaceMember(workspaceMember);
|
||||
|
||||
// Initialize unified format preferences state
|
||||
initializeFormatPreferences(workspaceMember);
|
||||
dynamicActivate(
|
||||
(workspaceMember.locale as keyof typeof APP_LOCALES) ?? SOURCE_LOCALE,
|
||||
);
|
||||
dynamicActivate(memberLocale);
|
||||
|
||||
const currentLocale = store.get(dateLocaleState.atom);
|
||||
|
||||
if (currentLocale.locale !== memberLocale) {
|
||||
getDateFnsLocale(memberLocale).then((localeCatalog) => {
|
||||
store.set(dateLocaleState.atom, {
|
||||
locale: memberLocale,
|
||||
localeCatalog: localeCatalog || enUS,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const workspace = isDefined(user.currentWorkspace)
|
||||
? {
|
||||
...user.currentWorkspace,
|
||||
defaultRole: user.currentWorkspace.defaultRole ?? null,
|
||||
workspaceCustomApplication:
|
||||
user.currentWorkspace.workspaceCustomApplication ?? null,
|
||||
}
|
||||
@@ -127,6 +155,8 @@ export const useLoadCurrentUser = () => {
|
||||
});
|
||||
}
|
||||
|
||||
setIsCurrentUserLoaded(true);
|
||||
|
||||
return {
|
||||
user,
|
||||
workspaceMember,
|
||||
@@ -134,13 +164,16 @@ export const useLoadCurrentUser = () => {
|
||||
};
|
||||
}, [
|
||||
client,
|
||||
store,
|
||||
setCurrentUser,
|
||||
setCurrentWorkspace,
|
||||
isOnAWorkspace,
|
||||
setCurrentWorkspaceMembers,
|
||||
setCurrentWorkspaceDeletedMembers,
|
||||
setAvailableWorkspaces,
|
||||
setCurrentUserWorkspace,
|
||||
setCurrentWorkspaceMember,
|
||||
setIsCurrentUserLoaded,
|
||||
initializeFormatPreferences,
|
||||
setLastAuthenticateWorkspaceDomain,
|
||||
authProviders,
|
||||
|
||||
@@ -84,7 +84,9 @@ const StandardContent = ({
|
||||
export const SignInUp = () => {
|
||||
const { t } = useLingui();
|
||||
const setSignInUpStep = useSetAtomState(signInUpStepState);
|
||||
const clientConfigApiStatus = useAtomStateValue(clientConfigApiStatusState);
|
||||
const { isLoaded: isClientConfigLoaded } = useAtomStateValue(
|
||||
clientConfigApiStatusState,
|
||||
);
|
||||
|
||||
const { form } = useSignInUpForm();
|
||||
const { signInUpStep } = useSignInUp(form);
|
||||
@@ -146,7 +148,7 @@ export const SignInUp = () => {
|
||||
]);
|
||||
|
||||
const signInUpForm = useMemo(() => {
|
||||
if (getPublicWorkspaceDataLoading || !clientConfigApiStatus.isLoadedOnce) {
|
||||
if (getPublicWorkspaceDataLoading || !isClientConfigLoaded) {
|
||||
return (
|
||||
<StyledLoaderContainer>
|
||||
<Loader color="gray" />
|
||||
@@ -194,7 +196,7 @@ export const SignInUp = () => {
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
clientConfigApiStatus.isLoadedOnce,
|
||||
isClientConfigLoaded,
|
||||
isDefaultDomain,
|
||||
isMultiWorkspaceEnabled,
|
||||
isOnAWorkspace,
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ export const SettingsAdminAiProviderDetail = () => {
|
||||
const { providerName } = useParams<{ providerName: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
|
||||
const { refetch: refetchClientConfig } = useClientConfig();
|
||||
const { fetchClientConfig: refetchClientConfig } = useClientConfig();
|
||||
const { openModal } = useModal();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [modelToRemove, setModelToRemove] = useState<{
|
||||
|
||||
@@ -20,7 +20,7 @@ import { IsMinimalMetadataReadyEffect } from '@/metadata-store/effect-components
|
||||
import { MinimalMetadataLoadEffect } from '@/metadata-store/effect-components/MinimalMetadataLoadEffect';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { useState } from 'react';
|
||||
import { ClientConfigProvider } from '~/modules/client-config/components/ClientConfigProvider';
|
||||
import { ClientConfigErrorGate } from '~/modules/client-config/components/ClientConfigErrorGate';
|
||||
import { mockedApolloClient } from '~/testing/mockedApolloClient';
|
||||
|
||||
import { MainContextStoreProvider } from '@/context-store/components/MainContextStoreProvider';
|
||||
@@ -85,7 +85,7 @@ const Providers = () => {
|
||||
<I18nProvider i18n={i18n}>
|
||||
<ApolloStorybookDevLogEffect />
|
||||
<ClientConfigProviderEffect />
|
||||
<ClientConfigProvider>
|
||||
<ClientConfigErrorGate>
|
||||
<UserMetadataProviderInitialEffect />
|
||||
<MinimalMetadataLoadEffect />
|
||||
<IsMinimalMetadataReadyEffect />
|
||||
@@ -106,7 +106,7 @@ const Providers = () => {
|
||||
<MainContextStoreProvider />
|
||||
</ApolloCoreClientMockedProvider>
|
||||
</MinimalMetadataGater>
|
||||
</ClientConfigProvider>
|
||||
</ClientConfigErrorGate>
|
||||
</I18nProvider>
|
||||
</ApolloProvider>
|
||||
</SnackBarComponentInstanceContext.Provider>
|
||||
|
||||
+2
-2
@@ -95,7 +95,7 @@ describe('RefreshTokenService', () => {
|
||||
jest
|
||||
.spyOn(appTokenRepository, 'findOneBy')
|
||||
.mockResolvedValue(mockAppToken);
|
||||
jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser);
|
||||
jest.spyOn(userRepository, 'findOneBy').mockResolvedValue(mockUser);
|
||||
jest.spyOn(twentyConfigService, 'get').mockReturnValue('1h');
|
||||
|
||||
const result = await service.verifyRefreshToken(mockToken);
|
||||
@@ -204,7 +204,7 @@ describe('RefreshTokenService', () => {
|
||||
|
||||
const user = { id: userId } as UserEntity;
|
||||
|
||||
jest.spyOn(userRepository, 'findOne').mockResolvedValue(user);
|
||||
jest.spyOn(userRepository, 'findOneBy').mockResolvedValue(user);
|
||||
|
||||
const out = await service.verifyRefreshToken(refreshToken);
|
||||
|
||||
|
||||
+5
-19
@@ -67,9 +67,8 @@ export class RefreshTokenService {
|
||||
);
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id: jwtPayload.sub },
|
||||
relations: ['appTokens'],
|
||||
const user = await this.userRepository.findOneBy({
|
||||
id: jwtPayload.sub,
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
@@ -84,23 +83,10 @@ export class RefreshTokenService {
|
||||
token.revokedAt.getTime() <= Date.now() - ms(reuseGracePeriod);
|
||||
|
||||
if (wasRevokedBeforeGracePeriod) {
|
||||
// Token was revoked long ago and is being reused -- suspicious.
|
||||
// Revoke all user refresh tokens as a safety measure.
|
||||
await Promise.all(
|
||||
user.appTokens.map(async ({ id, type }) => {
|
||||
if (type === AppTokenType.RefreshToken) {
|
||||
await this.appTokenRepository.update(
|
||||
{ id },
|
||||
{
|
||||
revokedAt: new Date(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// Reject the stale token but don't revoke all tokens — the most
|
||||
// common cause is a lost renewal response, not actual token theft.
|
||||
throw new AuthException(
|
||||
'Suspicious activity detected, this refresh token has been revoked. All tokens have been revoked.',
|
||||
'This refresh token has been revoked.',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user