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 * as Apollo from '@apollo/client';
|
||||
export type Maybe<T> = T | null;
|
||||
export type InputMaybe<T> = Maybe<T>;
|
||||
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']>;
|
||||
email: Scalars['String'];
|
||||
password: Scalars['String'];
|
||||
workspaceId?: InputMaybe<Scalars['String']>;
|
||||
workspaceInviteHash?: InputMaybe<Scalars['String']>;
|
||||
workspacePersonalInviteToken?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
@ -1977,6 +1978,7 @@ export type SignUpMutationVariables = Exact<{
|
||||
workspaceInviteHash?: InputMaybe<Scalars['String']>;
|
||||
workspacePersonalInviteToken?: 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 RenewTokenMutationOptions = Apollo.BaseMutationOptions<RenewTokenMutation, RenewTokenMutationVariables>;
|
||||
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(
|
||||
email: $email
|
||||
password: $password
|
||||
workspaceInviteHash: $workspaceInviteHash
|
||||
workspacePersonalInviteToken: $workspacePersonalInviteToken
|
||||
captchaToken: $captchaToken
|
||||
workspaceId: $workspaceId
|
||||
) {
|
||||
loginToken {
|
||||
...AuthTokenFragment
|
||||
@ -3027,6 +3030,7 @@ export type SignUpMutationFn = Apollo.MutationFunction<SignUpMutation, SignUpMut
|
||||
* workspaceInviteHash: // value for 'workspaceInviteHash'
|
||||
* workspacePersonalInviteToken: // value for 'workspacePersonalInviteToken'
|
||||
* captchaToken: // value for 'captchaToken'
|
||||
* workspaceId: // value for 'workspaceId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
@ -7,6 +7,7 @@ export const SIGN_UP = gql`
|
||||
$workspaceInviteHash: String
|
||||
$workspacePersonalInviteToken: String = null
|
||||
$captchaToken: String
|
||||
$workspaceId: String
|
||||
) {
|
||||
signUp(
|
||||
email: $email
|
||||
@ -14,6 +15,7 @@ export const SIGN_UP = gql`
|
||||
workspaceInviteHash: $workspaceInviteHash
|
||||
workspacePersonalInviteToken: $workspacePersonalInviteToken
|
||||
captchaToken: $captchaToken
|
||||
workspaceId: $workspaceId
|
||||
) {
|
||||
loginToken {
|
||||
...AuthTokenFragment
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
useSetRecoilState,
|
||||
} from 'recoil';
|
||||
import { iconsState } from 'twenty-ui';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
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 { useIsCurrentLocationOnAWorkspaceSubdomain } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspaceSubdomain';
|
||||
import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain';
|
||||
import { useReadWorkspaceSubdomainFromCurrentLocation } from '@/domain-manager/hooks/useReadWorkspaceSubdomainFromCurrentLocation';
|
||||
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
|
||||
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
||||
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
|
||||
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
|
||||
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
|
||||
|
||||
export const useAuth = () => {
|
||||
const setTokenPair = useSetRecoilState(tokenPairState);
|
||||
@ -82,7 +82,8 @@ export const useAuth = () => {
|
||||
|
||||
const { isOnAWorkspaceSubdomain } =
|
||||
useIsCurrentLocationOnAWorkspaceSubdomain();
|
||||
const { workspaceSubdomain } = useReadWorkspaceSubdomainFromCurrentLocation();
|
||||
|
||||
const workspacePublicData = useRecoilValue(workspacePublicDataState);
|
||||
|
||||
const { setLastAuthenticateWorkspaceDomain } =
|
||||
useLastAuthenticatedWorkspaceDomain();
|
||||
@ -328,6 +329,9 @@ export const useAuth = () => {
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
captchaToken,
|
||||
...(workspacePublicData?.id
|
||||
? { workspaceId: workspacePublicData.id }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
@ -354,6 +358,7 @@ export const useAuth = () => {
|
||||
[
|
||||
setIsVerifyPendingState,
|
||||
signUp,
|
||||
workspacePublicData,
|
||||
isMultiWorkspaceEnabled,
|
||||
handleVerify,
|
||||
redirectToWorkspaceDomain,
|
||||
@ -386,13 +391,13 @@ export const useAuth = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isDefined(workspaceSubdomain)) {
|
||||
url.searchParams.set('workspaceSubdomain', workspaceSubdomain);
|
||||
if (isDefined(workspacePublicData)) {
|
||||
url.searchParams.set('workspaceId', workspacePublicData.id);
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
},
|
||||
[workspaceSubdomain],
|
||||
[workspacePublicData],
|
||||
);
|
||||
|
||||
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 { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.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';
|
||||
|
||||
@ -103,6 +104,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
||||
SwitchWorkspaceService,
|
||||
TransientTokenService,
|
||||
ApiKeyService,
|
||||
SocialSsoService,
|
||||
// reenable when working on: https://github.com/twentyhq/twenty/issues/9143
|
||||
// OAuthService,
|
||||
],
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { UseFilters, UseGuards } from '@nestjs/common';
|
||||
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 { AppTokenInput } from 'src/engine/core-modules/auth/dto/app-token.input';
|
||||
@ -55,6 +58,8 @@ import { AuthService } from './services/auth.service';
|
||||
@UseFilters(AuthGraphqlApiExceptionFilter)
|
||||
export class AuthResolver {
|
||||
constructor(
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
private authService: AuthService,
|
||||
private renewTokenService: RenewTokenService,
|
||||
private userService: UserService,
|
||||
@ -104,12 +109,14 @@ export class AuthResolver {
|
||||
origin,
|
||||
);
|
||||
|
||||
if (!workspace) {
|
||||
throw new AuthException(
|
||||
workspaceValidator.assertIsDefinedOrThrow(
|
||||
workspace,
|
||||
new AuthException(
|
||||
'Workspace not found',
|
||||
AuthExceptionCode.WORKSPACE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
|
||||
const user = await this.authService.challenge(challengeInput, workspace);
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
@ -121,16 +128,46 @@ export class AuthResolver {
|
||||
|
||||
@UseGuards(CaptchaGuard)
|
||||
@Mutation(() => SignUpOutput)
|
||||
async signUp(
|
||||
@Args() signUpInput: SignUpInput,
|
||||
@OriginHeader() origin: string,
|
||||
): Promise<SignUpOutput> {
|
||||
const { user, workspace } = await this.authService.signInUp({
|
||||
...signUpInput,
|
||||
targetWorkspaceSubdomain:
|
||||
this.domainManagerService.getWorkspaceSubdomainByOrigin(origin),
|
||||
fromSSO: false,
|
||||
async signUp(@Args() signUpInput: SignUpInput): Promise<SignUpOutput> {
|
||||
const currentWorkspace = await this.authService.findWorkspaceForSignInUp({
|
||||
workspaceInviteHash: signUpInput.workspaceInviteHash,
|
||||
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(
|
||||
|
||||
@ -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 { 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 { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
|
||||
@Controller('auth/google')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
@ -33,9 +32,8 @@ export class GoogleAuthController {
|
||||
private readonly loginTokenService: LoginTokenService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@ -49,57 +47,61 @@ export class GoogleAuthController {
|
||||
@UseGuards(GoogleProviderEnabledGuard, GoogleOauthGuard)
|
||||
@UseFilters(AuthOAuthExceptionFilter)
|
||||
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 {
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
picture,
|
||||
workspaceInviteHash,
|
||||
const invitation = await this.authService.findInvitationForSignInUp({
|
||||
currentWorkspace,
|
||||
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,
|
||||
} = 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(
|
||||
user.email,
|
||||
@ -107,20 +109,17 @@ export class GoogleAuthController {
|
||||
);
|
||||
|
||||
return res.redirect(
|
||||
this.authService.computeRedirectURI(
|
||||
loginToken.token,
|
||||
workspace.subdomain,
|
||||
this.authService.computeRedirectURI({
|
||||
loginToken: loginToken.token,
|
||||
subdomain: workspace.subdomain,
|
||||
billingCheckoutSessionState,
|
||||
),
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof AuthException) {
|
||||
return res.redirect(
|
||||
this.domainManagerService.computeRedirectErrorUrl({
|
||||
subdomain:
|
||||
req.user.targetWorkspaceSubdomain ??
|
||||
this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
errorMessage: err.message,
|
||||
this.domainManagerService.computeRedirectErrorUrl(err.message, {
|
||||
subdomain: currentWorkspace?.subdomain,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 { 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 { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
|
||||
@Controller('auth/microsoft')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
@ -29,9 +28,8 @@ export class MicrosoftAuthController {
|
||||
private readonly loginTokenService: LoginTokenService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@ -47,38 +45,60 @@ export class MicrosoftAuthController {
|
||||
@Req() req: MicrosoftRequest,
|
||||
@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 {
|
||||
const signInUpParams = req.user;
|
||||
const invitation = await this.authService.findInvitationForSignInUp({
|
||||
currentWorkspace,
|
||||
workspacePersonalInviteToken,
|
||||
email,
|
||||
});
|
||||
|
||||
if (
|
||||
this.environmentService.get('IS_MULTIWORKSPACE_ENABLED') &&
|
||||
(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'],
|
||||
});
|
||||
const existingUser = await this.userRepository.findOne({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
if (workspaceWithGoogleAuthActive) {
|
||||
signInUpParams.targetWorkspaceSubdomain =
|
||||
workspaceWithGoogleAuthActive.subdomain;
|
||||
}
|
||||
}
|
||||
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({
|
||||
...signInUpParams,
|
||||
fromSSO: true,
|
||||
authProvider: 'microsoft',
|
||||
invitation,
|
||||
workspace: currentWorkspace,
|
||||
userData,
|
||||
authParams: {
|
||||
provider: 'microsoft',
|
||||
},
|
||||
billingCheckoutSessionState,
|
||||
});
|
||||
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
@ -87,20 +107,18 @@ export class MicrosoftAuthController {
|
||||
);
|
||||
|
||||
return res.redirect(
|
||||
this.authService.computeRedirectURI(
|
||||
loginToken.token,
|
||||
workspace.subdomain,
|
||||
signInUpParams.billingCheckoutSessionState,
|
||||
),
|
||||
this.authService.computeRedirectURI({
|
||||
loginToken: loginToken.token,
|
||||
subdomain: workspace.subdomain,
|
||||
|
||||
billingCheckoutSessionState,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof AuthException) {
|
||||
return res.redirect(
|
||||
this.domainManagerService.computeRedirectErrorUrl({
|
||||
subdomain:
|
||||
req.user.targetWorkspaceSubdomain ??
|
||||
this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
errorMessage: err.message,
|
||||
this.domainManagerService.computeRedirectErrorUrl(err.message, {
|
||||
subdomain: currentWorkspace?.subdomain,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@ -30,9 +30,9 @@ import {
|
||||
IdentityProviderType,
|
||||
WorkspaceSSOIdentityProvider,
|
||||
} 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 { 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')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
@ -41,9 +41,10 @@ export class SSOAuthController {
|
||||
private readonly loginTokenService: LoginTokenService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly userService: UserService,
|
||||
private readonly ssoService: SSOService,
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
@InjectRepository(WorkspaceSSOIdentityProvider, 'core')
|
||||
private readonly workspaceSSOIdentityProviderRepository: Repository<WorkspaceSSOIdentityProvider>,
|
||||
) {}
|
||||
@ -81,50 +82,44 @@ export class SSOAuthController {
|
||||
@Get('oidc/callback')
|
||||
@UseGuards(SSOProviderEnabledGuard, OIDCAuthGuard)
|
||||
async oidcAuthCallback(@Req() req: any, @Res() res: Response) {
|
||||
try {
|
||||
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;
|
||||
}
|
||||
return this.authCallback(req, res);
|
||||
}
|
||||
|
||||
@Post('saml/callback/:identityProviderId')
|
||||
@UseGuards(SSOProviderEnabledGuard, SAMLAuthGuard)
|
||||
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 {
|
||||
const { loginToken, identityProvider } = await this.generateLoginToken(
|
||||
req.user,
|
||||
workspaceIdentityProvider,
|
||||
);
|
||||
|
||||
return res.redirect(
|
||||
this.authService.computeRedirectURI(
|
||||
loginToken.token,
|
||||
identityProvider.workspace.subdomain,
|
||||
),
|
||||
this.authService.computeRedirectURI({
|
||||
loginToken: loginToken.token,
|
||||
subdomain: identityProvider.workspace.subdomain,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof AuthException) {
|
||||
return res.redirect(
|
||||
this.domainManagerService.computeRedirectErrorUrl({
|
||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
errorMessage: err.message,
|
||||
this.domainManagerService.computeRedirectErrorUrl(err.message, {
|
||||
subdomain: workspaceIdentityProvider.workspace.subdomain,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@ -132,26 +127,19 @@ export class SSOAuthController {
|
||||
}
|
||||
}
|
||||
|
||||
private async generateLoginToken({
|
||||
user,
|
||||
identityProviderId,
|
||||
}: {
|
||||
identityProviderId?: string;
|
||||
user: { email: string } & Record<string, string>;
|
||||
}) {
|
||||
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 findWorkspaceIdentityProviderByIdentityProviderId(
|
||||
identityProviderId: string,
|
||||
) {
|
||||
return await this.workspaceSSOIdentityProviderRepository.findOne({
|
||||
where: { id: identityProviderId },
|
||||
relations: ['workspace'],
|
||||
});
|
||||
}
|
||||
|
||||
private async generateLoginToken(
|
||||
user: { email: string } & Record<string, string>,
|
||||
identityProvider: WorkspaceSSOIdentityProvider,
|
||||
) {
|
||||
if (!identityProvider) {
|
||||
throw new AuthException(
|
||||
'Identity provider not found',
|
||||
@ -159,28 +147,24 @@ export class SSOAuthController {
|
||||
);
|
||||
}
|
||||
|
||||
await this.authService.signInUp({
|
||||
...user,
|
||||
...(this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')
|
||||
? {
|
||||
targetWorkspaceSubdomain: identityProvider.workspace.subdomain,
|
||||
}
|
||||
: {}),
|
||||
fromSSO: true,
|
||||
const existingUser = await this.userRepository.findOne({
|
||||
where: {
|
||||
email: user.email,
|
||||
},
|
||||
});
|
||||
|
||||
const isUserExistInWorkspace =
|
||||
await this.userWorkspaceService.checkUserWorkspaceExistsByEmail(
|
||||
user.email,
|
||||
identityProvider.workspaceId,
|
||||
);
|
||||
const { userData } = this.authService.formatUserDataPayload(
|
||||
user,
|
||||
existingUser,
|
||||
);
|
||||
|
||||
if (!isUserExistInWorkspace) {
|
||||
throw new AuthException(
|
||||
'User not found in workspace',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
await this.authService.signInUp({
|
||||
userData,
|
||||
workspace: identityProvider.workspace,
|
||||
authParams: {
|
||||
provider: 'sso',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
identityProvider,
|
||||
|
||||
@ -14,6 +14,11 @@ export class SignUpInput {
|
||||
@IsString()
|
||||
password: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
workspaceId?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
|
||||
@ -39,10 +39,10 @@ export class GoogleOauthGuard extends AuthGuard('google') {
|
||||
}
|
||||
|
||||
if (
|
||||
request.query.workspaceSubdomain &&
|
||||
typeof request.query.workspaceSubdomain === 'string'
|
||||
request.query.workspaceId &&
|
||||
typeof request.query.workspaceId === 'string'
|
||||
) {
|
||||
request.params.workspaceSubdomain = request.query.workspaceSubdomain;
|
||||
request.params.workspaceId = request.query.workspaceId;
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@ -3,6 +3,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import bcrypt from 'bcrypt';
|
||||
import { expect, jest } from '@jest/globals';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { AppToken } from 'src/engine/core-modules/app-token/app-token.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 { 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 { SocialSsoService } from 'src/engine/core-modules/auth/services/social-sso.service';
|
||||
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@ -31,6 +33,7 @@ const userWorkspaceAddUserToWorkspaceMock = jest.fn();
|
||||
|
||||
describe('AuthService', () => {
|
||||
let service: AuthService;
|
||||
let appTokenRepository: Repository<AppToken>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@ -48,7 +51,14 @@ describe('AuthService', () => {
|
||||
},
|
||||
{
|
||||
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,
|
||||
@ -94,10 +104,18 @@ describe('AuthService', () => {
|
||||
validateInvitation: workspaceInvitationValidateInvitationMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: SocialSsoService,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<AuthService>(AuthService);
|
||||
|
||||
appTokenRepository = module.get<Repository<AppToken>>(
|
||||
getRepositoryToken(AppToken, 'core'),
|
||||
);
|
||||
});
|
||||
|
||||
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 { WorkspaceAuthProvider } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
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()
|
||||
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
||||
@ -57,6 +66,8 @@ export class AuthService {
|
||||
private readonly refreshTokenService: RefreshTokenService,
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
private readonly workspaceInvitationService: WorkspaceInvitationService,
|
||||
private readonly socialSsoService: SocialSsoService,
|
||||
private readonly userService: UserService,
|
||||
private readonly signInUpService: SignInUpService,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
@ -149,41 +160,58 @@ export class AuthService {
|
||||
return user;
|
||||
}
|
||||
|
||||
async signInUp({
|
||||
email,
|
||||
password,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
targetWorkspaceSubdomain,
|
||||
firstName,
|
||||
lastName,
|
||||
picture,
|
||||
fromSSO,
|
||||
authProvider,
|
||||
}: {
|
||||
email: string;
|
||||
password?: string;
|
||||
firstName?: string | null;
|
||||
lastName?: string | null;
|
||||
workspaceInviteHash?: string;
|
||||
workspacePersonalInviteToken?: string;
|
||||
picture?: string | null;
|
||||
fromSSO: boolean;
|
||||
targetWorkspaceSubdomain?: string;
|
||||
authProvider?: WorkspaceAuthProvider;
|
||||
billingCheckoutSessionState?: string;
|
||||
}) {
|
||||
async signInUp(
|
||||
params: SignInUpBaseParams &
|
||||
ExistingUserOrNewUser &
|
||||
AuthProviderWithPasswordType,
|
||||
) {
|
||||
if (
|
||||
params.authParams.provider === 'password' &&
|
||||
params.userData.type === 'newUser'
|
||||
) {
|
||||
params.userData.newUserPayload.passwordHash =
|
||||
await this.signInUpService.generateHash(params.authParams.password);
|
||||
}
|
||||
|
||||
if (
|
||||
params.authParams.provider === 'password' &&
|
||||
params.userData.type === 'existingUser'
|
||||
) {
|
||||
await this.signInUpService.validatePassword({
|
||||
password: params.authParams.password,
|
||||
passwordHash: params.userData.existingUser.passwordHash,
|
||||
});
|
||||
}
|
||||
|
||||
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({
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
targetWorkspaceSubdomain,
|
||||
picture,
|
||||
fromSSO,
|
||||
authProvider,
|
||||
...params,
|
||||
userData: {
|
||||
type: 'existingUser',
|
||||
existingUser: params.userData.existingUser,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -414,11 +442,15 @@ export class AuthService {
|
||||
return workspace;
|
||||
}
|
||||
|
||||
computeRedirectURI(
|
||||
loginToken: string,
|
||||
subdomain?: string,
|
||||
billingCheckoutSessionState?: string,
|
||||
) {
|
||||
computeRedirectURI({
|
||||
loginToken,
|
||||
subdomain,
|
||||
billingCheckoutSessionState,
|
||||
}: {
|
||||
loginToken: string;
|
||||
subdomain?: string;
|
||||
billingCheckoutSessionState?: string;
|
||||
}) {
|
||||
const url = this.domainManagerService.buildWorkspaceURL({
|
||||
subdomain,
|
||||
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 { HttpService } from '@nestjs/axios';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { expect, jest } from '@jest/globals';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
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 { EnvironmentService } from 'src/engine/core-modules/environment/environment.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 { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
|
||||
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 { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.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 {
|
||||
Workspace,
|
||||
WorkspaceActivationStatus,
|
||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
|
||||
jest.mock('bcrypt');
|
||||
|
||||
const UserFindOneMock = jest.fn();
|
||||
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();
|
||||
jest.mock('src/utils/image', () => {
|
||||
return {
|
||||
getImageBufferFromUrl: () => Promise.resolve(Buffer.from('')),
|
||||
};
|
||||
});
|
||||
|
||||
describe('SignInUpService', () => {
|
||||
let service: SignInUpService;
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
let UserRepository: Repository<User>;
|
||||
let WorkspaceRepository: Repository<Workspace>;
|
||||
let fileUploadService: FileUploadService;
|
||||
let workspaceInvitationService: WorkspaceInvitationService;
|
||||
let userWorkspaceService: UserWorkspaceService;
|
||||
let onboardingService: OnboardingService;
|
||||
let httpService: HttpService;
|
||||
let environmentService: EnvironmentService;
|
||||
let domainManagerService: DomainManagerService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
SignInUpService,
|
||||
{
|
||||
provide: FileUploadService,
|
||||
useValue: {},
|
||||
provide: getRepositoryToken(User, 'core'),
|
||||
useValue: {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(Workspace, 'core'),
|
||||
useValue: {
|
||||
count: WorkspaceCountMock,
|
||||
create: WorkspaceCreateMock,
|
||||
save: WorkspaceSaveMock,
|
||||
findOne: WorkspaceFindOneMock,
|
||||
save: jest.fn(),
|
||||
create: jest.fn(),
|
||||
get: jest.fn(),
|
||||
count: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(User, 'core'),
|
||||
provide: FileUploadService,
|
||||
useValue: {
|
||||
findOne: UserFindOneMock,
|
||||
create: UserCreateMock,
|
||||
save: UserSaveMock,
|
||||
uploadImage: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(AppToken, 'core'),
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceInvitationService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceService,
|
||||
useValue: {},
|
||||
useValue: {
|
||||
validateInvitation: jest.fn(),
|
||||
invalidateWorkspaceInvitation: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserWorkspaceService,
|
||||
useValue: {
|
||||
addUserToWorkspace: userWorkspaceServiceAddUserToWorkspaceMock,
|
||||
addUserToWorkspace: jest.fn(),
|
||||
create: jest.fn(),
|
||||
},
|
||||
},
|
||||
@ -93,7 +86,6 @@ describe('SignInUpService', () => {
|
||||
useValue: {
|
||||
setOnboardingConnectAccountPending: jest.fn(),
|
||||
setOnboardingInviteTeamPending: jest.fn(),
|
||||
setOnboardingCreateProfilePending: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -103,410 +95,150 @@ describe('SignInUpService', () => {
|
||||
{
|
||||
provide: EnvironmentService,
|
||||
useValue: {
|
||||
get: EnvironmentServiceGetMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceInvitationService,
|
||||
useValue: {
|
||||
validateInvitation: workspaceInvitationValidateInvitationMock,
|
||||
invalidateWorkspaceInvitation:
|
||||
workspaceInvitationInvalidateWorkspaceInvitationMock,
|
||||
findInvitationByWorkspaceSubdomainAndUserEmail:
|
||||
workspaceInvitationFindInvitationByWorkspaceSubdomainAndUserEmailMock,
|
||||
get: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: DomainManagerService,
|
||||
useValue: {
|
||||
generateSubdomain: jest.fn().mockReturnValue('testSubDomain'),
|
||||
getWorkspaceBySubdomainOrDefaultWorkspace: jest
|
||||
.fn()
|
||||
.mockReturnValue({}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
hasUserAccessToWorkspaceOrThrow: jest.fn(),
|
||||
generateSubdomain: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<SignInUpService>(SignInUpService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it('signInUp - sso - new user', async () => {
|
||||
const email = 'test@test.com';
|
||||
|
||||
UserFindOneMock.mockReturnValueOnce(false);
|
||||
workspaceInvitationFindInvitationByWorkspaceSubdomainAndUserEmailMock.mockReturnValueOnce(
|
||||
undefined,
|
||||
UserRepository = module.get(getRepositoryToken(User, 'core'));
|
||||
WorkspaceRepository = module.get(getRepositoryToken(Workspace, 'core'));
|
||||
fileUploadService = module.get<FileUploadService>(FileUploadService);
|
||||
workspaceInvitationService = module.get<WorkspaceInvitationService>(
|
||||
WorkspaceInvitationService,
|
||||
);
|
||||
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
|
||||
.spyOn(service, 'signUpOnNewWorkspace')
|
||||
.mockResolvedValueOnce({ user: {}, workspace: {} } as {
|
||||
user: User;
|
||||
workspace: Workspace;
|
||||
it('should handle signInUp with valid personal invitation', async () => {
|
||||
const params: SignInUpBaseParams &
|
||||
ExistingUserOrPartialUserWithPicture &
|
||||
AuthProviderWithPasswordType = {
|
||||
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({
|
||||
email: 'test@test.com',
|
||||
fromSSO: true,
|
||||
targetWorkspaceSubdomain: 'testSubDomain',
|
||||
jest
|
||||
.spyOn(workspaceInvitationService, 'invalidateWorkspaceInvitation')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
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(
|
||||
workspaceInvitationInvalidateWorkspaceInvitationMock,
|
||||
).toHaveBeenCalledWith(workspaceId, email);
|
||||
workspaceInvitationService.invalidateWorkspaceInvitation,
|
||||
).toHaveBeenCalledWith(
|
||||
(params.workspace as Workspace).id,
|
||||
'test@example.com',
|
||||
);
|
||||
});
|
||||
it('signInUp - sso - existing user - existing invitation', async () => {
|
||||
const email = 'existinguser@test.com';
|
||||
const workspaceId = 'workspace-id';
|
||||
const existingUser = {
|
||||
id: 'user-id',
|
||||
email,
|
||||
passwordHash: undefined,
|
||||
|
||||
it('should handle signInUp on existing workspace without invitation', async () => {
|
||||
const params: SignInUpBaseParams &
|
||||
ExistingUserOrPartialUserWithPicture &
|
||||
AuthProviderWithPasswordType = {
|
||||
workspace: {
|
||||
id: 'workspaceId',
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
} as Workspace,
|
||||
authParams: { provider: 'password', password: 'validPassword' },
|
||||
userData: {
|
||||
type: 'existingUser',
|
||||
existingUser: { email: 'test@example.com' } as User,
|
||||
},
|
||||
};
|
||||
|
||||
const workspace = {
|
||||
id: workspaceId,
|
||||
jest
|
||||
.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,
|
||||
};
|
||||
|
||||
UserFindOneMock.mockReturnValueOnce(existingUser);
|
||||
workspaceInvitationFindInvitationByWorkspaceSubdomainAndUserEmailMock.mockReturnValueOnce(
|
||||
{
|
||||
value: 'personal-token-value',
|
||||
},
|
||||
);
|
||||
workspaceInvitationValidateInvitationMock.mockReturnValueOnce({
|
||||
isValid: true,
|
||||
workspace,
|
||||
} as Workspace);
|
||||
jest.spyOn(fileUploadService, 'uploadImage').mockResolvedValue({
|
||||
id: '',
|
||||
mimeType: '',
|
||||
paths: ['path/to/image'],
|
||||
});
|
||||
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(
|
||||
true,
|
||||
);
|
||||
const result = await service.signInUp(params);
|
||||
|
||||
userWorkspaceServiceAddUserToWorkspaceMock.mockReturnValueOnce({});
|
||||
|
||||
const result = await service.signInUp({
|
||||
email,
|
||||
fromSSO: true,
|
||||
targetWorkspaceSubdomain: 'testSubDomain',
|
||||
});
|
||||
|
||||
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);
|
||||
expect(result.workspace).toBeDefined();
|
||||
expect(result.user).toBeDefined();
|
||||
expect(WorkspaceRepository.create).toHaveBeenCalled();
|
||||
expect(WorkspaceRepository.save).toHaveBeenCalled();
|
||||
expect(UserRepository.create).toHaveBeenCalled();
|
||||
expect(UserRepository.save).toHaveBeenCalled();
|
||||
expect(fileUploadService.uploadImage).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@ -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 { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.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 { userValidator } from 'src/engine/core-modules/user/user.validate';
|
||||
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 {
|
||||
Workspace,
|
||||
WorkspaceActivationStatus,
|
||||
@ -35,21 +32,15 @@ import {
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
|
||||
import { getImageBufferFromUrl } from 'src/utils/image';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
import { isWorkEmail } from 'src/utils/is-work-email';
|
||||
|
||||
export type SignInUpServiceInput = {
|
||||
email: string;
|
||||
password?: string;
|
||||
firstName?: string | null;
|
||||
lastName?: string | null;
|
||||
workspaceInviteHash?: string;
|
||||
workspacePersonalInviteToken?: string;
|
||||
picture?: string | null;
|
||||
fromSSO: boolean;
|
||||
targetWorkspaceSubdomain?: string;
|
||||
authProvider?: WorkspaceAuthProvider;
|
||||
};
|
||||
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import {
|
||||
AuthProviderWithPasswordType,
|
||||
ExistingUserOrPartialUserWithPicture,
|
||||
PartialUserWithPicture,
|
||||
SignInUpBaseParams,
|
||||
SignInUpNewUserPayload,
|
||||
} from 'src/engine/core-modules/auth/types/signInUp.type';
|
||||
|
||||
@Injectable()
|
||||
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
||||
@ -66,23 +57,112 @@ export class SignInUpService {
|
||||
private readonly httpService: HttpService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly userService: UserService,
|
||||
) {}
|
||||
|
||||
async signInUp({
|
||||
email,
|
||||
workspacePersonalInviteToken,
|
||||
workspaceInviteHash,
|
||||
async computeParamsForNewUser(
|
||||
newUserParams: SignInUpNewUserPayload,
|
||||
authParams: AuthProviderWithPasswordType['authParams'],
|
||||
) {
|
||||
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,
|
||||
firstName,
|
||||
lastName,
|
||||
picture,
|
||||
fromSSO,
|
||||
targetWorkspaceSubdomain,
|
||||
authProvider,
|
||||
}: SignInUpServiceInput) {
|
||||
if (!firstName) firstName = '';
|
||||
if (!lastName) lastName = '';
|
||||
passwordHash,
|
||||
}: {
|
||||
password: string;
|
||||
passwordHash: string;
|
||||
}) {
|
||||
const isValid = await compareHash(
|
||||
await this.generateHash(password),
|
||||
passwordHash,
|
||||
);
|
||||
|
||||
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) {
|
||||
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 =
|
||||
workspacePersonalInviteToken || workspaceInviteHash || maybeInvitation
|
||||
? await this.workspaceInvitationService.validateInvitation({
|
||||
workspacePersonalInviteToken:
|
||||
workspacePersonalInviteToken ?? maybeInvitation?.value,
|
||||
workspaceInviteHash,
|
||||
email,
|
||||
})
|
||||
: null;
|
||||
|
||||
if (
|
||||
invitationValidation?.isValid === true &&
|
||||
invitationValidation.workspace
|
||||
) {
|
||||
const updatedUser = await this.signInUpOnExistingWorkspace({
|
||||
await this.workspaceInvitationService.validateInvitation({
|
||||
workspacePersonalInviteToken: params.invitation.value,
|
||||
email,
|
||||
passwordHash,
|
||||
workspace: invitationValidation.workspace,
|
||||
firstName,
|
||||
lastName,
|
||||
picture,
|
||||
existingUser,
|
||||
authProvider,
|
||||
});
|
||||
|
||||
await this.workspaceInvitationService.invalidateWorkspaceInvitation(
|
||||
invitationValidation.workspace.id,
|
||||
email,
|
||||
);
|
||||
|
||||
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',
|
||||
if (!invitationValidation?.isValid) {
|
||||
throw new AuthException(
|
||||
'Invitation not found',
|
||||
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(
|
||||
workspace,
|
||||
params.workspace,
|
||||
new AuthException(
|
||||
'Workspace is not ready to welcome new members',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
),
|
||||
);
|
||||
|
||||
if (authProvider) {
|
||||
workspaceValidator.isAuthEnabledOrThrow(authProvider, workspace);
|
||||
}
|
||||
|
||||
if (isNewUser) {
|
||||
const imagePath = await this.uploadPicture(picture, workspace.id);
|
||||
|
||||
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 currentUser =
|
||||
params.userData.type === 'newUserWithPicture'
|
||||
? await this.persistNewUser(
|
||||
params.userData.newUserWithPicture,
|
||||
params.workspace,
|
||||
)
|
||||
: params.userData.existingUser;
|
||||
|
||||
const updatedUser = await this.userWorkspaceService.addUserToWorkspace(
|
||||
user,
|
||||
workspace,
|
||||
currentUser,
|
||||
params.workspace,
|
||||
);
|
||||
|
||||
await this.activateOnboardingForUser(user, workspace, {
|
||||
firstName,
|
||||
lastName,
|
||||
});
|
||||
const user = Object.assign(currentUser, updatedUser);
|
||||
|
||||
return Object.assign(user, updatedUser);
|
||||
await this.activateOnboardingForUser(user, params.workspace);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
private async activateOnboardingForUser(
|
||||
user: User,
|
||||
workspace: Workspace,
|
||||
{ firstName, lastName }: { firstName: string; lastName: string },
|
||||
) {
|
||||
private async activateOnboardingForUser(user: User, workspace: Workspace) {
|
||||
await this.onboardingService.setOnboardingConnectAccountPending({
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
value: true,
|
||||
});
|
||||
|
||||
if (firstName === '' && lastName === '') {
|
||||
if (user.firstName === '' && user.lastName === '') {
|
||||
await this.onboardingService.setOnboardingCreateProfilePending({
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
@ -357,27 +263,19 @@ export class SignInUpService {
|
||||
}
|
||||
}
|
||||
|
||||
async signUpOnNewWorkspace({
|
||||
email,
|
||||
passwordHash,
|
||||
firstName,
|
||||
lastName,
|
||||
picture,
|
||||
}: {
|
||||
email: string;
|
||||
passwordHash: string | undefined;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
picture: SignInUpServiceInput['picture'];
|
||||
}) {
|
||||
const user: Partial<User> = {
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
async signUpOnNewWorkspace(partialUserWithPicture: PartialUserWithPicture) {
|
||||
const user: PartialUserWithPicture = {
|
||||
...partialUserWithPicture,
|
||||
canImpersonate: false,
|
||||
passwordHash,
|
||||
};
|
||||
|
||||
if (!user.email) {
|
||||
throw new AuthException(
|
||||
'Email is required',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')) {
|
||||
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 () => {
|
||||
try {
|
||||
return (
|
||||
@ -406,7 +304,7 @@ export class SignInUpService {
|
||||
};
|
||||
|
||||
const logo =
|
||||
isWorkEmail(email) && (await isLogoUrlValid()) ? logoUrl : undefined;
|
||||
isWorkEmail(user.email) && (await isLogoUrlValid()) ? logoUrl : undefined;
|
||||
|
||||
const workspaceToCreate = this.workspaceRepository.create({
|
||||
subdomain: await this.domainManagerService.generateSubdomain(),
|
||||
@ -419,7 +317,10 @@ export class SignInUpService {
|
||||
|
||||
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);
|
||||
|
||||
@ -427,10 +328,7 @@ export class SignInUpService {
|
||||
|
||||
await this.userWorkspaceService.create(newUser.id, workspace.id);
|
||||
|
||||
await this.activateOnboardingForUser(newUser, workspace, {
|
||||
firstName,
|
||||
lastName,
|
||||
});
|
||||
await this.activateOnboardingForUser(newUser, workspace);
|
||||
|
||||
await this.onboardingService.setOnboardingInviteTeamPending({
|
||||
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;
|
||||
workspaceInviteHash?: string;
|
||||
workspacePersonalInviteToken?: string;
|
||||
targetWorkspaceSubdomain?: string;
|
||||
workspaceId?: string;
|
||||
billingCheckoutSessionState?: string;
|
||||
};
|
||||
};
|
||||
@ -39,7 +39,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
|
||||
...options,
|
||||
state: JSON.stringify({
|
||||
workspaceInviteHash: req.params.workspaceInviteHash,
|
||||
workspaceSubdomain: req.params.workspaceSubdomain,
|
||||
workspaceId: req.params.workspaceId,
|
||||
...(req.params.billingCheckoutSessionState
|
||||
? {
|
||||
billingCheckoutSessionState:
|
||||
@ -78,7 +78,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
|
||||
picture: photos?.[0]?.value,
|
||||
workspaceInviteHash: state.workspaceInviteHash,
|
||||
workspacePersonalInviteToken: state.workspacePersonalInviteToken,
|
||||
targetWorkspaceSubdomain: state.workspaceSubdomain,
|
||||
workspaceId: state.workspaceId,
|
||||
billingCheckoutSessionState: state.billingCheckoutSessionState,
|
||||
};
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ export type MicrosoftRequest = Omit<
|
||||
picture: string | null;
|
||||
workspaceInviteHash?: string;
|
||||
workspacePersonalInviteToken?: string;
|
||||
targetWorkspaceSubdomain?: string;
|
||||
workspaceId?: string;
|
||||
billingCheckoutSessionState?: string;
|
||||
};
|
||||
};
|
||||
@ -43,7 +43,7 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy, 'microsoft') {
|
||||
...options,
|
||||
state: JSON.stringify({
|
||||
workspaceInviteHash: req.params.workspaceInviteHash,
|
||||
workspaceSubdomain: req.params.workspaceSubdomain,
|
||||
workspaceId: req.params.workspaceId,
|
||||
...(req.params.billingCheckoutSessionState
|
||||
? {
|
||||
billingCheckoutSessionState:
|
||||
@ -92,7 +92,7 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy, 'microsoft') {
|
||||
picture: photos?.[0]?.value,
|
||||
workspaceInviteHash: state.workspaceInviteHash,
|
||||
workspacePersonalInviteToken: state.workspacePersonalInviteToken,
|
||||
targetWorkspaceSubdomain: state.workspaceSubdomain,
|
||||
workspaceId: state.workspaceId,
|
||||
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');
|
||||
}
|
||||
|
||||
computeRedirectErrorUrl({
|
||||
errorMessage,
|
||||
subdomain,
|
||||
}: {
|
||||
errorMessage: string;
|
||||
subdomain?: string;
|
||||
}) {
|
||||
const url = this.buildWorkspaceURL({
|
||||
computeRedirectErrorUrl(
|
||||
errorMessage: string,
|
||||
{
|
||||
subdomain,
|
||||
}: {
|
||||
subdomain?: string;
|
||||
},
|
||||
) {
|
||||
const url = this.buildWorkspaceURL({
|
||||
subdomain: subdomain ?? this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
pathname: '/verify',
|
||||
searchParams: { errorMessage },
|
||||
});
|
||||
@ -193,10 +194,12 @@ export class DomainManagerService {
|
||||
|
||||
if (!isDefined(subdomain)) return;
|
||||
|
||||
return await this.workspaceRepository.findOne({
|
||||
where: { subdomain },
|
||||
relations: ['workspaceSSOIdentityProviders'],
|
||||
});
|
||||
return (
|
||||
(await this.workspaceRepository.findOne({
|
||||
where: { subdomain },
|
||||
relations: ['workspaceSSOIdentityProviders'],
|
||||
})) ?? undefined
|
||||
);
|
||||
} catch (e) {
|
||||
throw new WorkspaceException(
|
||||
'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 === 'microsoft' && workspace.isMicrosoftAuthEnabled) return true;
|
||||
if (provider === 'password' && workspace.isPasswordAuthEnabled) return true;
|
||||
if (provider === 'sso') return true;
|
||||
|
||||
throw exceptionToThrowCustom;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user