review(): from PR #8656 (#8870)

This commit is contained in:
Antoine Moreaux 2024-12-05 10:46:13 +01:00 committed by GitHub
parent 99caab1412
commit 33e69805cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 176 additions and 179 deletions

View File

@ -22,6 +22,7 @@ import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProvide
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { Outlet, useLocation } from 'react-router-dom'; import { Outlet, useLocation } from 'react-router-dom';
import { getPageTitleFromPath } from '~/utils/title-utils'; import { getPageTitleFromPath } from '~/utils/title-utils';
import { PageFavicon } from '@/ui/utilities/page-favicon/components/PageFavicon';
export const AppRouterProviders = () => { export const AppRouterProviders = () => {
const { pathname } = useLocation(); const { pathname } = useLocation();
@ -49,6 +50,7 @@ export const AppRouterProviders = () => {
<PromiseRejectionEffect /> <PromiseRejectionEffect />
<GotoHotkeysEffectsProvider /> <GotoHotkeysEffectsProvider />
<PageTitle title={pageTitle} /> <PageTitle title={pageTitle} />
<PageFavicon />
<Outlet /> <Outlet />
</StrictMode> </StrictMode>
</DialogManager> </DialogManager>

View File

@ -31,7 +31,7 @@ import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
import { signInUpModeState } from '@/auth/states/signInUpModeState'; import { signInUpModeState } from '@/auth/states/signInUpModeState';
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken'; import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { useUrlManager } from '@/url-manager/hooks/useUrlManager'; import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
import { SignInUpMode } from '@/auth/types/signInUpMode.type'; import { SignInUpMode } from '@/auth/types/signInUpMode';
const StyledContentContainer = styled(motion.div)` const StyledContentContainer = styled(motion.div)`
margin-bottom: ${({ theme }) => theme.spacing(8)}; margin-bottom: ${({ theme }) => theme.spacing(8)};

View File

@ -5,7 +5,7 @@ import { motion } from 'framer-motion';
import { StyledText } from 'twenty-ui'; import { StyledText } from 'twenty-ui';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import { Form } from '@/auth/sign-in-up/hooks/useSignInUpForm'; import { Form } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { SignInUpMode } from '@/auth/types/signInUpMode.type'; import { SignInUpMode } from '@/auth/types/signInUpMode';
const StyledFullWidthMotionDiv = styled(motion.div)` const StyledFullWidthMotionDiv = styled(motion.div)`
width: 100%; width: 100%;

View File

@ -14,7 +14,7 @@ import { useState, useMemo } from 'react';
import { captchaProviderState } from '@/client-config/states/captchaProviderState'; import { captchaProviderState } from '@/client-config/states/captchaProviderState';
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState'; import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
import { FormProvider } from 'react-hook-form'; import { FormProvider } from 'react-hook-form';
import { SignInUpMode } from '@/auth/types/signInUpMode.type'; import { SignInUpMode } from '@/auth/types/signInUpMode';
const StyledForm = styled.form` const StyledForm = styled.form`
align-items: center; align-items: center;

View File

@ -4,15 +4,12 @@ import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { SignInUpStep } from '@/auth/states/signInUpStepState'; import { SignInUpStep } from '@/auth/states/signInUpStepState';
import { authProvidersState } from '@/client-config/states/authProvidersState'; import { authProvidersState } from '@/client-config/states/authProvidersState';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useCallback, useEffect } from 'react';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { ActionLink, HorizontalSeparator } from 'twenty-ui'; import { ActionLink, HorizontalSeparator } from 'twenty-ui';
import { SignInUpWithGoogle } from '@/auth/sign-in-up/components/SignInUpWithGoogle'; import { SignInUpWithGoogle } from '@/auth/sign-in-up/components/SignInUpWithGoogle';
import { SignInUpWithMicrosoft } from '@/auth/sign-in-up/components/SignInUpWithMicrosoft'; import { SignInUpWithMicrosoft } from '@/auth/sign-in-up/components/SignInUpWithMicrosoft';
import { SignInUpWithSSO } from '@/auth/sign-in-up/components/SignInUpWithSSO'; import { SignInUpWithSSO } from '@/auth/sign-in-up/components/SignInUpWithSSO';
import { SignInUpWithCredentials } from '@/auth/sign-in-up/components/SignInUpWithCredentials'; import { SignInUpWithCredentials } from '@/auth/sign-in-up/components/SignInUpWithCredentials';
import { useLocation } from 'react-router-dom';
import { isDefined } from '~/utils/isDefined';
const StyledContentContainer = styled.div` const StyledContentContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(8)}; margin-bottom: ${({ theme }) => theme.spacing(8)};
@ -25,38 +22,7 @@ export const SignInUpWorkspaceScopeForm = () => {
const { form } = useSignInUpForm(); const { form } = useSignInUpForm();
const { handleResetPassword } = useHandleResetPassword(); const { handleResetPassword } = useHandleResetPassword();
const { signInUpStep, continueWithEmail, continueWithCredentials } = const { signInUpStep } = useSignInUp(form);
useSignInUp(form);
const location = useLocation();
const checkAuthProviders = useCallback(() => {
if (
signInUpStep === SignInUpStep.Init &&
!authProviders.google &&
!authProviders.microsoft &&
!authProviders.sso
) {
return continueWithEmail();
}
const searchParams = new URLSearchParams(location.search);
const email = searchParams.get('email');
if (isDefined(email) && authProviders.password) {
return continueWithCredentials();
}
}, [
continueWithCredentials,
location.search,
authProviders.google,
authProviders.microsoft,
authProviders.password,
authProviders.sso,
continueWithEmail,
signInUpStep,
]);
useEffect(() => {
checkAuthProviders();
}, [checkAuthProviders]);
return ( return (
<> <>

View File

@ -0,0 +1,48 @@
import { SignInUpStep } from '@/auth/states/signInUpStepState';
import { isDefined } from '~/utils/isDefined';
import { useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { useRecoilState } from 'recoil';
import { useCallback, useEffect } from 'react';
const searchParams = new URLSearchParams(window.location.search);
const email = searchParams.get('email');
export const SignInUpWorkspaceScopeFormEffect = () => {
const [authProviders] = useRecoilState(authProvidersState);
const { form } = useSignInUpForm();
const { signInUpStep, continueWithEmail, continueWithCredentials } =
useSignInUp(form);
const checkAuthProviders = useCallback(() => {
if (
signInUpStep === SignInUpStep.Init &&
!authProviders.google &&
!authProviders.microsoft &&
!authProviders.sso
) {
return continueWithEmail();
}
if (isDefined(email) && authProviders.password) {
return continueWithCredentials();
}
}, [
signInUpStep,
authProviders.google,
authProviders.microsoft,
authProviders.sso,
authProviders.password,
continueWithEmail,
continueWithCredentials,
]);
useEffect(() => {
checkAuthProviders();
}, [checkAuthProviders]);
return <></>;
};

View File

@ -16,7 +16,7 @@ import {
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { useAuth } from '../../hooks/useAuth'; import { useAuth } from '../../hooks/useAuth';
import { signInUpModeState } from '@/auth/states/signInUpModeState'; import { signInUpModeState } from '@/auth/states/signInUpModeState';
import { SignInUpMode } from '@/auth/types/signInUpMode.type'; import { SignInUpMode } from '@/auth/types/signInUpMode';
export const useSignInUp = (form: UseFormReturn<Form>) => { export const useSignInUp = (form: UseFormReturn<Form>) => {
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();

View File

@ -58,8 +58,10 @@ export const useSignInUpForm = () => {
useEffect(() => { useEffect(() => {
if (isDefined(prefilledEmail)) { if (isDefined(prefilledEmail)) {
form.setValue('email', prefilledEmail); form.setValue('email', prefilledEmail);
} else if (isDeveloperDefaultSignInPrefilled === true) { }
form.setValue('email', 'tim@apple.dev');
if (isDeveloperDefaultSignInPrefilled === true) {
form.setValue('email', prefilledEmail ?? 'tim@apple.dev');
form.setValue('password', 'Applecar2025'); form.setValue('password', 'Applecar2025');
} }
}, [ }, [
@ -68,5 +70,5 @@ export const useSignInUpForm = () => {
prefilledEmail, prefilledEmail,
location.search, location.search,
]); ]);
return { form, validationSchema }; return { form: form, validationSchema };
}; };

View File

@ -1,5 +1,5 @@
import { createState } from 'twenty-ui'; import { createState } from 'twenty-ui';
import { SignInUpMode } from '@/auth/types/signInUpMode.type'; import { SignInUpMode } from '@/auth/types/signInUpMode';
export const signInUpModeState = createState<SignInUpMode>({ export const signInUpModeState = createState<SignInUpMode>({
key: 'signInUpModeState', key: 'signInUpModeState',

View File

@ -14,7 +14,7 @@ import { useRecoilValue, useRecoilState } from 'recoil';
import { IconKey } from 'twenty-ui'; import { IconKey } from 'twenty-ui';
import { useListSsoIdentityProvidersByWorkspaceIdQuery } from '~/generated/graphql'; import { useListSsoIdentityProvidersByWorkspaceIdQuery } from '~/generated/graphql';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state'; import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
const StyledLink = styled(Link, { const StyledLink = styled(Link, {

View File

@ -6,7 +6,7 @@ import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
import { SettingsPath } from '@/types/SettingsPath'; import { SettingsPath } from '@/types/SettingsPath';
import { SettingsListCard } from '@/settings/components/SettingsListCard'; import { SettingsListCard } from '@/settings/components/SettingsListCard';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state'; import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
export const SettingsSSOIdentitiesProvidersListCardWrapper = () => { export const SettingsSSOIdentitiesProvidersListCardWrapper = () => {

View File

@ -1,7 +1,7 @@
/* @license Enterprise */ /* @license Enterprise */
import { SettingsSecuritySSORowDropdownMenu } from '@/settings/security/components/SettingsSecuritySSORowDropdownMenu'; import { SettingsSecuritySSORowDropdownMenu } from '@/settings/security/components/SettingsSecuritySSORowDropdownMenu';
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state'; import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState';
import { getColorBySSOIdentityProviderStatus } from '@/settings/security/utils/getColorBySSOIdentityProviderStatus'; import { getColorBySSOIdentityProviderStatus } from '@/settings/security/utils/getColorBySSOIdentityProviderStatus';
import { Status } from 'twenty-ui'; import { Status } from 'twenty-ui';
import styled from '@emotion/styled'; import styled from '@emotion/styled';

View File

@ -14,7 +14,7 @@ import {
import { useUpdateWorkspaceMutation } from '~/generated/graphql'; import { useUpdateWorkspaceMutation } from '~/generated/graphql';
import { AuthProviders } from '~/generated-metadata/graphql'; import { AuthProviders } from '~/generated-metadata/graphql';
import { capitalize } from '~/utils/string/capitalize'; import { capitalize } from '~/utils/string/capitalize';
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state'; import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState';
const StyledSettingsSecurityOptionsList = styled.div` const StyledSettingsSecurityOptionsList = styled.div`
display: flex; display: flex;

View File

@ -10,7 +10,7 @@ import {
import { useDeleteSSOIdentityProvider } from '@/settings/security/hooks/useDeleteSSOIdentityProvider'; import { useDeleteSSOIdentityProvider } from '@/settings/security/hooks/useDeleteSSOIdentityProvider';
import { useUpdateSSOIdentityProvider } from '@/settings/security/hooks/useUpdateSSOIdentityProvider'; import { useUpdateSSOIdentityProvider } from '@/settings/security/hooks/useUpdateSSOIdentityProvider';
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state'; import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';

View File

@ -1,6 +1,6 @@
/* @license Enterprise */ /* @license Enterprise */
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state'; import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState';
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { import {
CreateOidcIdentityProviderMutationVariables, CreateOidcIdentityProviderMutationVariables,

View File

@ -1,6 +1,6 @@
/* @license Enterprise */ /* @license Enterprise */
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state'; import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState';
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { import {
DeleteSsoIdentityProviderMutationVariables, DeleteSsoIdentityProviderMutationVariables,

View File

@ -1,6 +1,6 @@
/* @license Enterprise */ /* @license Enterprise */
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state'; import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState';
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { import {
EditSsoIdentityProviderMutationVariables, EditSsoIdentityProviderMutationVariables,

View File

@ -1,4 +0,0 @@
export type AuthProvidersKeys =
| 'isGoogleAuthEnabled'
| 'isMicrosoftAuthEnabled'
| 'isPasswordAuthEnabled';

View File

@ -0,0 +1,20 @@
import { Helmet } from 'react-helmet-async';
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
import { useRecoilValue } from 'recoil';
import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
export const PageFavicon = () => {
const workspacePublicData = useRecoilValue(workspacePublicDataState);
return (
<Helmet>
{workspacePublicData?.logo && (
<link
rel="icon"
type="image/x-icon"
href={getImageAbsoluteURI(workspacePublicData.logo)}
/>
)}
</Helmet>
);
};

View File

@ -76,21 +76,5 @@ export const WorkspaceProviderEffect = () => {
redirectToWorkspace, redirectToWorkspace,
]); ]);
useEffect(() => {
try {
if (isDefined(workspacePublicData?.logo)) {
const link: HTMLLinkElement =
document.querySelector("link[rel*='icon']") ||
document.createElement('link');
link.rel = 'icon';
link.href = workspacePublicData.logo;
document.getElementsByTagName('head')[0].appendChild(link);
}
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
}
}, [workspacePublicData]);
return <></>; return <></>;
}; };

View File

@ -17,6 +17,7 @@ import {
} from '~/generated/graphql'; } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { currentUserState } from '@/auth/states/currentUserState'; import { currentUserState } from '@/auth/states/currentUserState';
import { SignInUpWorkspaceScopeFormEffect } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeFormEffect';
const StyledContentContainer = styled.div` const StyledContentContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(8)}; margin-bottom: ${({ theme }) => theme.spacing(8)};
@ -93,7 +94,10 @@ export const Invite = () => {
<FooterNote /> <FooterNote />
</> </>
) : ( ) : (
<>
<SignInUpWorkspaceScopeFormEffect />
<SignInUpWorkspaceScopeForm /> <SignInUpWorkspaceScopeForm />
</>
)} )}
</> </>
); );

View File

@ -17,6 +17,7 @@ import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWork
import { useUrlManager } from '@/url-manager/hooks/useUrlManager'; import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { SignInUpWorkspaceScopeFormEffect } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeFormEffect';
export const SignInUp = () => { export const SignInUp = () => {
const { form } = useSignInUpForm(); const { form } = useSignInUpForm();
@ -43,7 +44,12 @@ export const SignInUp = () => {
isDefined(workspacePublicData) && isDefined(workspacePublicData) &&
(!isMultiWorkspaceEnabled || isTwentyWorkspaceSubdomain) (!isMultiWorkspaceEnabled || isTwentyWorkspaceSubdomain)
) { ) {
return <SignInUpWorkspaceScopeForm />; return (
<>
<SignInUpWorkspaceScopeFormEffect />
<SignInUpWorkspaceScopeForm />
</>
);
} }
return <SignInUpGlobalScopeForm />; return <SignInUpGlobalScopeForm />;

View File

@ -38,7 +38,6 @@ import {
} from 'src/engine/core-modules/auth/auth.exception'; } from 'src/engine/core-modules/auth/auth.exception';
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator'; import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
import { AvailableWorkspaceOutput } from 'src/engine/core-modules/auth/dto/available-workspaces.output'; import { AvailableWorkspaceOutput } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { ChallengeInput } from './dto/challenge.input'; import { ChallengeInput } from './dto/challenge.input';
@ -129,13 +128,7 @@ export class AuthResolver {
targetWorkspaceSubdomain: targetWorkspaceSubdomain:
this.domainManagerService.getWorkspaceSubdomainByOrigin(origin), this.domainManagerService.getWorkspaceSubdomainByOrigin(origin),
fromSSO: false, fromSSO: false,
isAuthEnabled: workspaceValidator.isAuthEnabled( authProvider: 'password',
'password',
new AuthException(
'Password auth is not enabled for this workspace',
AuthExceptionCode.OAUTH_ACCESS_DENIED,
),
),
}); });
const loginToken = await this.loginTokenService.generateLoginToken( const loginToken = await this.loginTokenService.generateLoginToken(

View File

@ -18,13 +18,9 @@ import { GoogleProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service'; import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
import { GoogleRequest } from 'src/engine/core-modules/auth/strategies/google.auth.strategy'; import { GoogleRequest } from 'src/engine/core-modules/auth/strategies/google.auth.strategy';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Controller('auth/google') @Controller('auth/google')
@ -70,13 +66,7 @@ export class GoogleAuthController {
workspacePersonalInviteToken, workspacePersonalInviteToken,
targetWorkspaceSubdomain, targetWorkspaceSubdomain,
fromSSO: true, fromSSO: true,
isAuthEnabled: workspaceValidator.isAuthEnabled( isAuthEnabled: 'google',
'google',
new AuthException(
'Google auth is not enabled for this workspace',
AuthExceptionCode.OAUTH_ACCESS_DENIED,
),
),
}; };
if ( if (

View File

@ -15,12 +15,8 @@ import { MicrosoftProviderEnabledGuard } from 'src/engine/core-modules/auth/guar
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service'; import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
import { MicrosoftRequest } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy'; import { MicrosoftRequest } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
@Controller('auth/microsoft') @Controller('auth/microsoft')
@ -66,13 +62,7 @@ export class MicrosoftAuthController {
workspacePersonalInviteToken, workspacePersonalInviteToken,
targetWorkspaceSubdomain, targetWorkspaceSubdomain,
fromSSO: true, fromSSO: true,
isAuthEnabled: workspaceValidator.isAuthEnabled( authProvider: 'microsoft',
'microsoft',
new AuthException(
'Microsoft auth is not enabled for this workspace',
AuthExceptionCode.OAUTH_ACCESS_DENIED,
),
),
}); });
const loginToken = await this.loginTokenService.generateLoginToken( const loginToken = await this.loginTokenService.generateLoginToken(

View File

@ -45,9 +45,9 @@ import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/use
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service'; import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
import { AvailableWorkspaceOutput } from 'src/engine/core-modules/auth/dto/available-workspaces.output'; import { AvailableWorkspaceOutput } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
import { UserService } from 'src/engine/core-modules/user/services/user.service'; import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
import { userValidator } from 'src/engine/core-modules/user/user.validate'; import { userValidator } from 'src/engine/core-modules/user/user.validate';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { WorkspaceAuthProvider } from 'src/engine/core-modules/workspace/types/workspace.type';
@Injectable() @Injectable()
// eslint-disable-next-line @nx/workspace-inject-workspace-repository // eslint-disable-next-line @nx/workspace-inject-workspace-repository
@ -161,7 +161,7 @@ export class AuthService {
lastName, lastName,
picture, picture,
fromSSO, fromSSO,
isAuthEnabled, authProvider,
}: { }: {
email: string; email: string;
password?: string; password?: string;
@ -172,7 +172,7 @@ export class AuthService {
picture?: string | null; picture?: string | null;
fromSSO: boolean; fromSSO: boolean;
targetWorkspaceSubdomain?: string; targetWorkspaceSubdomain?: string;
isAuthEnabled?: ReturnType<(typeof workspaceValidator)['isAuthEnabled']>; authProvider?: WorkspaceAuthProvider;
}) { }) {
return await this.signInUpService.signInUp({ return await this.signInUpService.signInUp({
email, email,
@ -184,7 +184,7 @@ export class AuthService {
targetWorkspaceSubdomain, targetWorkspaceSubdomain,
picture, picture,
fromSSO, fromSSO,
isAuthEnabled, authProvider,
}); });
} }
@ -201,7 +201,7 @@ export class AuthService {
where: { email }, where: { email },
}); });
userValidator.assertIsExist( userValidator.assertIsDefinedOrThrow(
userWithIdAndDefaultWorkspaceId, userWithIdAndDefaultWorkspaceId,
new AuthException('User not found', AuthExceptionCode.USER_NOT_FOUND), new AuthException('User not found', AuthExceptionCode.USER_NOT_FOUND),
); );
@ -210,7 +210,7 @@ export class AuthService {
workspaceId && workspaceId &&
userWithIdAndDefaultWorkspaceId.defaultWorkspaceId !== workspaceId userWithIdAndDefaultWorkspaceId.defaultWorkspaceId !== workspaceId
) { ) {
await this.userService.saveDefaultWorkspace( await this.userService.saveDefaultWorkspaceIfUserHasAccessOrThrow(
userWithIdAndDefaultWorkspaceId.id, userWithIdAndDefaultWorkspaceId.id,
workspaceId, workspaceId,
); );
@ -223,7 +223,7 @@ export class AuthService {
relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'], relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'],
}); });
userValidator.assertIsExist( userValidator.assertIsDefinedOrThrow(
user, user,
new AuthException('User not found', AuthExceptionCode.USER_NOT_FOUND), new AuthException('User not found', AuthExceptionCode.USER_NOT_FOUND),
); );
@ -254,7 +254,7 @@ export class AuthService {
email, email,
}); });
if (userValidator.isExist(user)) { if (userValidator.isDefined(user)) {
return { return {
exists: true, exists: true,
availableWorkspaces: await this.findAvailableWorkspacesByEmail(email), availableWorkspaces: await this.findAvailableWorkspacesByEmail(email),
@ -460,7 +460,7 @@ export class AuthService {
], ],
}); });
userValidator.assertIsExist( userValidator.assertIsDefinedOrThrow(
user, user,
new AuthException('User not found', AuthExceptionCode.USER_NOT_FOUND), new AuthException('User not found', AuthExceptionCode.USER_NOT_FOUND),
); );

View File

@ -32,6 +32,7 @@ import {
} from 'src/engine/core-modules/workspace/workspace.entity'; } from 'src/engine/core-modules/workspace/workspace.entity';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
import { getImageBufferFromUrl } from 'src/utils/image'; import { getImageBufferFromUrl } from 'src/utils/image';
import { WorkspaceAuthProvider } from 'src/engine/core-modules/workspace/types/workspace.type';
export type SignInUpServiceInput = { export type SignInUpServiceInput = {
email: string; email: string;
@ -43,7 +44,7 @@ export type SignInUpServiceInput = {
picture?: string | null; picture?: string | null;
fromSSO: boolean; fromSSO: boolean;
targetWorkspaceSubdomain?: string; targetWorkspaceSubdomain?: string;
isAuthEnabled?: ReturnType<(typeof workspaceValidator)['isAuthEnabled']>; authProvider?: WorkspaceAuthProvider;
}; };
@Injectable() @Injectable()
@ -73,7 +74,7 @@ export class SignInUpService {
picture, picture,
fromSSO, fromSSO,
targetWorkspaceSubdomain, targetWorkspaceSubdomain,
isAuthEnabled, authProvider,
}: SignInUpServiceInput) { }: SignInUpServiceInput) {
if (!firstName) firstName = ''; if (!firstName) firstName = '';
if (!lastName) lastName = ''; if (!lastName) lastName = '';
@ -154,7 +155,7 @@ export class SignInUpService {
lastName, lastName,
picture, picture,
existingUser, existingUser,
isAuthEnabled, authProvider,
}); });
await this.workspaceInvitationService.invalidateWorkspaceInvitation( await this.workspaceInvitationService.invalidateWorkspaceInvitation(
@ -187,7 +188,7 @@ export class SignInUpService {
lastName, lastName,
picture, picture,
existingUser, existingUser,
isAuthEnabled, authProvider,
}: { }: {
email: string; email: string;
passwordHash: string | undefined; passwordHash: string | undefined;
@ -196,7 +197,7 @@ export class SignInUpService {
lastName: string; lastName: string;
picture: SignInUpServiceInput['picture']; picture: SignInUpServiceInput['picture'];
existingUser: User | null; existingUser: User | null;
isAuthEnabled?: ReturnType<(typeof workspaceValidator)['isAuthEnabled']>; authProvider?: WorkspaceAuthProvider;
}) { }) {
const isNewUser = !isDefined(existingUser); const isNewUser = !isDefined(existingUser);
let user = existingUser; let user = existingUser;
@ -217,8 +218,16 @@ export class SignInUpService {
), ),
); );
if (isAuthEnabled) if (authProvider) {
workspaceValidator.validateAuth(isAuthEnabled, workspace); workspaceValidator.isAuthEnabledOrThrow(
authProvider,
workspace,
new AuthException(
`${authProvider} auth is not enabled for this workspace`,
AuthExceptionCode.OAUTH_ACCESS_DENIED,
),
);
}
if (isNewUser) { if (isNewUser) {
const imagePath = await this.uploadPicture(picture, workspace.id); const imagePath = await this.uploadPicture(picture, workspace.id);
@ -236,7 +245,7 @@ export class SignInUpService {
user = await this.userRepository.save(userToCreate); user = await this.userRepository.save(userToCreate);
} }
userValidator.assertIsExist( userValidator.assertIsDefinedOrThrow(
user, user,
new AuthException( new AuthException(
'User not found', 'User not found',

View File

@ -47,7 +47,7 @@ describe('SwitchWorkspaceService', () => {
{ {
provide: UserService, provide: UserService,
useValue: { useValue: {
saveDefaultWorkspace: jest.fn(), saveDefaultWorkspaceIfUserHasAccessOrThrow: jest.fn(),
}, },
}, },
], ],
@ -211,10 +211,9 @@ describe('SwitchWorkspaceService', () => {
refreshToken: mockRefreshToken, refreshToken: mockRefreshToken,
}, },
}); });
expect(userService.saveDefaultWorkspace).toHaveBeenCalledWith( expect(
mockUser.id, userService.saveDefaultWorkspaceIfUserHasAccessOrThrow,
mockWorkspace.id, ).toHaveBeenCalledWith(mockUser.id, mockWorkspace.id);
);
expect(accessTokenService.generateAccessToken).toHaveBeenCalledWith( expect(accessTokenService.generateAccessToken).toHaveBeenCalledWith(
mockUser.id, mockUser.id,
mockWorkspace.id, mockWorkspace.id,

View File

@ -78,7 +78,10 @@ export class SwitchWorkspaceService {
user: User, user: User,
workspace: Workspace, workspace: Workspace,
): Promise<AuthTokens> { ): Promise<AuthTokens> {
await this.userService.saveDefaultWorkspace(user.id, workspace.id); await this.userService.saveDefaultWorkspaceIfUserHasAccessOrThrow(
user.id,
workspace.id,
);
const token = await this.accessTokenService.generateAccessToken( const token = await this.accessTokenService.generateAccessToken(
user.id, user.id,

View File

@ -135,7 +135,7 @@ export class UserService extends TypeOrmQueryService<User> {
return user; return user;
} }
async saveDefaultWorkspace(userId: string, workspaceId: string) { async hasUserAccessToWorkspaceOrThrow(userId: string, workspaceId: string) {
const user = await this.userRepository.findOne({ const user = await this.userRepository.findOne({
where: { where: {
id: userId, id: userId,
@ -146,13 +146,20 @@ export class UserService extends TypeOrmQueryService<User> {
relations: ['workspaces'], relations: ['workspaces'],
}); });
userValidator.assertIsExist( userValidator.assertIsDefinedOrThrow(
user, user,
new AuthException( new AuthException(
'User does not have access to this workspace', 'User does not have access to this workspace',
AuthExceptionCode.FORBIDDEN_EXCEPTION, AuthExceptionCode.FORBIDDEN_EXCEPTION,
), ),
); );
}
async saveDefaultWorkspaceIfUserHasAccessOrThrow(
userId: string,
workspaceId: string,
) {
await this.hasUserAccessToWorkspaceOrThrow(userId, workspaceId);
return await this.userRepository.save({ return await this.userRepository.save({
id: userId, id: userId,

View File

@ -77,7 +77,10 @@ export class UserResolver {
this.environmentService.get('IS_MULTIWORKSPACE_ENABLED') && this.environmentService.get('IS_MULTIWORKSPACE_ENABLED') &&
workspaceId workspaceId
) { ) {
await this.userService.saveDefaultWorkspace(userId, workspaceId); await this.userService.saveDefaultWorkspaceIfUserHasAccessOrThrow(
userId,
workspaceId,
);
} }
const user = await this.userRepository.findOne({ const user = await this.userRepository.findOne({
@ -87,7 +90,7 @@ export class UserResolver {
relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'], relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'],
}); });
userValidator.assertIsExist( userValidator.assertIsDefinedOrThrow(
user, user,
new AuthException('User not found', AuthExceptionCode.USER_NOT_FOUND), new AuthException('User not found', AuthExceptionCode.USER_NOT_FOUND),
); );

View File

@ -1,34 +1,24 @@
import { User } from 'src/engine/core-modules/user/user.entity'; import { User } from 'src/engine/core-modules/user/user.entity';
import { CustomException } from 'src/utils/custom-exception'; import { CustomException } from 'src/utils/custom-exception';
import { isDefined } from 'src/utils/is-defined';
const assertIsExist = ( const assertIsDefinedOrThrow = (
user: User | undefined | null, user: User | undefined | null,
exceptionToThrow: CustomException, exceptionToThrow: CustomException,
): asserts user is User => { ): asserts user is User => {
if (!user) { if (!isDefined(user)) {
throw exceptionToThrow; throw exceptionToThrow;
} }
}; };
const isExist = (user: User | undefined | null): user is User => { const isUserDefined = (user: User | undefined | null): user is User => {
return !!user; return isDefined(user);
};
const assertHasDefaultWorkspace = (
user: User,
exceptionToThrow?: CustomException,
): asserts user is User & { defaultWorkspaceId: string } => {
if (!user.defaultWorkspaceId) {
throw exceptionToThrow;
}
}; };
export const userValidator: { export const userValidator: {
assertIsExist: typeof assertIsExist; assertIsDefinedOrThrow: typeof assertIsDefinedOrThrow;
assertHasDefaultWorkspace: typeof assertHasDefaultWorkspace; isDefined: typeof isUserDefined;
isExist: typeof isExist;
} = { } = {
assertIsExist, assertIsDefinedOrThrow,
assertHasDefaultWorkspace, isDefined: isUserDefined,
isExist,
}; };

View File

@ -0,0 +1 @@
export type WorkspaceAuthProvider = 'google' | 'microsoft' | 'password';

View File

@ -3,8 +3,8 @@ import {
WorkspaceActivationStatus, WorkspaceActivationStatus,
} from 'src/engine/core-modules/workspace/workspace.entity'; } from 'src/engine/core-modules/workspace/workspace.entity';
import { CustomException } from 'src/utils/custom-exception'; import { CustomException } from 'src/utils/custom-exception';
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
type WorkspaceAuthProvider = 'google' | 'microsoft' | 'password'; import { WorkspaceAuthProvider } from 'src/engine/core-modules/workspace/types/workspace.type';
const assertIsExist = ( const assertIsExist = (
workspace: Workspace | undefined | null, workspace: Workspace | undefined | null,
@ -25,40 +25,24 @@ const assertIsActive = (
throw exceptionToThrow; throw exceptionToThrow;
}; };
type IsAuthEnabled = <P extends WorkspaceAuthProvider>( const isAuthEnabledOrThrow = (
provider: P, provider: WorkspaceAuthProvider,
exceptionToThrow: CustomException,
) => (
workspace: Workspace, workspace: Workspace,
exceptionToThrowCustom?: CustomException, exceptionToThrowCustom: AuthException,
) => boolean; ) => {
const isAuthEnabled: IsAuthEnabled = (provider, exceptionToThrow) => {
return (workspace, exceptionToThrowCustom = exceptionToThrow) => {
if (provider === 'google' && workspace.isGoogleAuthEnabled) return true; if (provider === 'google' && workspace.isGoogleAuthEnabled) return true;
if (provider === 'microsoft' && workspace.isMicrosoftAuthEnabled) if (provider === 'microsoft' && workspace.isMicrosoftAuthEnabled) return true;
return true;
if (provider === 'password' && workspace.isPasswordAuthEnabled) return true; if (provider === 'password' && workspace.isPasswordAuthEnabled) return true;
if (exceptionToThrowCustom) {
throw exceptionToThrowCustom; throw exceptionToThrowCustom;
}
return false;
}; };
};
const validateAuth = (fn: ReturnType<IsAuthEnabled>, workspace: Workspace) =>
fn(workspace);
export const workspaceValidator: { export const workspaceValidator: {
assertIsExist: typeof assertIsExist; assertIsExist: typeof assertIsExist;
assertIsActive: typeof assertIsActive; assertIsActive: typeof assertIsActive;
isAuthEnabled: IsAuthEnabled; isAuthEnabledOrThrow: typeof isAuthEnabledOrThrow;
validateAuth: typeof validateAuth;
} = { } = {
assertIsExist: assertIsExist, assertIsExist: assertIsExist,
assertIsActive: assertIsActive, assertIsActive: assertIsActive,
isAuthEnabled, isAuthEnabledOrThrow: isAuthEnabledOrThrow,
validateAuth,
}; };