fix(auth): handle missing invitation during sign-up (#9572)
Add validation to throw an exception when a sign-up is attempted without a valid invitation. Updated the test suite to cover this case and ensure proper error handling with appropriate exceptions. Fix https://github.com/twentyhq/twenty/issues/9566 https://github.com/twentyhq/twenty/issues/9564
This commit is contained in:
@ -1,5 +1,5 @@
|
|||||||
import * as Apollo from '@apollo/client';
|
|
||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
|
import * as Apollo from '@apollo/client';
|
||||||
export type Maybe<T> = T | null;
|
export type Maybe<T> = T | null;
|
||||||
export type InputMaybe<T> = Maybe<T>;
|
export type InputMaybe<T> = Maybe<T>;
|
||||||
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||||
@ -715,6 +715,7 @@ export type MutationSignUpArgs = {
|
|||||||
captchaToken?: InputMaybe<Scalars['String']>;
|
captchaToken?: InputMaybe<Scalars['String']>;
|
||||||
email: Scalars['String'];
|
email: Scalars['String'];
|
||||||
password: Scalars['String'];
|
password: Scalars['String'];
|
||||||
|
workspaceId?: InputMaybe<Scalars['String']>;
|
||||||
workspaceInviteHash?: InputMaybe<Scalars['String']>;
|
workspaceInviteHash?: InputMaybe<Scalars['String']>;
|
||||||
workspacePersonalInviteToken?: InputMaybe<Scalars['String']>;
|
workspacePersonalInviteToken?: InputMaybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
@ -1977,6 +1978,7 @@ export type SignUpMutationVariables = Exact<{
|
|||||||
workspaceInviteHash?: InputMaybe<Scalars['String']>;
|
workspaceInviteHash?: InputMaybe<Scalars['String']>;
|
||||||
workspacePersonalInviteToken?: InputMaybe<Scalars['String']>;
|
workspacePersonalInviteToken?: InputMaybe<Scalars['String']>;
|
||||||
captchaToken?: InputMaybe<Scalars['String']>;
|
captchaToken?: InputMaybe<Scalars['String']>;
|
||||||
|
workspaceId?: InputMaybe<Scalars['String']>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
@ -2989,13 +2991,14 @@ export type RenewTokenMutationHookResult = ReturnType<typeof useRenewTokenMutati
|
|||||||
export type RenewTokenMutationResult = Apollo.MutationResult<RenewTokenMutation>;
|
export type RenewTokenMutationResult = Apollo.MutationResult<RenewTokenMutation>;
|
||||||
export type RenewTokenMutationOptions = Apollo.BaseMutationOptions<RenewTokenMutation, RenewTokenMutationVariables>;
|
export type RenewTokenMutationOptions = Apollo.BaseMutationOptions<RenewTokenMutation, RenewTokenMutationVariables>;
|
||||||
export const SignUpDocument = gql`
|
export const SignUpDocument = gql`
|
||||||
mutation SignUp($email: String!, $password: String!, $workspaceInviteHash: String, $workspacePersonalInviteToken: String = null, $captchaToken: String) {
|
mutation SignUp($email: String!, $password: String!, $workspaceInviteHash: String, $workspacePersonalInviteToken: String = null, $captchaToken: String, $workspaceId: String) {
|
||||||
signUp(
|
signUp(
|
||||||
email: $email
|
email: $email
|
||||||
password: $password
|
password: $password
|
||||||
workspaceInviteHash: $workspaceInviteHash
|
workspaceInviteHash: $workspaceInviteHash
|
||||||
workspacePersonalInviteToken: $workspacePersonalInviteToken
|
workspacePersonalInviteToken: $workspacePersonalInviteToken
|
||||||
captchaToken: $captchaToken
|
captchaToken: $captchaToken
|
||||||
|
workspaceId: $workspaceId
|
||||||
) {
|
) {
|
||||||
loginToken {
|
loginToken {
|
||||||
...AuthTokenFragment
|
...AuthTokenFragment
|
||||||
@ -3027,6 +3030,7 @@ export type SignUpMutationFn = Apollo.MutationFunction<SignUpMutation, SignUpMut
|
|||||||
* workspaceInviteHash: // value for 'workspaceInviteHash'
|
* workspaceInviteHash: // value for 'workspaceInviteHash'
|
||||||
* workspacePersonalInviteToken: // value for 'workspacePersonalInviteToken'
|
* workspacePersonalInviteToken: // value for 'workspacePersonalInviteToken'
|
||||||
* captchaToken: // value for 'captchaToken'
|
* captchaToken: // value for 'captchaToken'
|
||||||
|
* workspaceId: // value for 'workspaceId'
|
||||||
* },
|
* },
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -7,6 +7,7 @@ export const SIGN_UP = gql`
|
|||||||
$workspaceInviteHash: String
|
$workspaceInviteHash: String
|
||||||
$workspacePersonalInviteToken: String = null
|
$workspacePersonalInviteToken: String = null
|
||||||
$captchaToken: String
|
$captchaToken: String
|
||||||
|
$workspaceId: String
|
||||||
) {
|
) {
|
||||||
signUp(
|
signUp(
|
||||||
email: $email
|
email: $email
|
||||||
@ -14,6 +15,7 @@ export const SIGN_UP = gql`
|
|||||||
workspaceInviteHash: $workspaceInviteHash
|
workspaceInviteHash: $workspaceInviteHash
|
||||||
workspacePersonalInviteToken: $workspacePersonalInviteToken
|
workspacePersonalInviteToken: $workspacePersonalInviteToken
|
||||||
captchaToken: $captchaToken
|
captchaToken: $captchaToken
|
||||||
|
workspaceId: $workspaceId
|
||||||
) {
|
) {
|
||||||
loginToken {
|
loginToken {
|
||||||
...AuthTokenFragment
|
...AuthTokenFragment
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
useSetRecoilState,
|
useSetRecoilState,
|
||||||
} from 'recoil';
|
} from 'recoil';
|
||||||
import { iconsState } from 'twenty-ui';
|
import { iconsState } from 'twenty-ui';
|
||||||
|
import { AppPath } from '@/types/AppPath';
|
||||||
|
|
||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||||
@ -47,13 +48,12 @@ import { BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type
|
|||||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||||
import { useIsCurrentLocationOnAWorkspaceSubdomain } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspaceSubdomain';
|
import { useIsCurrentLocationOnAWorkspaceSubdomain } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspaceSubdomain';
|
||||||
import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain';
|
import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain';
|
||||||
import { useReadWorkspaceSubdomainFromCurrentLocation } from '@/domain-manager/hooks/useReadWorkspaceSubdomainFromCurrentLocation';
|
|
||||||
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
|
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
|
||||||
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
||||||
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
|
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
|
||||||
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
|
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
|
||||||
import { AppPath } from '@/types/AppPath';
|
|
||||||
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
|
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
|
||||||
|
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
|
||||||
|
|
||||||
export const useAuth = () => {
|
export const useAuth = () => {
|
||||||
const setTokenPair = useSetRecoilState(tokenPairState);
|
const setTokenPair = useSetRecoilState(tokenPairState);
|
||||||
@ -82,7 +82,8 @@ export const useAuth = () => {
|
|||||||
|
|
||||||
const { isOnAWorkspaceSubdomain } =
|
const { isOnAWorkspaceSubdomain } =
|
||||||
useIsCurrentLocationOnAWorkspaceSubdomain();
|
useIsCurrentLocationOnAWorkspaceSubdomain();
|
||||||
const { workspaceSubdomain } = useReadWorkspaceSubdomainFromCurrentLocation();
|
|
||||||
|
const workspacePublicData = useRecoilValue(workspacePublicDataState);
|
||||||
|
|
||||||
const { setLastAuthenticateWorkspaceDomain } =
|
const { setLastAuthenticateWorkspaceDomain } =
|
||||||
useLastAuthenticatedWorkspaceDomain();
|
useLastAuthenticatedWorkspaceDomain();
|
||||||
@ -328,6 +329,9 @@ export const useAuth = () => {
|
|||||||
workspaceInviteHash,
|
workspaceInviteHash,
|
||||||
workspacePersonalInviteToken,
|
workspacePersonalInviteToken,
|
||||||
captchaToken,
|
captchaToken,
|
||||||
|
...(workspacePublicData?.id
|
||||||
|
? { workspaceId: workspacePublicData.id }
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -354,6 +358,7 @@ export const useAuth = () => {
|
|||||||
[
|
[
|
||||||
setIsVerifyPendingState,
|
setIsVerifyPendingState,
|
||||||
signUp,
|
signUp,
|
||||||
|
workspacePublicData,
|
||||||
isMultiWorkspaceEnabled,
|
isMultiWorkspaceEnabled,
|
||||||
handleVerify,
|
handleVerify,
|
||||||
redirectToWorkspaceDomain,
|
redirectToWorkspaceDomain,
|
||||||
@ -386,13 +391,13 @@ export const useAuth = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDefined(workspaceSubdomain)) {
|
if (isDefined(workspacePublicData)) {
|
||||||
url.searchParams.set('workspaceSubdomain', workspaceSubdomain);
|
url.searchParams.set('workspaceId', workspacePublicData.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return url.toString();
|
return url.toString();
|
||||||
},
|
},
|
||||||
[workspaceSubdomain],
|
[workspacePublicData],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleGoogleLogin = useCallback(
|
const handleGoogleLogin = useCallback(
|
||||||
|
|||||||
@ -43,6 +43,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
|
|||||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||||
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
|
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
|
||||||
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
||||||
|
import { SocialSsoService } from 'src/engine/core-modules/auth/services/social-sso.service';
|
||||||
|
|
||||||
import { AuthResolver } from './auth.resolver';
|
import { AuthResolver } from './auth.resolver';
|
||||||
|
|
||||||
@ -103,6 +104,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
|||||||
SwitchWorkspaceService,
|
SwitchWorkspaceService,
|
||||||
TransientTokenService,
|
TransientTokenService,
|
||||||
ApiKeyService,
|
ApiKeyService,
|
||||||
|
SocialSsoService,
|
||||||
// reenable when working on: https://github.com/twentyhq/twenty/issues/9143
|
// reenable when working on: https://github.com/twentyhq/twenty/issues/9143
|
||||||
// OAuthService,
|
// OAuthService,
|
||||||
],
|
],
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import { UseFilters, UseGuards } from '@nestjs/common';
|
import { UseFilters, UseGuards } from '@nestjs/common';
|
||||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { ApiKeyTokenInput } from 'src/engine/core-modules/auth/dto/api-key-token.input';
|
import { ApiKeyTokenInput } from 'src/engine/core-modules/auth/dto/api-key-token.input';
|
||||||
import { AppTokenInput } from 'src/engine/core-modules/auth/dto/app-token.input';
|
import { AppTokenInput } from 'src/engine/core-modules/auth/dto/app-token.input';
|
||||||
@ -55,6 +58,8 @@ import { AuthService } from './services/auth.service';
|
|||||||
@UseFilters(AuthGraphqlApiExceptionFilter)
|
@UseFilters(AuthGraphqlApiExceptionFilter)
|
||||||
export class AuthResolver {
|
export class AuthResolver {
|
||||||
constructor(
|
constructor(
|
||||||
|
@InjectRepository(User, 'core')
|
||||||
|
private readonly userRepository: Repository<User>,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private renewTokenService: RenewTokenService,
|
private renewTokenService: RenewTokenService,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
@ -104,12 +109,14 @@ export class AuthResolver {
|
|||||||
origin,
|
origin,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!workspace) {
|
workspaceValidator.assertIsDefinedOrThrow(
|
||||||
throw new AuthException(
|
workspace,
|
||||||
|
new AuthException(
|
||||||
'Workspace not found',
|
'Workspace not found',
|
||||||
AuthExceptionCode.WORKSPACE_NOT_FOUND,
|
AuthExceptionCode.WORKSPACE_NOT_FOUND,
|
||||||
);
|
),
|
||||||
}
|
);
|
||||||
|
|
||||||
const user = await this.authService.challenge(challengeInput, workspace);
|
const user = await this.authService.challenge(challengeInput, workspace);
|
||||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||||
user.email,
|
user.email,
|
||||||
@ -121,16 +128,46 @@ export class AuthResolver {
|
|||||||
|
|
||||||
@UseGuards(CaptchaGuard)
|
@UseGuards(CaptchaGuard)
|
||||||
@Mutation(() => SignUpOutput)
|
@Mutation(() => SignUpOutput)
|
||||||
async signUp(
|
async signUp(@Args() signUpInput: SignUpInput): Promise<SignUpOutput> {
|
||||||
@Args() signUpInput: SignUpInput,
|
const currentWorkspace = await this.authService.findWorkspaceForSignInUp({
|
||||||
@OriginHeader() origin: string,
|
workspaceInviteHash: signUpInput.workspaceInviteHash,
|
||||||
): Promise<SignUpOutput> {
|
|
||||||
const { user, workspace } = await this.authService.signInUp({
|
|
||||||
...signUpInput,
|
|
||||||
targetWorkspaceSubdomain:
|
|
||||||
this.domainManagerService.getWorkspaceSubdomainByOrigin(origin),
|
|
||||||
fromSSO: false,
|
|
||||||
authProvider: 'password',
|
authProvider: 'password',
|
||||||
|
workspaceId: signUpInput.workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const invitation = await this.authService.findInvitationForSignInUp({
|
||||||
|
currentWorkspace,
|
||||||
|
workspacePersonalInviteToken: signUpInput.workspacePersonalInviteToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingUser = await this.userRepository.findOne({
|
||||||
|
where: {
|
||||||
|
email: signUpInput.email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { userData } = this.authService.formatUserDataPayload(
|
||||||
|
{
|
||||||
|
email: signUpInput.email,
|
||||||
|
},
|
||||||
|
existingUser,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.authService.checkAccessForSignIn({
|
||||||
|
userData,
|
||||||
|
invitation,
|
||||||
|
workspaceInviteHash: signUpInput.workspaceInviteHash,
|
||||||
|
workspace: currentWorkspace,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { user, workspace } = await this.authService.signInUp({
|
||||||
|
userData,
|
||||||
|
workspace: currentWorkspace,
|
||||||
|
invitation,
|
||||||
|
authParams: {
|
||||||
|
provider: 'password',
|
||||||
|
password: signUpInput.password,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||||
|
|||||||
@ -23,8 +23,7 @@ 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 { 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 { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|
||||||
|
|
||||||
@Controller('auth/google')
|
@Controller('auth/google')
|
||||||
@UseFilters(AuthRestApiExceptionFilter)
|
@UseFilters(AuthRestApiExceptionFilter)
|
||||||
@ -33,9 +32,8 @@ export class GoogleAuthController {
|
|||||||
private readonly loginTokenService: LoginTokenService,
|
private readonly loginTokenService: LoginTokenService,
|
||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
private readonly domainManagerService: DomainManagerService,
|
private readonly domainManagerService: DomainManagerService,
|
||||||
private readonly environmentService: EnvironmentService,
|
@InjectRepository(User, 'core')
|
||||||
@InjectRepository(Workspace, 'core')
|
private readonly userRepository: Repository<User>,
|
||||||
private readonly workspaceRepository: Repository<Workspace>,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ -49,57 +47,61 @@ export class GoogleAuthController {
|
|||||||
@UseGuards(GoogleProviderEnabledGuard, GoogleOauthGuard)
|
@UseGuards(GoogleProviderEnabledGuard, GoogleOauthGuard)
|
||||||
@UseFilters(AuthOAuthExceptionFilter)
|
@UseFilters(AuthOAuthExceptionFilter)
|
||||||
async googleAuthRedirect(@Req() req: GoogleRequest, @Res() res: Response) {
|
async googleAuthRedirect(@Req() req: GoogleRequest, @Res() res: Response) {
|
||||||
|
const {
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
picture,
|
||||||
|
workspaceInviteHash,
|
||||||
|
workspacePersonalInviteToken,
|
||||||
|
workspaceId,
|
||||||
|
billingCheckoutSessionState,
|
||||||
|
} = req.user;
|
||||||
|
|
||||||
|
const currentWorkspace = await this.authService.findWorkspaceForSignInUp({
|
||||||
|
workspaceId,
|
||||||
|
workspaceInviteHash,
|
||||||
|
email,
|
||||||
|
authProvider: 'google',
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const invitation = await this.authService.findInvitationForSignInUp({
|
||||||
firstName,
|
currentWorkspace,
|
||||||
lastName,
|
|
||||||
email,
|
|
||||||
picture,
|
|
||||||
workspaceInviteHash,
|
|
||||||
workspacePersonalInviteToken,
|
workspacePersonalInviteToken,
|
||||||
targetWorkspaceSubdomain,
|
email,
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingUser = await this.userRepository.findOne({
|
||||||
|
where: { email },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { userData } = this.authService.formatUserDataPayload(
|
||||||
|
{
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
picture,
|
||||||
|
},
|
||||||
|
existingUser,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.authService.checkAccessForSignIn({
|
||||||
|
userData,
|
||||||
|
invitation,
|
||||||
|
workspaceInviteHash,
|
||||||
|
workspace: currentWorkspace,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { user, workspace } = await this.authService.signInUp({
|
||||||
|
invitation,
|
||||||
|
workspace: currentWorkspace,
|
||||||
|
userData,
|
||||||
|
authParams: {
|
||||||
|
provider: 'google',
|
||||||
|
},
|
||||||
billingCheckoutSessionState,
|
billingCheckoutSessionState,
|
||||||
} = req.user;
|
});
|
||||||
|
|
||||||
const signInUpParams = {
|
|
||||||
email,
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
picture,
|
|
||||||
workspaceInviteHash,
|
|
||||||
workspacePersonalInviteToken,
|
|
||||||
targetWorkspaceSubdomain,
|
|
||||||
fromSSO: true,
|
|
||||||
isAuthEnabled: 'google',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.environmentService.get('IS_MULTIWORKSPACE_ENABLED') &&
|
|
||||||
(targetWorkspaceSubdomain ===
|
|
||||||
this.environmentService.get('DEFAULT_SUBDOMAIN') ||
|
|
||||||
!targetWorkspaceSubdomain)
|
|
||||||
) {
|
|
||||||
const workspaceWithGoogleAuthActive =
|
|
||||||
await this.workspaceRepository.findOne({
|
|
||||||
where: {
|
|
||||||
isGoogleAuthEnabled: true,
|
|
||||||
workspaceUsers: {
|
|
||||||
user: {
|
|
||||||
email,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
relations: ['workspaceUsers', 'workspaceUsers.user'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (workspaceWithGoogleAuthActive) {
|
|
||||||
signInUpParams.targetWorkspaceSubdomain =
|
|
||||||
workspaceWithGoogleAuthActive.subdomain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { user, workspace } =
|
|
||||||
await this.authService.signInUp(signInUpParams);
|
|
||||||
|
|
||||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||||
user.email,
|
user.email,
|
||||||
@ -107,20 +109,17 @@ export class GoogleAuthController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return res.redirect(
|
return res.redirect(
|
||||||
this.authService.computeRedirectURI(
|
this.authService.computeRedirectURI({
|
||||||
loginToken.token,
|
loginToken: loginToken.token,
|
||||||
workspace.subdomain,
|
subdomain: workspace.subdomain,
|
||||||
billingCheckoutSessionState,
|
billingCheckoutSessionState,
|
||||||
),
|
}),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof AuthException) {
|
if (err instanceof AuthException) {
|
||||||
return res.redirect(
|
return res.redirect(
|
||||||
this.domainManagerService.computeRedirectErrorUrl({
|
this.domainManagerService.computeRedirectErrorUrl(err.message, {
|
||||||
subdomain:
|
subdomain: currentWorkspace?.subdomain,
|
||||||
req.user.targetWorkspaceSubdomain ??
|
|
||||||
this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
|
||||||
errorMessage: err.message,
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,8 +19,7 @@ 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 { 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 { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|
||||||
|
|
||||||
@Controller('auth/microsoft')
|
@Controller('auth/microsoft')
|
||||||
@UseFilters(AuthRestApiExceptionFilter)
|
@UseFilters(AuthRestApiExceptionFilter)
|
||||||
@ -29,9 +28,8 @@ export class MicrosoftAuthController {
|
|||||||
private readonly loginTokenService: LoginTokenService,
|
private readonly loginTokenService: LoginTokenService,
|
||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
private readonly domainManagerService: DomainManagerService,
|
private readonly domainManagerService: DomainManagerService,
|
||||||
private readonly environmentService: EnvironmentService,
|
@InjectRepository(User, 'core')
|
||||||
@InjectRepository(Workspace, 'core')
|
private readonly userRepository: Repository<User>,
|
||||||
private readonly workspaceRepository: Repository<Workspace>,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ -47,38 +45,60 @@ export class MicrosoftAuthController {
|
|||||||
@Req() req: MicrosoftRequest,
|
@Req() req: MicrosoftRequest,
|
||||||
@Res() res: Response,
|
@Res() res: Response,
|
||||||
) {
|
) {
|
||||||
|
const {
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
picture,
|
||||||
|
workspaceInviteHash,
|
||||||
|
workspacePersonalInviteToken,
|
||||||
|
workspaceId,
|
||||||
|
billingCheckoutSessionState,
|
||||||
|
} = req.user;
|
||||||
|
|
||||||
|
const currentWorkspace = await this.authService.findWorkspaceForSignInUp({
|
||||||
|
workspaceId,
|
||||||
|
workspaceInviteHash,
|
||||||
|
email,
|
||||||
|
authProvider: 'microsoft',
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const signInUpParams = req.user;
|
const invitation = await this.authService.findInvitationForSignInUp({
|
||||||
|
currentWorkspace,
|
||||||
|
workspacePersonalInviteToken,
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
|
||||||
if (
|
const existingUser = await this.userRepository.findOne({
|
||||||
this.environmentService.get('IS_MULTIWORKSPACE_ENABLED') &&
|
where: { email },
|
||||||
(signInUpParams.targetWorkspaceSubdomain ===
|
});
|
||||||
this.environmentService.get('DEFAULT_SUBDOMAIN') ||
|
|
||||||
!signInUpParams.targetWorkspaceSubdomain)
|
|
||||||
) {
|
|
||||||
const workspaceWithGoogleAuthActive =
|
|
||||||
await this.workspaceRepository.findOne({
|
|
||||||
where: {
|
|
||||||
isMicrosoftAuthEnabled: true,
|
|
||||||
workspaceUsers: {
|
|
||||||
user: {
|
|
||||||
email: signInUpParams.email,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
relations: ['userWorkspaces', 'userWorkspaces.user'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (workspaceWithGoogleAuthActive) {
|
const { userData } = this.authService.formatUserDataPayload(
|
||||||
signInUpParams.targetWorkspaceSubdomain =
|
{
|
||||||
workspaceWithGoogleAuthActive.subdomain;
|
firstName,
|
||||||
}
|
lastName,
|
||||||
}
|
email,
|
||||||
|
picture,
|
||||||
|
},
|
||||||
|
existingUser,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.authService.checkAccessForSignIn({
|
||||||
|
userData,
|
||||||
|
invitation,
|
||||||
|
workspaceInviteHash,
|
||||||
|
workspace: currentWorkspace,
|
||||||
|
});
|
||||||
|
|
||||||
const { user, workspace } = await this.authService.signInUp({
|
const { user, workspace } = await this.authService.signInUp({
|
||||||
...signInUpParams,
|
invitation,
|
||||||
fromSSO: true,
|
workspace: currentWorkspace,
|
||||||
authProvider: 'microsoft',
|
userData,
|
||||||
|
authParams: {
|
||||||
|
provider: 'microsoft',
|
||||||
|
},
|
||||||
|
billingCheckoutSessionState,
|
||||||
});
|
});
|
||||||
|
|
||||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||||
@ -87,20 +107,18 @@ export class MicrosoftAuthController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return res.redirect(
|
return res.redirect(
|
||||||
this.authService.computeRedirectURI(
|
this.authService.computeRedirectURI({
|
||||||
loginToken.token,
|
loginToken: loginToken.token,
|
||||||
workspace.subdomain,
|
subdomain: workspace.subdomain,
|
||||||
signInUpParams.billingCheckoutSessionState,
|
|
||||||
),
|
billingCheckoutSessionState,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof AuthException) {
|
if (err instanceof AuthException) {
|
||||||
return res.redirect(
|
return res.redirect(
|
||||||
this.domainManagerService.computeRedirectErrorUrl({
|
this.domainManagerService.computeRedirectErrorUrl(err.message, {
|
||||||
subdomain:
|
subdomain: currentWorkspace?.subdomain,
|
||||||
req.user.targetWorkspaceSubdomain ??
|
|
||||||
this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
|
||||||
errorMessage: err.message,
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,9 +30,9 @@ import {
|
|||||||
IdentityProviderType,
|
IdentityProviderType,
|
||||||
WorkspaceSSOIdentityProvider,
|
WorkspaceSSOIdentityProvider,
|
||||||
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.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 { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
|
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||||
|
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
@UseFilters(AuthRestApiExceptionFilter)
|
@UseFilters(AuthRestApiExceptionFilter)
|
||||||
@ -41,9 +41,10 @@ export class SSOAuthController {
|
|||||||
private readonly loginTokenService: LoginTokenService,
|
private readonly loginTokenService: LoginTokenService,
|
||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
private readonly domainManagerService: DomainManagerService,
|
private readonly domainManagerService: DomainManagerService,
|
||||||
private readonly userWorkspaceService: UserWorkspaceService,
|
private readonly userService: UserService,
|
||||||
private readonly environmentService: EnvironmentService,
|
|
||||||
private readonly ssoService: SSOService,
|
private readonly ssoService: SSOService,
|
||||||
|
@InjectRepository(User, 'core')
|
||||||
|
private readonly userRepository: Repository<User>,
|
||||||
@InjectRepository(WorkspaceSSOIdentityProvider, 'core')
|
@InjectRepository(WorkspaceSSOIdentityProvider, 'core')
|
||||||
private readonly workspaceSSOIdentityProviderRepository: Repository<WorkspaceSSOIdentityProvider>,
|
private readonly workspaceSSOIdentityProviderRepository: Repository<WorkspaceSSOIdentityProvider>,
|
||||||
) {}
|
) {}
|
||||||
@ -81,50 +82,44 @@ export class SSOAuthController {
|
|||||||
@Get('oidc/callback')
|
@Get('oidc/callback')
|
||||||
@UseGuards(SSOProviderEnabledGuard, OIDCAuthGuard)
|
@UseGuards(SSOProviderEnabledGuard, OIDCAuthGuard)
|
||||||
async oidcAuthCallback(@Req() req: any, @Res() res: Response) {
|
async oidcAuthCallback(@Req() req: any, @Res() res: Response) {
|
||||||
try {
|
return this.authCallback(req, res);
|
||||||
const { loginToken, identityProvider } = await this.generateLoginToken(
|
|
||||||
req.user,
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.redirect(
|
|
||||||
this.authService.computeRedirectURI(
|
|
||||||
loginToken.token,
|
|
||||||
identityProvider.workspace.subdomain,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof AuthException) {
|
|
||||||
return res.redirect(
|
|
||||||
this.domainManagerService.computeRedirectErrorUrl({
|
|
||||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
|
||||||
errorMessage: err.message,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('saml/callback/:identityProviderId')
|
@Post('saml/callback/:identityProviderId')
|
||||||
@UseGuards(SSOProviderEnabledGuard, SAMLAuthGuard)
|
@UseGuards(SSOProviderEnabledGuard, SAMLAuthGuard)
|
||||||
async samlAuthCallback(@Req() req: any, @Res() res: Response) {
|
async samlAuthCallback(@Req() req: any, @Res() res: Response) {
|
||||||
|
return this.authCallback(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async authCallback(req: any, res: Response) {
|
||||||
|
const workspaceIdentityProvider =
|
||||||
|
await this.findWorkspaceIdentityProviderByIdentityProviderId(
|
||||||
|
req.user.identityProviderId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!workspaceIdentityProvider) {
|
||||||
|
throw new AuthException(
|
||||||
|
'Identity provider not found',
|
||||||
|
AuthExceptionCode.INVALID_DATA,
|
||||||
|
);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const { loginToken, identityProvider } = await this.generateLoginToken(
|
const { loginToken, identityProvider } = await this.generateLoginToken(
|
||||||
req.user,
|
req.user,
|
||||||
|
workspaceIdentityProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.redirect(
|
return res.redirect(
|
||||||
this.authService.computeRedirectURI(
|
this.authService.computeRedirectURI({
|
||||||
loginToken.token,
|
loginToken: loginToken.token,
|
||||||
identityProvider.workspace.subdomain,
|
subdomain: identityProvider.workspace.subdomain,
|
||||||
),
|
}),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof AuthException) {
|
if (err instanceof AuthException) {
|
||||||
return res.redirect(
|
return res.redirect(
|
||||||
this.domainManagerService.computeRedirectErrorUrl({
|
this.domainManagerService.computeRedirectErrorUrl(err.message, {
|
||||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
subdomain: workspaceIdentityProvider.workspace.subdomain,
|
||||||
errorMessage: err.message,
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -132,26 +127,19 @@ export class SSOAuthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async generateLoginToken({
|
private async findWorkspaceIdentityProviderByIdentityProviderId(
|
||||||
user,
|
identityProviderId: string,
|
||||||
identityProviderId,
|
) {
|
||||||
}: {
|
return await this.workspaceSSOIdentityProviderRepository.findOne({
|
||||||
identityProviderId?: string;
|
where: { id: identityProviderId },
|
||||||
user: { email: string } & Record<string, string>;
|
relations: ['workspace'],
|
||||||
}) {
|
});
|
||||||
if (!identityProviderId) {
|
}
|
||||||
throw new AuthException(
|
|
||||||
'Identity provider ID is required',
|
|
||||||
AuthExceptionCode.INVALID_DATA,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const identityProvider =
|
|
||||||
await this.workspaceSSOIdentityProviderRepository.findOne({
|
|
||||||
where: { id: identityProviderId },
|
|
||||||
relations: ['workspace'],
|
|
||||||
});
|
|
||||||
|
|
||||||
|
private async generateLoginToken(
|
||||||
|
user: { email: string } & Record<string, string>,
|
||||||
|
identityProvider: WorkspaceSSOIdentityProvider,
|
||||||
|
) {
|
||||||
if (!identityProvider) {
|
if (!identityProvider) {
|
||||||
throw new AuthException(
|
throw new AuthException(
|
||||||
'Identity provider not found',
|
'Identity provider not found',
|
||||||
@ -159,28 +147,24 @@ export class SSOAuthController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.authService.signInUp({
|
const existingUser = await this.userRepository.findOne({
|
||||||
...user,
|
where: {
|
||||||
...(this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')
|
email: user.email,
|
||||||
? {
|
},
|
||||||
targetWorkspaceSubdomain: identityProvider.workspace.subdomain,
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
fromSSO: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const isUserExistInWorkspace =
|
const { userData } = this.authService.formatUserDataPayload(
|
||||||
await this.userWorkspaceService.checkUserWorkspaceExistsByEmail(
|
user,
|
||||||
user.email,
|
existingUser,
|
||||||
identityProvider.workspaceId,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
if (!isUserExistInWorkspace) {
|
await this.authService.signInUp({
|
||||||
throw new AuthException(
|
userData,
|
||||||
'User not found in workspace',
|
workspace: identityProvider.workspace,
|
||||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
authParams: {
|
||||||
);
|
provider: 'sso',
|
||||||
}
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
identityProvider,
|
identityProvider,
|
||||||
|
|||||||
@ -14,6 +14,11 @@ export class SignUpInput {
|
|||||||
@IsString()
|
@IsString()
|
||||||
password: string;
|
password: string;
|
||||||
|
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
workspaceId?: string;
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
@ -39,10 +39,10 @@ export class GoogleOauthGuard extends AuthGuard('google') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
request.query.workspaceSubdomain &&
|
request.query.workspaceId &&
|
||||||
typeof request.query.workspaceSubdomain === 'string'
|
typeof request.query.workspaceId === 'string'
|
||||||
) {
|
) {
|
||||||
request.params.workspaceSubdomain = request.query.workspaceSubdomain;
|
request.params.workspaceId = request.query.workspaceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import { expect, jest } from '@jest/globals';
|
import { expect, jest } from '@jest/globals';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
@ -16,6 +17,7 @@ import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services
|
|||||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||||
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 { 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 { SocialSsoService } from 'src/engine/core-modules/auth/services/social-sso.service';
|
||||||
|
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
@ -31,6 +33,7 @@ const userWorkspaceAddUserToWorkspaceMock = jest.fn();
|
|||||||
|
|
||||||
describe('AuthService', () => {
|
describe('AuthService', () => {
|
||||||
let service: AuthService;
|
let service: AuthService;
|
||||||
|
let appTokenRepository: Repository<AppToken>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
@ -48,7 +51,14 @@ describe('AuthService', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: getRepositoryToken(AppToken, 'core'),
|
provide: getRepositoryToken(AppToken, 'core'),
|
||||||
useValue: {},
|
useValue: {
|
||||||
|
createQueryBuilder: jest.fn().mockReturnValue({
|
||||||
|
leftJoin: jest.fn().mockReturnThis(),
|
||||||
|
andWhere: jest.fn().mockReturnThis(),
|
||||||
|
where: jest.fn().mockReturnThis(),
|
||||||
|
getOne: jest.fn().mockImplementation(() => null),
|
||||||
|
}),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: SignInUpService,
|
provide: SignInUpService,
|
||||||
@ -94,10 +104,18 @@ describe('AuthService', () => {
|
|||||||
validateInvitation: workspaceInvitationValidateInvitationMock,
|
validateInvitation: workspaceInvitationValidateInvitationMock,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: SocialSsoService,
|
||||||
|
useValue: {},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<AuthService>(AuthService);
|
service = module.get<AuthService>(AuthService);
|
||||||
|
|
||||||
|
appTokenRepository = module.get<Repository<AppToken>>(
|
||||||
|
getRepositoryToken(AppToken, 'core'),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', async () => {
|
it('should be defined', async () => {
|
||||||
|
|||||||
@ -47,6 +47,15 @@ import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
|||||||
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 { WorkspaceAuthProvider } from 'src/engine/core-modules/workspace/types/workspace.type';
|
import { WorkspaceAuthProvider } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import { SocialSsoService } from 'src/engine/core-modules/auth/services/social-sso.service';
|
||||||
|
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||||
|
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||||
|
import {
|
||||||
|
AuthProviderWithPasswordType,
|
||||||
|
ExistingUserOrNewUser,
|
||||||
|
SignInUpBaseParams,
|
||||||
|
SignInUpNewUserPayload,
|
||||||
|
} from 'src/engine/core-modules/auth/types/signInUp.type';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
||||||
@ -57,6 +66,8 @@ export class AuthService {
|
|||||||
private readonly refreshTokenService: RefreshTokenService,
|
private readonly refreshTokenService: RefreshTokenService,
|
||||||
private readonly userWorkspaceService: UserWorkspaceService,
|
private readonly userWorkspaceService: UserWorkspaceService,
|
||||||
private readonly workspaceInvitationService: WorkspaceInvitationService,
|
private readonly workspaceInvitationService: WorkspaceInvitationService,
|
||||||
|
private readonly socialSsoService: SocialSsoService,
|
||||||
|
private readonly userService: UserService,
|
||||||
private readonly signInUpService: SignInUpService,
|
private readonly signInUpService: SignInUpService,
|
||||||
@InjectRepository(Workspace, 'core')
|
@InjectRepository(Workspace, 'core')
|
||||||
private readonly workspaceRepository: Repository<Workspace>,
|
private readonly workspaceRepository: Repository<Workspace>,
|
||||||
@ -149,41 +160,58 @@ export class AuthService {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async signInUp({
|
async signInUp(
|
||||||
email,
|
params: SignInUpBaseParams &
|
||||||
password,
|
ExistingUserOrNewUser &
|
||||||
workspaceInviteHash,
|
AuthProviderWithPasswordType,
|
||||||
workspacePersonalInviteToken,
|
) {
|
||||||
targetWorkspaceSubdomain,
|
if (
|
||||||
firstName,
|
params.authParams.provider === 'password' &&
|
||||||
lastName,
|
params.userData.type === 'newUser'
|
||||||
picture,
|
) {
|
||||||
fromSSO,
|
params.userData.newUserPayload.passwordHash =
|
||||||
authProvider,
|
await this.signInUpService.generateHash(params.authParams.password);
|
||||||
}: {
|
}
|
||||||
email: string;
|
|
||||||
password?: string;
|
if (
|
||||||
firstName?: string | null;
|
params.authParams.provider === 'password' &&
|
||||||
lastName?: string | null;
|
params.userData.type === 'existingUser'
|
||||||
workspaceInviteHash?: string;
|
) {
|
||||||
workspacePersonalInviteToken?: string;
|
await this.signInUpService.validatePassword({
|
||||||
picture?: string | null;
|
password: params.authParams.password,
|
||||||
fromSSO: boolean;
|
passwordHash: params.userData.existingUser.passwordHash,
|
||||||
targetWorkspaceSubdomain?: string;
|
});
|
||||||
authProvider?: WorkspaceAuthProvider;
|
}
|
||||||
billingCheckoutSessionState?: string;
|
|
||||||
}) {
|
if (params.workspace) {
|
||||||
|
workspaceValidator.isAuthEnabledOrThrow(
|
||||||
|
params.authParams.provider,
|
||||||
|
params.workspace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.userData.type === 'newUser') {
|
||||||
|
const partialUserWithPicture =
|
||||||
|
await this.signInUpService.computeParamsForNewUser(
|
||||||
|
params.userData.newUserPayload,
|
||||||
|
params.authParams,
|
||||||
|
);
|
||||||
|
|
||||||
|
return await this.signInUpService.signInUp({
|
||||||
|
...params,
|
||||||
|
userData: {
|
||||||
|
type: 'newUserWithPicture',
|
||||||
|
newUserWithPicture: partialUserWithPicture,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return await this.signInUpService.signInUp({
|
return await this.signInUpService.signInUp({
|
||||||
email,
|
...params,
|
||||||
password,
|
userData: {
|
||||||
firstName,
|
type: 'existingUser',
|
||||||
lastName,
|
existingUser: params.userData.existingUser,
|
||||||
workspaceInviteHash,
|
},
|
||||||
workspacePersonalInviteToken,
|
|
||||||
targetWorkspaceSubdomain,
|
|
||||||
picture,
|
|
||||||
fromSSO,
|
|
||||||
authProvider,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,11 +442,15 @@ export class AuthService {
|
|||||||
return workspace;
|
return workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
computeRedirectURI(
|
computeRedirectURI({
|
||||||
loginToken: string,
|
loginToken,
|
||||||
subdomain?: string,
|
subdomain,
|
||||||
billingCheckoutSessionState?: string,
|
billingCheckoutSessionState,
|
||||||
) {
|
}: {
|
||||||
|
loginToken: string;
|
||||||
|
subdomain?: string;
|
||||||
|
billingCheckoutSessionState?: string;
|
||||||
|
}) {
|
||||||
const url = this.domainManagerService.buildWorkspaceURL({
|
const url = this.domainManagerService.buildWorkspaceURL({
|
||||||
subdomain,
|
subdomain,
|
||||||
pathname: '/verify',
|
pathname: '/verify',
|
||||||
@ -472,4 +504,118 @@ export class AuthService {
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findInvitationForSignInUp({
|
||||||
|
currentWorkspace,
|
||||||
|
workspacePersonalInviteToken,
|
||||||
|
email,
|
||||||
|
}: {
|
||||||
|
currentWorkspace?: Workspace | null;
|
||||||
|
workspacePersonalInviteToken?: string;
|
||||||
|
email?: string;
|
||||||
|
}) {
|
||||||
|
if (!currentWorkspace) return undefined;
|
||||||
|
|
||||||
|
const qr = this.appTokenRepository.createQueryBuilder('appToken');
|
||||||
|
|
||||||
|
qr.where('"appToken"."workspaceId" = :workspaceId', {
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
}).andWhere('"appToken".type = :type', {
|
||||||
|
type: AppTokenType.InvitationToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (email) {
|
||||||
|
qr.andWhere('"appToken".context->>\'email\' = :email', {
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workspacePersonalInviteToken) {
|
||||||
|
qr.andWhere('"appToken".value = :personalInviteToken', {
|
||||||
|
personalInviteToken: workspacePersonalInviteToken,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await qr.getOne()) ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findWorkspaceForSignInUp(
|
||||||
|
params: {
|
||||||
|
workspaceId?: string;
|
||||||
|
workspaceInviteHash?: string;
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
authProvider: Exclude<WorkspaceAuthProvider, 'password'>;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
| { authProvider: Extract<WorkspaceAuthProvider, 'password'> }
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
if (params.workspaceInviteHash) {
|
||||||
|
return (
|
||||||
|
(await this.workspaceRepository.findOneBy({
|
||||||
|
inviteHash: params.workspaceInviteHash,
|
||||||
|
})) ?? undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.authProvider !== 'password') {
|
||||||
|
return (
|
||||||
|
(await this.socialSsoService.findWorkspaceFromWorkspaceIdOrAuthProvider(
|
||||||
|
{
|
||||||
|
email: params.email,
|
||||||
|
authProvider: params.authProvider,
|
||||||
|
},
|
||||||
|
params.workspaceId,
|
||||||
|
)) ?? undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.authProvider === 'password' && params.workspaceId) {
|
||||||
|
return (
|
||||||
|
(await this.workspaceRepository.findOneBy({
|
||||||
|
id: params.workspaceId,
|
||||||
|
})) ?? undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatUserDataPayload(
|
||||||
|
newUserPayload: SignInUpNewUserPayload,
|
||||||
|
existingUser?: User | null,
|
||||||
|
): ExistingUserOrNewUser {
|
||||||
|
return {
|
||||||
|
userData: existingUser
|
||||||
|
? { type: 'existingUser', existingUser }
|
||||||
|
: {
|
||||||
|
type: 'newUser',
|
||||||
|
newUserPayload,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkAccessForSignIn({
|
||||||
|
userData,
|
||||||
|
invitation,
|
||||||
|
workspaceInviteHash,
|
||||||
|
workspace,
|
||||||
|
}: {
|
||||||
|
workspaceInviteHash?: string;
|
||||||
|
} & ExistingUserOrNewUser &
|
||||||
|
SignInUpBaseParams) {
|
||||||
|
if (!invitation && !workspaceInviteHash && workspace) {
|
||||||
|
if (userData.type === 'existingUser') {
|
||||||
|
await this.userService.hasUserAccessToWorkspaceOrThrow(
|
||||||
|
userData.existingUser.id,
|
||||||
|
workspace.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new AuthException(
|
||||||
|
'User does not have access to this workspace',
|
||||||
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,90 +1,83 @@
|
|||||||
import { HttpService } from '@nestjs/axios';
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { expect, jest } from '@jest/globals';
|
import { Repository } from 'typeorm';
|
||||||
import bcrypt from 'bcrypt';
|
|
||||||
|
|
||||||
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
|
||||||
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.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 { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||||
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
|
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
|
||||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
|
||||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
|
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
|
||||||
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 { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
|
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||||
|
import {
|
||||||
|
AuthProviderWithPasswordType,
|
||||||
|
ExistingUserOrPartialUserWithPicture,
|
||||||
|
SignInUpBaseParams,
|
||||||
|
} from 'src/engine/core-modules/auth/types/signInUp.type';
|
||||||
import {
|
import {
|
||||||
Workspace,
|
Workspace,
|
||||||
WorkspaceActivationStatus,
|
WorkspaceActivationStatus,
|
||||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||||
|
|
||||||
jest.mock('bcrypt');
|
jest.mock('src/utils/image', () => {
|
||||||
|
return {
|
||||||
const UserFindOneMock = jest.fn();
|
getImageBufferFromUrl: () => Promise.resolve(Buffer.from('')),
|
||||||
const workspaceInvitationValidateInvitationMock = jest.fn();
|
};
|
||||||
const workspaceInvitationInvalidateWorkspaceInvitationMock = jest.fn();
|
});
|
||||||
const workspaceInvitationFindInvitationByWorkspaceSubdomainAndUserEmailMock =
|
|
||||||
jest.fn();
|
|
||||||
const userWorkspaceServiceAddUserToWorkspaceMock = jest.fn();
|
|
||||||
const UserCreateMock = jest.fn();
|
|
||||||
const UserSaveMock = jest.fn();
|
|
||||||
const EnvironmentServiceGetMock = jest.fn();
|
|
||||||
const WorkspaceCountMock = jest.fn();
|
|
||||||
const WorkspaceCreateMock = jest.fn();
|
|
||||||
const WorkspaceSaveMock = jest.fn();
|
|
||||||
const WorkspaceFindOneMock = jest.fn();
|
|
||||||
|
|
||||||
describe('SignInUpService', () => {
|
describe('SignInUpService', () => {
|
||||||
let service: SignInUpService;
|
let service: SignInUpService;
|
||||||
|
let UserRepository: Repository<User>;
|
||||||
afterEach(() => {
|
let WorkspaceRepository: Repository<Workspace>;
|
||||||
jest.clearAllMocks();
|
let fileUploadService: FileUploadService;
|
||||||
});
|
let workspaceInvitationService: WorkspaceInvitationService;
|
||||||
|
let userWorkspaceService: UserWorkspaceService;
|
||||||
|
let onboardingService: OnboardingService;
|
||||||
|
let httpService: HttpService;
|
||||||
|
let environmentService: EnvironmentService;
|
||||||
|
let domainManagerService: DomainManagerService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
SignInUpService,
|
SignInUpService,
|
||||||
{
|
{
|
||||||
provide: FileUploadService,
|
provide: getRepositoryToken(User, 'core'),
|
||||||
useValue: {},
|
useValue: {
|
||||||
|
create: jest.fn(),
|
||||||
|
save: jest.fn(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: getRepositoryToken(Workspace, 'core'),
|
provide: getRepositoryToken(Workspace, 'core'),
|
||||||
useValue: {
|
useValue: {
|
||||||
count: WorkspaceCountMock,
|
save: jest.fn(),
|
||||||
create: WorkspaceCreateMock,
|
create: jest.fn(),
|
||||||
save: WorkspaceSaveMock,
|
get: jest.fn(),
|
||||||
findOne: WorkspaceFindOneMock,
|
count: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: getRepositoryToken(User, 'core'),
|
provide: FileUploadService,
|
||||||
useValue: {
|
useValue: {
|
||||||
findOne: UserFindOneMock,
|
uploadImage: jest.fn(),
|
||||||
create: UserCreateMock,
|
|
||||||
save: UserSaveMock,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
provide: getRepositoryToken(AppToken, 'core'),
|
|
||||||
useValue: {},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: WorkspaceInvitationService,
|
provide: WorkspaceInvitationService,
|
||||||
useValue: {},
|
useValue: {
|
||||||
},
|
validateInvitation: jest.fn(),
|
||||||
{
|
invalidateWorkspaceInvitation: jest.fn(),
|
||||||
provide: WorkspaceService,
|
},
|
||||||
useValue: {},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: UserWorkspaceService,
|
provide: UserWorkspaceService,
|
||||||
useValue: {
|
useValue: {
|
||||||
addUserToWorkspace: userWorkspaceServiceAddUserToWorkspaceMock,
|
addUserToWorkspace: jest.fn(),
|
||||||
create: jest.fn(),
|
create: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -93,7 +86,6 @@ describe('SignInUpService', () => {
|
|||||||
useValue: {
|
useValue: {
|
||||||
setOnboardingConnectAccountPending: jest.fn(),
|
setOnboardingConnectAccountPending: jest.fn(),
|
||||||
setOnboardingInviteTeamPending: jest.fn(),
|
setOnboardingInviteTeamPending: jest.fn(),
|
||||||
setOnboardingCreateProfilePending: jest.fn(),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -103,410 +95,150 @@ describe('SignInUpService', () => {
|
|||||||
{
|
{
|
||||||
provide: EnvironmentService,
|
provide: EnvironmentService,
|
||||||
useValue: {
|
useValue: {
|
||||||
get: EnvironmentServiceGetMock,
|
get: jest.fn(),
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: WorkspaceInvitationService,
|
|
||||||
useValue: {
|
|
||||||
validateInvitation: workspaceInvitationValidateInvitationMock,
|
|
||||||
invalidateWorkspaceInvitation:
|
|
||||||
workspaceInvitationInvalidateWorkspaceInvitationMock,
|
|
||||||
findInvitationByWorkspaceSubdomainAndUserEmail:
|
|
||||||
workspaceInvitationFindInvitationByWorkspaceSubdomainAndUserEmailMock,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: DomainManagerService,
|
provide: DomainManagerService,
|
||||||
useValue: {
|
useValue: {
|
||||||
generateSubdomain: jest.fn().mockReturnValue('testSubDomain'),
|
generateSubdomain: jest.fn(),
|
||||||
getWorkspaceBySubdomainOrDefaultWorkspace: jest
|
|
||||||
.fn()
|
|
||||||
.mockReturnValue({}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: UserService,
|
|
||||||
useValue: {
|
|
||||||
hasUserAccessToWorkspaceOrThrow: jest.fn(),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<SignInUpService>(SignInUpService);
|
service = module.get<SignInUpService>(SignInUpService);
|
||||||
});
|
UserRepository = module.get(getRepositoryToken(User, 'core'));
|
||||||
|
WorkspaceRepository = module.get(getRepositoryToken(Workspace, 'core'));
|
||||||
it('should be defined', () => {
|
fileUploadService = module.get<FileUploadService>(FileUploadService);
|
||||||
expect(service).toBeDefined();
|
workspaceInvitationService = module.get<WorkspaceInvitationService>(
|
||||||
});
|
WorkspaceInvitationService,
|
||||||
|
|
||||||
it('signInUp - sso - new user', async () => {
|
|
||||||
const email = 'test@test.com';
|
|
||||||
|
|
||||||
UserFindOneMock.mockReturnValueOnce(false);
|
|
||||||
workspaceInvitationFindInvitationByWorkspaceSubdomainAndUserEmailMock.mockReturnValueOnce(
|
|
||||||
undefined,
|
|
||||||
);
|
);
|
||||||
|
userWorkspaceService =
|
||||||
|
module.get<UserWorkspaceService>(UserWorkspaceService);
|
||||||
|
onboardingService = module.get<OnboardingService>(OnboardingService);
|
||||||
|
httpService = module.get<HttpService>(HttpService);
|
||||||
|
environmentService = module.get<EnvironmentService>(EnvironmentService);
|
||||||
|
domainManagerService =
|
||||||
|
module.get<DomainManagerService>(DomainManagerService);
|
||||||
|
});
|
||||||
|
|
||||||
const spy = jest
|
it('should handle signInUp with valid personal invitation', async () => {
|
||||||
.spyOn(service, 'signUpOnNewWorkspace')
|
const params: SignInUpBaseParams &
|
||||||
.mockResolvedValueOnce({ user: {}, workspace: {} } as {
|
ExistingUserOrPartialUserWithPicture &
|
||||||
user: User;
|
AuthProviderWithPasswordType = {
|
||||||
workspace: Workspace;
|
invitation: { value: 'invitationToken' } as AppToken,
|
||||||
|
workspace: {
|
||||||
|
id: 'workspaceId',
|
||||||
|
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||||
|
} as Workspace,
|
||||||
|
authParams: { provider: 'password', password: 'validPassword' },
|
||||||
|
userData: {
|
||||||
|
type: 'existingUser',
|
||||||
|
existingUser: { email: 'test@example.com' } as User,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(workspaceInvitationService, 'validateInvitation')
|
||||||
|
.mockResolvedValue({
|
||||||
|
isValid: true,
|
||||||
|
workspace: params.workspace as Workspace,
|
||||||
});
|
});
|
||||||
|
|
||||||
await service.signInUp({
|
jest
|
||||||
email: 'test@test.com',
|
.spyOn(workspaceInvitationService, 'invalidateWorkspaceInvitation')
|
||||||
fromSSO: true,
|
.mockResolvedValue(undefined);
|
||||||
targetWorkspaceSubdomain: 'testSubDomain',
|
|
||||||
|
jest
|
||||||
|
.spyOn(userWorkspaceService, 'addUserToWorkspace')
|
||||||
|
.mockResolvedValue({} as User);
|
||||||
|
|
||||||
|
const result = await service.signInUp(params);
|
||||||
|
|
||||||
|
expect(result.workspace).toEqual(params.workspace);
|
||||||
|
expect(result.user).toBeDefined();
|
||||||
|
expect(workspaceInvitationService.validateInvitation).toHaveBeenCalledWith({
|
||||||
|
workspacePersonalInviteToken: 'invitationToken',
|
||||||
|
email: 'test@example.com',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
email,
|
|
||||||
passwordHash: undefined,
|
|
||||||
firstName: expect.any(String),
|
|
||||||
lastName: expect.any(String),
|
|
||||||
picture: undefined,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it('signInUp - sso - existing user', async () => {
|
|
||||||
const email = 'existing@test.com';
|
|
||||||
const existingUser = {
|
|
||||||
id: 'user-id',
|
|
||||||
email,
|
|
||||||
passwordHash: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
UserFindOneMock.mockReturnValueOnce(existingUser);
|
|
||||||
workspaceInvitationFindInvitationByWorkspaceSubdomainAndUserEmailMock.mockReturnValueOnce(
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
WorkspaceFindOneMock.mockReturnValueOnce({
|
|
||||||
id: 'another-workspace',
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await service.signInUp({
|
|
||||||
email,
|
|
||||||
fromSSO: true,
|
|
||||||
targetWorkspaceSubdomain: 'testSubDomain',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toEqual({ user: existingUser, workspace: {} });
|
|
||||||
});
|
|
||||||
it('signInUp - sso - new user - existing invitation', async () => {
|
|
||||||
const email = 'newuser@test.com';
|
|
||||||
const workspaceId = 'workspace-id';
|
|
||||||
|
|
||||||
UserFindOneMock.mockReturnValueOnce(null);
|
|
||||||
workspaceInvitationFindInvitationByWorkspaceSubdomainAndUserEmailMock.mockReturnValueOnce(
|
|
||||||
{
|
|
||||||
value: 'personal-token-value',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
workspaceInvitationValidateInvitationMock.mockReturnValueOnce({
|
|
||||||
isValid: true,
|
|
||||||
workspace: { id: workspaceId },
|
|
||||||
});
|
|
||||||
|
|
||||||
workspaceInvitationInvalidateWorkspaceInvitationMock.mockReturnValueOnce(
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
const spySignInUpOnExistingWorkspace = jest
|
|
||||||
.spyOn(service, 'signInUpOnExistingWorkspace')
|
|
||||||
.mockResolvedValueOnce(
|
|
||||||
{} as Awaited<
|
|
||||||
ReturnType<(typeof service)['signInUpOnExistingWorkspace']>
|
|
||||||
>,
|
|
||||||
);
|
|
||||||
|
|
||||||
await service.signInUp({
|
|
||||||
email,
|
|
||||||
fromSSO: true,
|
|
||||||
targetWorkspaceSubdomain: 'testSubDomain',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(spySignInUpOnExistingWorkspace).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
email,
|
|
||||||
passwordHash: undefined,
|
|
||||||
workspace: expect.objectContaining({
|
|
||||||
id: workspaceId,
|
|
||||||
}),
|
|
||||||
firstName: expect.any(String),
|
|
||||||
lastName: expect.any(String),
|
|
||||||
picture: undefined,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
workspaceInvitationInvalidateWorkspaceInvitationMock,
|
workspaceInvitationService.invalidateWorkspaceInvitation,
|
||||||
).toHaveBeenCalledWith(workspaceId, email);
|
).toHaveBeenCalledWith(
|
||||||
|
(params.workspace as Workspace).id,
|
||||||
|
'test@example.com',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
it('signInUp - sso - existing user - existing invitation', async () => {
|
|
||||||
const email = 'existinguser@test.com';
|
it('should handle signInUp on existing workspace without invitation', async () => {
|
||||||
const workspaceId = 'workspace-id';
|
const params: SignInUpBaseParams &
|
||||||
const existingUser = {
|
ExistingUserOrPartialUserWithPicture &
|
||||||
id: 'user-id',
|
AuthProviderWithPasswordType = {
|
||||||
email,
|
workspace: {
|
||||||
passwordHash: undefined,
|
id: 'workspaceId',
|
||||||
|
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||||
|
} as Workspace,
|
||||||
|
authParams: { provider: 'password', password: 'validPassword' },
|
||||||
|
userData: {
|
||||||
|
type: 'existingUser',
|
||||||
|
existingUser: { email: 'test@example.com' } as User,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const workspace = {
|
jest
|
||||||
id: workspaceId,
|
.spyOn(userWorkspaceService, 'addUserToWorkspace')
|
||||||
|
.mockResolvedValue({} as User);
|
||||||
|
|
||||||
|
const result = await service.signInUp(params);
|
||||||
|
|
||||||
|
expect(result.workspace).toEqual(params.workspace);
|
||||||
|
expect(result.user).toBeDefined();
|
||||||
|
expect(userWorkspaceService.addUserToWorkspace).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle signUp on new workspace for a new user', async () => {
|
||||||
|
const params: SignInUpBaseParams &
|
||||||
|
ExistingUserOrPartialUserWithPicture &
|
||||||
|
AuthProviderWithPasswordType = {
|
||||||
|
authParams: { provider: 'password', password: 'validPassword' },
|
||||||
|
userData: {
|
||||||
|
type: 'newUserWithPicture',
|
||||||
|
newUserWithPicture: {
|
||||||
|
email: 'newuser@example.com',
|
||||||
|
picture: 'pictureUrl',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(environmentService, 'get').mockReturnValue(false);
|
||||||
|
jest.spyOn(WorkspaceRepository, 'count').mockResolvedValue(0);
|
||||||
|
jest.spyOn(WorkspaceRepository, 'create').mockReturnValue({} as Workspace);
|
||||||
|
jest.spyOn(WorkspaceRepository, 'save').mockResolvedValue({
|
||||||
|
id: 'newWorkspaceId',
|
||||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||||
};
|
} as Workspace);
|
||||||
|
jest.spyOn(fileUploadService, 'uploadImage').mockResolvedValue({
|
||||||
UserFindOneMock.mockReturnValueOnce(existingUser);
|
id: '',
|
||||||
workspaceInvitationFindInvitationByWorkspaceSubdomainAndUserEmailMock.mockReturnValueOnce(
|
mimeType: '',
|
||||||
{
|
paths: ['path/to/image'],
|
||||||
value: 'personal-token-value',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
workspaceInvitationValidateInvitationMock.mockReturnValueOnce({
|
|
||||||
isValid: true,
|
|
||||||
workspace,
|
|
||||||
});
|
});
|
||||||
|
jest.spyOn(UserRepository, 'create').mockReturnValue({} as User);
|
||||||
|
jest
|
||||||
|
.spyOn(domainManagerService, 'generateSubdomain')
|
||||||
|
.mockResolvedValue('a-subdomain');
|
||||||
|
jest
|
||||||
|
.spyOn(UserRepository, 'save')
|
||||||
|
.mockResolvedValue({ id: 'newUserId' } as User);
|
||||||
|
jest.spyOn(userWorkspaceService, 'create').mockResolvedValue({} as any);
|
||||||
|
|
||||||
workspaceInvitationInvalidateWorkspaceInvitationMock.mockReturnValueOnce(
|
const result = await service.signInUp(params);
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
userWorkspaceServiceAddUserToWorkspaceMock.mockReturnValueOnce({});
|
expect(result.workspace).toBeDefined();
|
||||||
|
expect(result.user).toBeDefined();
|
||||||
const result = await service.signInUp({
|
expect(WorkspaceRepository.create).toHaveBeenCalled();
|
||||||
email,
|
expect(WorkspaceRepository.save).toHaveBeenCalled();
|
||||||
fromSSO: true,
|
expect(UserRepository.create).toHaveBeenCalled();
|
||||||
targetWorkspaceSubdomain: 'testSubDomain',
|
expect(UserRepository.save).toHaveBeenCalled();
|
||||||
});
|
expect(fileUploadService.uploadImage).toHaveBeenCalled();
|
||||||
|
|
||||||
expect(result).toEqual({ user: existingUser, workspace });
|
|
||||||
expect(userWorkspaceServiceAddUserToWorkspaceMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(
|
|
||||||
workspaceInvitationInvalidateWorkspaceInvitationMock,
|
|
||||||
).toHaveBeenCalledWith(workspaceId, email);
|
|
||||||
});
|
|
||||||
it('signInUp - sso - new user - personal invitation token', async () => {
|
|
||||||
const email = 'newuser@test.com';
|
|
||||||
const workspaceId = 'workspace-id';
|
|
||||||
const workspacePersonalInviteToken = 'personal-token-value';
|
|
||||||
const workspace = {
|
|
||||||
id: workspaceId,
|
|
||||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
|
||||||
};
|
|
||||||
|
|
||||||
UserFindOneMock.mockReturnValueOnce(null);
|
|
||||||
|
|
||||||
workspaceInvitationValidateInvitationMock.mockReturnValueOnce({
|
|
||||||
isValid: true,
|
|
||||||
workspace,
|
|
||||||
});
|
|
||||||
|
|
||||||
workspaceInvitationInvalidateWorkspaceInvitationMock.mockReturnValueOnce(
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
const spySignInUpOnExistingWorkspace = jest
|
|
||||||
.spyOn(service, 'signInUpOnExistingWorkspace')
|
|
||||||
.mockResolvedValueOnce(
|
|
||||||
{} as Awaited<
|
|
||||||
ReturnType<(typeof service)['signInUpOnExistingWorkspace']>
|
|
||||||
>,
|
|
||||||
);
|
|
||||||
|
|
||||||
await service.signInUp({
|
|
||||||
email,
|
|
||||||
fromSSO: true,
|
|
||||||
workspacePersonalInviteToken,
|
|
||||||
targetWorkspaceSubdomain: 'testSubDomain',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(spySignInUpOnExistingWorkspace).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
email,
|
|
||||||
passwordHash: undefined,
|
|
||||||
workspace: expect.objectContaining({
|
|
||||||
id: workspaceId,
|
|
||||||
}),
|
|
||||||
firstName: expect.any(String),
|
|
||||||
lastName: expect.any(String),
|
|
||||||
picture: undefined,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
workspaceInvitationFindInvitationByWorkspaceSubdomainAndUserEmailMock,
|
|
||||||
).not.toHaveBeenCalled();
|
|
||||||
expect(
|
|
||||||
workspaceInvitationInvalidateWorkspaceInvitationMock,
|
|
||||||
).toHaveBeenCalledWith(workspaceId, email);
|
|
||||||
});
|
|
||||||
it('signInUp - sso - existing user - personal invitation token', async () => {
|
|
||||||
const email = 'existinguser@test.com';
|
|
||||||
const workspaceId = 'workspace-id';
|
|
||||||
const workspacePersonalInviteToken = 'personal-token-value';
|
|
||||||
const existingUser = {
|
|
||||||
id: 'user-id',
|
|
||||||
email,
|
|
||||||
passwordHash: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
UserFindOneMock.mockReturnValueOnce(existingUser);
|
|
||||||
workspaceInvitationValidateInvitationMock.mockReturnValueOnce({
|
|
||||||
isValid: true,
|
|
||||||
workspace: {
|
|
||||||
id: workspaceId,
|
|
||||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
workspaceInvitationInvalidateWorkspaceInvitationMock.mockReturnValueOnce(
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
await service.signInUp({
|
|
||||||
email,
|
|
||||||
fromSSO: true,
|
|
||||||
workspacePersonalInviteToken,
|
|
||||||
targetWorkspaceSubdomain: 'testSubDomain',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
workspaceInvitationInvalidateWorkspaceInvitationMock,
|
|
||||||
).toHaveBeenCalledWith(workspaceId, email);
|
|
||||||
});
|
|
||||||
it('signInUp - credentials - existing user', async () => {
|
|
||||||
const email = 'existinguser@test.com';
|
|
||||||
const password = 'validPassword123';
|
|
||||||
const existingUser = {
|
|
||||||
id: 'user-id',
|
|
||||||
email,
|
|
||||||
passwordHash: 'hash-of-validPassword123',
|
|
||||||
};
|
|
||||||
|
|
||||||
UserFindOneMock.mockReturnValueOnce(existingUser);
|
|
||||||
|
|
||||||
EnvironmentServiceGetMock.mockReturnValueOnce(false);
|
|
||||||
|
|
||||||
WorkspaceFindOneMock.mockReturnValueOnce({
|
|
||||||
id: 'another-workspace',
|
|
||||||
});
|
|
||||||
|
|
||||||
(bcrypt.compare as jest.Mock).mockReturnValueOnce(true);
|
|
||||||
|
|
||||||
await service.signInUp({
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
fromSSO: false,
|
|
||||||
targetWorkspaceSubdomain: 'testSubDomain',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
workspaceInvitationInvalidateWorkspaceInvitationMock,
|
|
||||||
).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
it('signInUp - credentials - new user', async () => {
|
|
||||||
const email = 'newuser@test.com';
|
|
||||||
const password = 'validPassword123';
|
|
||||||
|
|
||||||
UserFindOneMock.mockReturnValueOnce(null);
|
|
||||||
|
|
||||||
UserCreateMock.mockReturnValueOnce({} as User);
|
|
||||||
UserSaveMock.mockReturnValueOnce({} as User);
|
|
||||||
|
|
||||||
EnvironmentServiceGetMock.mockReturnValueOnce(true);
|
|
||||||
|
|
||||||
WorkspaceCreateMock.mockReturnValueOnce({});
|
|
||||||
WorkspaceSaveMock.mockReturnValueOnce({});
|
|
||||||
|
|
||||||
await service.signInUp({
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
fromSSO: false,
|
|
||||||
targetWorkspaceSubdomain: 'testSubDomain',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(UserCreateMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(UserSaveMock).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
expect(WorkspaceSaveMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(WorkspaceCreateMock).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
it('signInUp - credentials - new user - personal invitation token', async () => {
|
|
||||||
const email = 'newuser@test.com';
|
|
||||||
const password = 'validPassword123';
|
|
||||||
const workspaceId = 'workspace-id';
|
|
||||||
const workspacePersonalInviteToken = 'personal-token-value';
|
|
||||||
|
|
||||||
UserFindOneMock.mockReturnValueOnce(null);
|
|
||||||
workspaceInvitationValidateInvitationMock.mockReturnValueOnce({
|
|
||||||
isValid: true,
|
|
||||||
workspace: {
|
|
||||||
id: workspaceId,
|
|
||||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
workspaceInvitationInvalidateWorkspaceInvitationMock.mockReturnValueOnce(
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
UserCreateMock.mockReturnValueOnce({} as User);
|
|
||||||
UserSaveMock.mockReturnValueOnce({} as User);
|
|
||||||
|
|
||||||
await service.signInUp({
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
fromSSO: false,
|
|
||||||
workspacePersonalInviteToken,
|
|
||||||
targetWorkspaceSubdomain: 'testSubDomain',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(UserCreateMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(UserSaveMock).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
workspaceInvitationInvalidateWorkspaceInvitationMock,
|
|
||||||
).toHaveBeenCalledWith(workspaceId, email);
|
|
||||||
});
|
|
||||||
it('signInUp - credentials - new user - public invitation token', async () => {
|
|
||||||
const email = 'newuser@test.com';
|
|
||||||
const password = 'validPassword123';
|
|
||||||
const workspaceId = 'workspace-id';
|
|
||||||
const workspaceInviteHash = 'public-token-value';
|
|
||||||
|
|
||||||
UserFindOneMock.mockReturnValueOnce(null);
|
|
||||||
workspaceInvitationValidateInvitationMock.mockReturnValueOnce({
|
|
||||||
isValid: true,
|
|
||||||
workspace: {
|
|
||||||
id: workspaceId,
|
|
||||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
workspaceInvitationInvalidateWorkspaceInvitationMock.mockReturnValueOnce(
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
UserCreateMock.mockReturnValueOnce({} as User);
|
|
||||||
UserSaveMock.mockReturnValueOnce({} as User);
|
|
||||||
|
|
||||||
await service.signInUp({
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
fromSSO: false,
|
|
||||||
targetWorkspaceSubdomain: 'testSubDomain',
|
|
||||||
workspaceInviteHash,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(UserCreateMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(UserSaveMock).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
workspaceInvitationInvalidateWorkspaceInvitationMock,
|
|
||||||
).toHaveBeenCalledWith(workspaceId, email);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -23,11 +23,8 @@ import { EnvironmentService } from 'src/engine/core-modules/environment/environm
|
|||||||
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
|
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
|
||||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
|
||||||
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 { WorkspaceAuthProvider } from 'src/engine/core-modules/workspace/types/workspace.type';
|
|
||||||
import {
|
import {
|
||||||
Workspace,
|
Workspace,
|
||||||
WorkspaceActivationStatus,
|
WorkspaceActivationStatus,
|
||||||
@ -35,21 +32,15 @@ import {
|
|||||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||||
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
|
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
|
||||||
import { getImageBufferFromUrl } from 'src/utils/image';
|
import { getImageBufferFromUrl } from 'src/utils/image';
|
||||||
import { isDefined } from 'src/utils/is-defined';
|
|
||||||
import { isWorkEmail } from 'src/utils/is-work-email';
|
import { isWorkEmail } from 'src/utils/is-work-email';
|
||||||
|
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||||
export type SignInUpServiceInput = {
|
import {
|
||||||
email: string;
|
AuthProviderWithPasswordType,
|
||||||
password?: string;
|
ExistingUserOrPartialUserWithPicture,
|
||||||
firstName?: string | null;
|
PartialUserWithPicture,
|
||||||
lastName?: string | null;
|
SignInUpBaseParams,
|
||||||
workspaceInviteHash?: string;
|
SignInUpNewUserPayload,
|
||||||
workspacePersonalInviteToken?: string;
|
} from 'src/engine/core-modules/auth/types/signInUp.type';
|
||||||
picture?: string | null;
|
|
||||||
fromSSO: boolean;
|
|
||||||
targetWorkspaceSubdomain?: string;
|
|
||||||
authProvider?: WorkspaceAuthProvider;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
||||||
@ -66,23 +57,112 @@ export class SignInUpService {
|
|||||||
private readonly httpService: HttpService,
|
private readonly httpService: HttpService,
|
||||||
private readonly environmentService: EnvironmentService,
|
private readonly environmentService: EnvironmentService,
|
||||||
private readonly domainManagerService: DomainManagerService,
|
private readonly domainManagerService: DomainManagerService,
|
||||||
private readonly userService: UserService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async signInUp({
|
async computeParamsForNewUser(
|
||||||
email,
|
newUserParams: SignInUpNewUserPayload,
|
||||||
workspacePersonalInviteToken,
|
authParams: AuthProviderWithPasswordType['authParams'],
|
||||||
workspaceInviteHash,
|
) {
|
||||||
|
if (!newUserParams.firstName) newUserParams.firstName = '';
|
||||||
|
if (!newUserParams.lastName) newUserParams.lastName = '';
|
||||||
|
|
||||||
|
if (!newUserParams?.email) {
|
||||||
|
throw new AuthException(
|
||||||
|
'Email is required',
|
||||||
|
AuthExceptionCode.INVALID_INPUT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authParams.provider === 'password') {
|
||||||
|
newUserParams.passwordHash = await this.generateHash(authParams.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newUserParams as PartialUserWithPicture;
|
||||||
|
}
|
||||||
|
|
||||||
|
async signInUp(
|
||||||
|
params: SignInUpBaseParams &
|
||||||
|
ExistingUserOrPartialUserWithPicture &
|
||||||
|
AuthProviderWithPasswordType,
|
||||||
|
) {
|
||||||
|
// with personal invitation flow
|
||||||
|
if (params.workspace && params.invitation) {
|
||||||
|
return {
|
||||||
|
workspace: params.workspace,
|
||||||
|
user: await this.signInUpWithPersonalInvitation({
|
||||||
|
invitation: params.invitation,
|
||||||
|
userData: params.userData,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// with global invitation flow
|
||||||
|
if (params.workspace) {
|
||||||
|
const updatedUser = await this.signInUpOnExistingWorkspace({
|
||||||
|
workspace: params.workspace,
|
||||||
|
userData: params.userData,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { user: updatedUser, workspace: params.workspace };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.userData.type === 'newUserWithPicture') {
|
||||||
|
return await this.signUpOnNewWorkspace(
|
||||||
|
params.userData.newUserWithPicture,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// should never happen.
|
||||||
|
throw new Error('Invalid sign in up params');
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateHash(password: string) {
|
||||||
|
const isPasswordValid = PASSWORD_REGEX.test(password);
|
||||||
|
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
throw new AuthException(
|
||||||
|
'Password too weak',
|
||||||
|
AuthExceptionCode.INVALID_INPUT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await hashPassword(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
async validatePassword({
|
||||||
password,
|
password,
|
||||||
firstName,
|
passwordHash,
|
||||||
lastName,
|
}: {
|
||||||
picture,
|
password: string;
|
||||||
fromSSO,
|
passwordHash: string;
|
||||||
targetWorkspaceSubdomain,
|
}) {
|
||||||
authProvider,
|
const isValid = await compareHash(
|
||||||
}: SignInUpServiceInput) {
|
await this.generateHash(password),
|
||||||
if (!firstName) firstName = '';
|
passwordHash,
|
||||||
if (!lastName) lastName = '';
|
);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
throw new AuthException(
|
||||||
|
'Wrong password',
|
||||||
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async signInUpWithPersonalInvitation(
|
||||||
|
params: { invitation: AppToken } & ExistingUserOrPartialUserWithPicture,
|
||||||
|
) {
|
||||||
|
if (!params.invitation) {
|
||||||
|
throw new AuthException(
|
||||||
|
'Invitation not found',
|
||||||
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const email =
|
||||||
|
params.userData.type === 'newUserWithPicture'
|
||||||
|
? params.userData.newUserWithPicture.email
|
||||||
|
: params.userData.existingUser.email;
|
||||||
|
|
||||||
if (!email) {
|
if (!email) {
|
||||||
throw new AuthException(
|
throw new AuthException(
|
||||||
@ -91,264 +171,90 @@ export class SignInUpService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password) {
|
|
||||||
const isPasswordValid = PASSWORD_REGEX.test(password);
|
|
||||||
|
|
||||||
if (!isPasswordValid) {
|
|
||||||
throw new AuthException(
|
|
||||||
'Password too weak',
|
|
||||||
AuthExceptionCode.INVALID_INPUT,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const passwordHash = password ? await hashPassword(password) : undefined;
|
|
||||||
|
|
||||||
const existingUser = await this.userRepository.findOne({
|
|
||||||
where: { email },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingUser && !fromSSO) {
|
|
||||||
const isValid = await compareHash(
|
|
||||||
password || '',
|
|
||||||
existingUser.passwordHash,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isValid) {
|
|
||||||
throw new AuthException(
|
|
||||||
'Wrong password',
|
|
||||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const signInUpWithInvitationResult = targetWorkspaceSubdomain
|
|
||||||
? await this.signInUpWithInvitation({
|
|
||||||
email,
|
|
||||||
workspacePersonalInviteToken,
|
|
||||||
workspaceInviteHash,
|
|
||||||
targetWorkspaceSubdomain,
|
|
||||||
fromSSO,
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
picture,
|
|
||||||
authProvider,
|
|
||||||
passwordHash,
|
|
||||||
existingUser,
|
|
||||||
})
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (isDefined(signInUpWithInvitationResult)) {
|
|
||||||
return signInUpWithInvitationResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existingUser) {
|
|
||||||
return await this.signUpOnNewWorkspace({
|
|
||||||
email,
|
|
||||||
passwordHash,
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
picture,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspace =
|
|
||||||
await this.domainManagerService.getWorkspaceBySubdomainOrDefaultWorkspace(
|
|
||||||
targetWorkspaceSubdomain,
|
|
||||||
);
|
|
||||||
|
|
||||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
|
||||||
|
|
||||||
return await this.validateSignIn({
|
|
||||||
user: existingUser,
|
|
||||||
workspace,
|
|
||||||
authProvider,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async signInUpWithInvitation({
|
|
||||||
email,
|
|
||||||
workspacePersonalInviteToken,
|
|
||||||
workspaceInviteHash,
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
picture,
|
|
||||||
fromSSO,
|
|
||||||
targetWorkspaceSubdomain,
|
|
||||||
authProvider,
|
|
||||||
passwordHash,
|
|
||||||
existingUser,
|
|
||||||
}: {
|
|
||||||
email: string;
|
|
||||||
workspacePersonalInviteToken?: string;
|
|
||||||
workspaceInviteHash?: string;
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
picture?: string | null;
|
|
||||||
authProvider?: WorkspaceAuthProvider;
|
|
||||||
passwordHash?: string;
|
|
||||||
existingUser: User | null;
|
|
||||||
fromSSO: boolean;
|
|
||||||
targetWorkspaceSubdomain: string;
|
|
||||||
}) {
|
|
||||||
const maybeInvitation =
|
|
||||||
fromSSO && !workspacePersonalInviteToken && !workspaceInviteHash
|
|
||||||
? await this.workspaceInvitationService.findInvitationByWorkspaceSubdomainAndUserEmail(
|
|
||||||
{
|
|
||||||
subdomain: targetWorkspaceSubdomain,
|
|
||||||
email,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const invitationValidation =
|
const invitationValidation =
|
||||||
workspacePersonalInviteToken || workspaceInviteHash || maybeInvitation
|
await this.workspaceInvitationService.validateInvitation({
|
||||||
? await this.workspaceInvitationService.validateInvitation({
|
workspacePersonalInviteToken: params.invitation.value,
|
||||||
workspacePersonalInviteToken:
|
|
||||||
workspacePersonalInviteToken ?? maybeInvitation?.value,
|
|
||||||
workspaceInviteHash,
|
|
||||||
email,
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (
|
|
||||||
invitationValidation?.isValid === true &&
|
|
||||||
invitationValidation.workspace
|
|
||||||
) {
|
|
||||||
const updatedUser = await this.signInUpOnExistingWorkspace({
|
|
||||||
email,
|
email,
|
||||||
passwordHash,
|
|
||||||
workspace: invitationValidation.workspace,
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
picture,
|
|
||||||
existingUser,
|
|
||||||
authProvider,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.workspaceInvitationService.invalidateWorkspaceInvitation(
|
if (!invitationValidation?.isValid) {
|
||||||
invitationValidation.workspace.id,
|
throw new AuthException(
|
||||||
email,
|
'Invitation not found',
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
user: updatedUser,
|
|
||||||
workspace: invitationValidation.workspace,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async validateSignIn({
|
|
||||||
user,
|
|
||||||
workspace,
|
|
||||||
authProvider,
|
|
||||||
}: {
|
|
||||||
user: User;
|
|
||||||
workspace: Workspace;
|
|
||||||
authProvider: SignInUpServiceInput['authProvider'];
|
|
||||||
}) {
|
|
||||||
if (authProvider) {
|
|
||||||
workspaceValidator.isAuthEnabledOrThrow(authProvider, workspace);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.userService.hasUserAccessToWorkspaceOrThrow(
|
|
||||||
user.id,
|
|
||||||
workspace.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
return { user, workspace };
|
|
||||||
}
|
|
||||||
|
|
||||||
async signInUpOnExistingWorkspace({
|
|
||||||
email,
|
|
||||||
passwordHash,
|
|
||||||
workspace,
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
picture,
|
|
||||||
existingUser,
|
|
||||||
authProvider,
|
|
||||||
}: {
|
|
||||||
email: string;
|
|
||||||
passwordHash: string | undefined;
|
|
||||||
workspace: Workspace;
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
picture: SignInUpServiceInput['picture'];
|
|
||||||
existingUser: User | null;
|
|
||||||
authProvider?: WorkspaceAuthProvider;
|
|
||||||
}) {
|
|
||||||
const isNewUser = !isDefined(existingUser);
|
|
||||||
let user = existingUser;
|
|
||||||
|
|
||||||
workspaceValidator.assertIsDefinedOrThrow(
|
|
||||||
workspace,
|
|
||||||
new AuthException(
|
|
||||||
'Workspace not found',
|
|
||||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
),
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedUser = await this.signInUpOnExistingWorkspace({
|
||||||
|
workspace: invitationValidation.workspace,
|
||||||
|
userData: params.userData,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.workspaceInvitationService.invalidateWorkspaceInvitation(
|
||||||
|
invitationValidation.workspace.id,
|
||||||
|
email,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return updatedUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async persistNewUser(
|
||||||
|
newUser: PartialUserWithPicture,
|
||||||
|
workspace: Workspace,
|
||||||
|
) {
|
||||||
|
const imagePath = await this.uploadPicture(newUser.picture, workspace.id);
|
||||||
|
|
||||||
|
delete newUser.picture;
|
||||||
|
|
||||||
|
const userToCreate = this.userRepository.create({
|
||||||
|
...newUser,
|
||||||
|
defaultAvatarUrl: imagePath,
|
||||||
|
canImpersonate: false,
|
||||||
|
} as Partial<User>);
|
||||||
|
|
||||||
|
return await this.userRepository.save(userToCreate);
|
||||||
|
}
|
||||||
|
|
||||||
|
async signInUpOnExistingWorkspace(
|
||||||
|
params: {
|
||||||
|
workspace: Workspace;
|
||||||
|
} & ExistingUserOrPartialUserWithPicture,
|
||||||
|
) {
|
||||||
workspaceValidator.assertIsActive(
|
workspaceValidator.assertIsActive(
|
||||||
workspace,
|
params.workspace,
|
||||||
new AuthException(
|
new AuthException(
|
||||||
'Workspace is not ready to welcome new members',
|
'Workspace is not ready to welcome new members',
|
||||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (authProvider) {
|
const currentUser =
|
||||||
workspaceValidator.isAuthEnabledOrThrow(authProvider, workspace);
|
params.userData.type === 'newUserWithPicture'
|
||||||
}
|
? await this.persistNewUser(
|
||||||
|
params.userData.newUserWithPicture,
|
||||||
if (isNewUser) {
|
params.workspace,
|
||||||
const imagePath = await this.uploadPicture(picture, workspace.id);
|
)
|
||||||
|
: params.userData.existingUser;
|
||||||
const userToCreate = this.userRepository.create({
|
|
||||||
email: email,
|
|
||||||
firstName: firstName,
|
|
||||||
lastName: lastName,
|
|
||||||
defaultAvatarUrl: imagePath,
|
|
||||||
canImpersonate: false,
|
|
||||||
passwordHash,
|
|
||||||
});
|
|
||||||
|
|
||||||
user = await this.userRepository.save(userToCreate);
|
|
||||||
}
|
|
||||||
|
|
||||||
userValidator.assertIsDefinedOrThrow(
|
|
||||||
user,
|
|
||||||
new AuthException(
|
|
||||||
'User not found',
|
|
||||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedUser = await this.userWorkspaceService.addUserToWorkspace(
|
const updatedUser = await this.userWorkspaceService.addUserToWorkspace(
|
||||||
user,
|
currentUser,
|
||||||
workspace,
|
params.workspace,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.activateOnboardingForUser(user, workspace, {
|
const user = Object.assign(currentUser, updatedUser);
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
});
|
|
||||||
|
|
||||||
return Object.assign(user, updatedUser);
|
await this.activateOnboardingForUser(user, params.workspace);
|
||||||
|
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async activateOnboardingForUser(
|
private async activateOnboardingForUser(user: User, workspace: Workspace) {
|
||||||
user: User,
|
|
||||||
workspace: Workspace,
|
|
||||||
{ firstName, lastName }: { firstName: string; lastName: string },
|
|
||||||
) {
|
|
||||||
await this.onboardingService.setOnboardingConnectAccountPending({
|
await this.onboardingService.setOnboardingConnectAccountPending({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
value: true,
|
value: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (firstName === '' && lastName === '') {
|
if (user.firstName === '' && user.lastName === '') {
|
||||||
await this.onboardingService.setOnboardingCreateProfilePending({
|
await this.onboardingService.setOnboardingCreateProfilePending({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
@ -357,27 +263,19 @@ export class SignInUpService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async signUpOnNewWorkspace({
|
async signUpOnNewWorkspace(partialUserWithPicture: PartialUserWithPicture) {
|
||||||
email,
|
const user: PartialUserWithPicture = {
|
||||||
passwordHash,
|
...partialUserWithPicture,
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
picture,
|
|
||||||
}: {
|
|
||||||
email: string;
|
|
||||||
passwordHash: string | undefined;
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
picture: SignInUpServiceInput['picture'];
|
|
||||||
}) {
|
|
||||||
const user: Partial<User> = {
|
|
||||||
email,
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
canImpersonate: false,
|
canImpersonate: false,
|
||||||
passwordHash,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!user.email) {
|
||||||
|
throw new AuthException(
|
||||||
|
'Email is required',
|
||||||
|
AuthExceptionCode.INVALID_INPUT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')) {
|
if (!this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')) {
|
||||||
const workspacesCount = await this.workspaceRepository.count();
|
const workspacesCount = await this.workspaceRepository.count();
|
||||||
|
|
||||||
@ -393,7 +291,7 @@ export class SignInUpService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const logoUrl = `${TWENTY_ICONS_BASE_URL}/${getDomainNameByEmail(email)}`;
|
const logoUrl = `${TWENTY_ICONS_BASE_URL}/${getDomainNameByEmail(user.email)}`;
|
||||||
const isLogoUrlValid = async () => {
|
const isLogoUrlValid = async () => {
|
||||||
try {
|
try {
|
||||||
return (
|
return (
|
||||||
@ -406,7 +304,7 @@ export class SignInUpService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const logo =
|
const logo =
|
||||||
isWorkEmail(email) && (await isLogoUrlValid()) ? logoUrl : undefined;
|
isWorkEmail(user.email) && (await isLogoUrlValid()) ? logoUrl : undefined;
|
||||||
|
|
||||||
const workspaceToCreate = this.workspaceRepository.create({
|
const workspaceToCreate = this.workspaceRepository.create({
|
||||||
subdomain: await this.domainManagerService.generateSubdomain(),
|
subdomain: await this.domainManagerService.generateSubdomain(),
|
||||||
@ -419,7 +317,10 @@ export class SignInUpService {
|
|||||||
|
|
||||||
const workspace = await this.workspaceRepository.save(workspaceToCreate);
|
const workspace = await this.workspaceRepository.save(workspaceToCreate);
|
||||||
|
|
||||||
user.defaultAvatarUrl = await this.uploadPicture(picture, workspace.id);
|
user.defaultAvatarUrl = await this.uploadPicture(
|
||||||
|
partialUserWithPicture.picture,
|
||||||
|
workspace.id,
|
||||||
|
);
|
||||||
|
|
||||||
const userCreated = this.userRepository.create(user);
|
const userCreated = this.userRepository.create(user);
|
||||||
|
|
||||||
@ -427,10 +328,7 @@ export class SignInUpService {
|
|||||||
|
|
||||||
await this.userWorkspaceService.create(newUser.id, workspace.id);
|
await this.userWorkspaceService.create(newUser.id, workspace.id);
|
||||||
|
|
||||||
await this.activateOnboardingForUser(newUser, workspace, {
|
await this.activateOnboardingForUser(newUser, workspace);
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.onboardingService.setOnboardingInviteTeamPending({
|
await this.onboardingService.setOnboardingInviteTeamPending({
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
|
|||||||
@ -0,0 +1,68 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||||
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import { WorkspaceAuthProvider } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SocialSsoService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Workspace, 'core')
|
||||||
|
private readonly workspaceRepository: Repository<Workspace>,
|
||||||
|
private readonly environmentService: EnvironmentService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private getAuthProviderColumnNameByProvider(
|
||||||
|
authProvider: WorkspaceAuthProvider,
|
||||||
|
) {
|
||||||
|
if (authProvider === 'google') {
|
||||||
|
return 'isGoogleAuthEnabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authProvider === 'microsoft') {
|
||||||
|
return 'isMicrosoftAuthEnabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authProvider === 'password') {
|
||||||
|
return 'isPasswordAuthEnabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`${authProvider} is not a valid auth provider.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findWorkspaceFromWorkspaceIdOrAuthProvider(
|
||||||
|
{
|
||||||
|
authProvider,
|
||||||
|
email,
|
||||||
|
}: { authProvider: WorkspaceAuthProvider; email: string },
|
||||||
|
workspaceId?: string,
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
this.environmentService.get('IS_MULTIWORKSPACE_ENABLED') &&
|
||||||
|
!workspaceId
|
||||||
|
) {
|
||||||
|
// Multi-workspace enable mode but on non workspace url.
|
||||||
|
// so get the first workspace with the current auth method enable
|
||||||
|
const workspace = await this.workspaceRepository.findOne({
|
||||||
|
where: {
|
||||||
|
[this.getAuthProviderColumnNameByProvider(authProvider)]: true,
|
||||||
|
workspaceUsers: {
|
||||||
|
user: {
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
relations: ['workspaceUsers', 'workspaceUsers.user'],
|
||||||
|
});
|
||||||
|
|
||||||
|
return workspace ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.workspaceRepository.findOneBy({
|
||||||
|
id: workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,7 +17,7 @@ export type GoogleRequest = Omit<
|
|||||||
picture: string | null;
|
picture: string | null;
|
||||||
workspaceInviteHash?: string;
|
workspaceInviteHash?: string;
|
||||||
workspacePersonalInviteToken?: string;
|
workspacePersonalInviteToken?: string;
|
||||||
targetWorkspaceSubdomain?: string;
|
workspaceId?: string;
|
||||||
billingCheckoutSessionState?: string;
|
billingCheckoutSessionState?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -39,7 +39,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
|
|||||||
...options,
|
...options,
|
||||||
state: JSON.stringify({
|
state: JSON.stringify({
|
||||||
workspaceInviteHash: req.params.workspaceInviteHash,
|
workspaceInviteHash: req.params.workspaceInviteHash,
|
||||||
workspaceSubdomain: req.params.workspaceSubdomain,
|
workspaceId: req.params.workspaceId,
|
||||||
...(req.params.billingCheckoutSessionState
|
...(req.params.billingCheckoutSessionState
|
||||||
? {
|
? {
|
||||||
billingCheckoutSessionState:
|
billingCheckoutSessionState:
|
||||||
@ -78,7 +78,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
|
|||||||
picture: photos?.[0]?.value,
|
picture: photos?.[0]?.value,
|
||||||
workspaceInviteHash: state.workspaceInviteHash,
|
workspaceInviteHash: state.workspaceInviteHash,
|
||||||
workspacePersonalInviteToken: state.workspacePersonalInviteToken,
|
workspacePersonalInviteToken: state.workspacePersonalInviteToken,
|
||||||
targetWorkspaceSubdomain: state.workspaceSubdomain,
|
workspaceId: state.workspaceId,
|
||||||
billingCheckoutSessionState: state.billingCheckoutSessionState,
|
billingCheckoutSessionState: state.billingCheckoutSessionState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export type MicrosoftRequest = Omit<
|
|||||||
picture: string | null;
|
picture: string | null;
|
||||||
workspaceInviteHash?: string;
|
workspaceInviteHash?: string;
|
||||||
workspacePersonalInviteToken?: string;
|
workspacePersonalInviteToken?: string;
|
||||||
targetWorkspaceSubdomain?: string;
|
workspaceId?: string;
|
||||||
billingCheckoutSessionState?: string;
|
billingCheckoutSessionState?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -43,7 +43,7 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy, 'microsoft') {
|
|||||||
...options,
|
...options,
|
||||||
state: JSON.stringify({
|
state: JSON.stringify({
|
||||||
workspaceInviteHash: req.params.workspaceInviteHash,
|
workspaceInviteHash: req.params.workspaceInviteHash,
|
||||||
workspaceSubdomain: req.params.workspaceSubdomain,
|
workspaceId: req.params.workspaceId,
|
||||||
...(req.params.billingCheckoutSessionState
|
...(req.params.billingCheckoutSessionState
|
||||||
? {
|
? {
|
||||||
billingCheckoutSessionState:
|
billingCheckoutSessionState:
|
||||||
@ -92,7 +92,7 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy, 'microsoft') {
|
|||||||
picture: photos?.[0]?.value,
|
picture: photos?.[0]?.value,
|
||||||
workspaceInviteHash: state.workspaceInviteHash,
|
workspaceInviteHash: state.workspaceInviteHash,
|
||||||
workspacePersonalInviteToken: state.workspacePersonalInviteToken,
|
workspacePersonalInviteToken: state.workspacePersonalInviteToken,
|
||||||
targetWorkspaceSubdomain: state.workspaceSubdomain,
|
workspaceId: state.workspaceId,
|
||||||
billingCheckoutSessionState: state.billingCheckoutSessionState,
|
billingCheckoutSessionState: state.billingCheckoutSessionState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,51 @@
|
|||||||
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
|
import { WorkspaceAuthProvider } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||||
|
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||||
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
|
||||||
|
export type SignInUpBaseParams = {
|
||||||
|
invitation?: AppToken;
|
||||||
|
workspace?: Workspace | null;
|
||||||
|
billingCheckoutSessionState?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SignInUpNewUserPayload = {
|
||||||
|
email: string;
|
||||||
|
firstName?: string | null;
|
||||||
|
lastName?: string | null;
|
||||||
|
picture?: string | null;
|
||||||
|
passwordHash?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PartialUserWithPicture = {
|
||||||
|
picture?: string;
|
||||||
|
} & Partial<User>;
|
||||||
|
|
||||||
|
export type ExistingUserOrNewUser = {
|
||||||
|
userData:
|
||||||
|
| { type: 'existingUser'; existingUser: User }
|
||||||
|
| {
|
||||||
|
type: 'newUser';
|
||||||
|
newUserPayload: SignInUpNewUserPayload;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExistingUserOrPartialUserWithPicture = {
|
||||||
|
userData:
|
||||||
|
| { type: 'existingUser'; existingUser: User }
|
||||||
|
| {
|
||||||
|
type: 'newUserWithPicture';
|
||||||
|
newUserWithPicture: PartialUserWithPicture;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AuthProviderWithPasswordType = {
|
||||||
|
authParams:
|
||||||
|
| {
|
||||||
|
provider: Extract<WorkspaceAuthProvider, 'password'>;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
provider: Exclude<WorkspaceAuthProvider, 'password'>;
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -129,15 +129,16 @@ export class DomainManagerService {
|
|||||||
return subdomain === this.environmentService.get('DEFAULT_SUBDOMAIN');
|
return subdomain === this.environmentService.get('DEFAULT_SUBDOMAIN');
|
||||||
}
|
}
|
||||||
|
|
||||||
computeRedirectErrorUrl({
|
computeRedirectErrorUrl(
|
||||||
errorMessage,
|
errorMessage: string,
|
||||||
subdomain,
|
{
|
||||||
}: {
|
|
||||||
errorMessage: string;
|
|
||||||
subdomain?: string;
|
|
||||||
}) {
|
|
||||||
const url = this.buildWorkspaceURL({
|
|
||||||
subdomain,
|
subdomain,
|
||||||
|
}: {
|
||||||
|
subdomain?: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const url = this.buildWorkspaceURL({
|
||||||
|
subdomain: subdomain ?? this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||||
pathname: '/verify',
|
pathname: '/verify',
|
||||||
searchParams: { errorMessage },
|
searchParams: { errorMessage },
|
||||||
});
|
});
|
||||||
@ -193,10 +194,12 @@ export class DomainManagerService {
|
|||||||
|
|
||||||
if (!isDefined(subdomain)) return;
|
if (!isDefined(subdomain)) return;
|
||||||
|
|
||||||
return await this.workspaceRepository.findOne({
|
return (
|
||||||
where: { subdomain },
|
(await this.workspaceRepository.findOne({
|
||||||
relations: ['workspaceSSOIdentityProviders'],
|
where: { subdomain },
|
||||||
});
|
relations: ['workspaceSSOIdentityProviders'],
|
||||||
|
})) ?? undefined
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new WorkspaceException(
|
throw new WorkspaceException(
|
||||||
'Workspace not found',
|
'Workspace not found',
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
export type WorkspaceAuthProvider = 'google' | 'microsoft' | 'password';
|
export type WorkspaceAuthProvider = 'google' | 'microsoft' | 'password' | 'sso';
|
||||||
|
|||||||
@ -46,6 +46,7 @@ const isAuthEnabledOrThrow = (
|
|||||||
if (provider === 'google' && workspace.isGoogleAuthEnabled) return true;
|
if (provider === 'google' && workspace.isGoogleAuthEnabled) return true;
|
||||||
if (provider === 'microsoft' && workspace.isMicrosoftAuthEnabled) return true;
|
if (provider === 'microsoft' && workspace.isMicrosoftAuthEnabled) return true;
|
||||||
if (provider === 'password' && workspace.isPasswordAuthEnabled) return true;
|
if (provider === 'password' && workspace.isPasswordAuthEnabled) return true;
|
||||||
|
if (provider === 'sso') return true;
|
||||||
|
|
||||||
throw exceptionToThrowCustom;
|
throw exceptionToThrowCustom;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user