feat(*): allow to select auth providers + add multiworkspace with subdomain management (#8656)
## Summary Add support for multi-workspace feature and adjust configurations and states accordingly. - Introduced new state isMultiWorkspaceEnabledState. - Updated ClientConfigProviderEffect component to handle multi-workspace. - Modified GraphQL schema and queries to include multi-workspace related configurations. - Adjusted server environment variables and their respective documentation to support multi-workspace toggle. - Updated server-side logic to handle new multi-workspace configurations and conditions.
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-restricted-imports */
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
@ -11,7 +11,6 @@ import { GoogleAuthController } from 'src/engine/core-modules/auth/controllers/g
|
||||
import { MicrosoftAPIsAuthController } from 'src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller';
|
||||
import { MicrosoftAuthController } from 'src/engine/core-modules/auth/controllers/microsoft-auth.controller';
|
||||
import { SSOAuthController } from 'src/engine/core-modules/auth/controllers/sso-auth.controller';
|
||||
import { VerifyAuthController } from 'src/engine/core-modules/auth/controllers/verify-auth.controller';
|
||||
import { ApiKeyService } from 'src/engine/core-modules/auth/services/api-key.service';
|
||||
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
|
||||
import { MicrosoftAPIsService } from 'src/engine/core-modules/auth/services/microsoft-apis.service';
|
||||
@ -24,7 +23,6 @@ import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
|
||||
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
|
||||
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
||||
@ -36,13 +34,15 @@ import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/worksp
|
||||
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { UserModule } from 'src/engine/core-modules/user/user.module';
|
||||
import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
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 { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
||||
import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module';
|
||||
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
|
||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
||||
|
||||
import { AuthResolver } from './auth.resolver';
|
||||
|
||||
@ -54,7 +54,9 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
||||
JwtModule,
|
||||
FileUploadModule,
|
||||
DataSourceModule,
|
||||
forwardRef(() => UserModule),
|
||||
DomainManagerModule,
|
||||
TokenModule,
|
||||
UserModule,
|
||||
WorkspaceManagerModule,
|
||||
TypeORMModule,
|
||||
TypeOrmModule.forFeature(
|
||||
@ -69,22 +71,20 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
||||
'core',
|
||||
),
|
||||
HttpModule,
|
||||
TokenModule,
|
||||
UserWorkspaceModule,
|
||||
WorkspaceModule,
|
||||
OnboardingModule,
|
||||
WorkspaceDataSourceModule,
|
||||
WorkspaceInvitationModule,
|
||||
ConnectedAccountModule,
|
||||
WorkspaceSSOModule,
|
||||
FeatureFlagModule,
|
||||
WorkspaceInvitationModule,
|
||||
],
|
||||
controllers: [
|
||||
GoogleAuthController,
|
||||
MicrosoftAuthController,
|
||||
GoogleAPIsAuthController,
|
||||
MicrosoftAPIsAuthController,
|
||||
VerifyAuthController,
|
||||
SSOAuthController,
|
||||
],
|
||||
providers: [
|
||||
|
||||
@ -7,6 +7,7 @@ import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/use
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
|
||||
import { AuthResolver } from './auth.resolver';
|
||||
|
||||
@ -43,6 +44,14 @@ describe('AuthResolver', () => {
|
||||
provide: UserService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: DomainManagerService,
|
||||
useValue: {
|
||||
buildWorkspaceURL: jest
|
||||
.fn()
|
||||
.mockResolvedValue(new URL('http://localhost:3001')),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserWorkspaceService,
|
||||
useValue: {},
|
||||
|
||||
@ -9,12 +9,6 @@ import { EmailPasswordResetLink } from 'src/engine/core-modules/auth/dto/email-p
|
||||
import { EmailPasswordResetLinkInput } from 'src/engine/core-modules/auth/dto/email-password-reset-link.input';
|
||||
import { ExchangeAuthCode } from 'src/engine/core-modules/auth/dto/exchange-auth-code.entity';
|
||||
import { ExchangeAuthCodeInput } from 'src/engine/core-modules/auth/dto/exchange-auth-code.input';
|
||||
import { GenerateJwtInput } from 'src/engine/core-modules/auth/dto/generate-jwt.input';
|
||||
import {
|
||||
GenerateJWTOutput,
|
||||
GenerateJWTOutputWithAuthTokens,
|
||||
GenerateJWTOutputWithSSOAUTH,
|
||||
} from 'src/engine/core-modules/auth/dto/generateJWT.output';
|
||||
import { InvalidatePassword } from 'src/engine/core-modules/auth/dto/invalidate-password.entity';
|
||||
import { TransientToken } from 'src/engine/core-modules/auth/dto/transient-token.entity';
|
||||
import { UpdatePasswordViaResetTokenInput } from 'src/engine/core-modules/auth/dto/update-password-via-reset-token.input';
|
||||
@ -36,12 +30,22 @@ import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { SwitchWorkspaceInput } from 'src/engine/core-modules/auth/dto/switch-workspace.input';
|
||||
import { PublicWorkspaceDataOutput } from 'src/engine/core-modules/workspace/dtos/public-workspace-data.output';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
|
||||
import { AvailableWorkspaceOutput } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
|
||||
import { ChallengeInput } from './dto/challenge.input';
|
||||
import { LoginToken } from './dto/login-token.entity';
|
||||
import { SignUpInput } from './dto/sign-up.input';
|
||||
import { ApiKeyToken, AuthTokens } from './dto/token.entity';
|
||||
import { UserExists } from './dto/user-exists.entity';
|
||||
import { UserExistsOutput } from './dto/user-exists.entity';
|
||||
import { CheckUserExistsInput } from './dto/user-exists.input';
|
||||
import { Verify } from './dto/verify.entity';
|
||||
import { VerifyInput } from './dto/verify.input';
|
||||
@ -62,18 +66,15 @@ export class AuthResolver {
|
||||
private switchWorkspaceService: SwitchWorkspaceService,
|
||||
private transientTokenService: TransientTokenService,
|
||||
private oauthService: OAuthService,
|
||||
private domainManagerService: DomainManagerService,
|
||||
) {}
|
||||
|
||||
@UseGuards(CaptchaGuard)
|
||||
@Query(() => UserExists)
|
||||
@Query(() => UserExistsOutput)
|
||||
async checkUserExists(
|
||||
@Args() checkUserExistsInput: CheckUserExistsInput,
|
||||
): Promise<UserExists> {
|
||||
const { exists } = await this.authService.checkUserExists(
|
||||
checkUserExistsInput.email,
|
||||
);
|
||||
|
||||
return { exists };
|
||||
): Promise<typeof UserExistsOutput> {
|
||||
return await this.authService.checkUserExists(checkUserExistsInput.email);
|
||||
}
|
||||
|
||||
@Query(() => WorkspaceInviteHashValid)
|
||||
@ -96,8 +97,20 @@ export class AuthResolver {
|
||||
|
||||
@UseGuards(CaptchaGuard)
|
||||
@Mutation(() => LoginToken)
|
||||
async challenge(@Args() challengeInput: ChallengeInput): Promise<LoginToken> {
|
||||
const user = await this.authService.challenge(challengeInput);
|
||||
async challenge(
|
||||
@Args() challengeInput: ChallengeInput,
|
||||
@OriginHeader() origin: string,
|
||||
): Promise<LoginToken> {
|
||||
const workspace =
|
||||
await this.domainManagerService.getWorkspaceByOrigin(origin);
|
||||
|
||||
if (!workspace) {
|
||||
throw 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,
|
||||
);
|
||||
@ -107,10 +120,22 @@ export class AuthResolver {
|
||||
|
||||
@UseGuards(CaptchaGuard)
|
||||
@Mutation(() => LoginToken)
|
||||
async signUp(@Args() signUpInput: SignUpInput): Promise<LoginToken> {
|
||||
async signUp(
|
||||
@Args() signUpInput: SignUpInput,
|
||||
@OriginHeader() origin: string,
|
||||
): Promise<LoginToken> {
|
||||
const user = await this.authService.signInUp({
|
||||
...signUpInput,
|
||||
targetWorkspaceSubdomain:
|
||||
this.domainManagerService.getWorkspaceSubdomainByOrigin(origin),
|
||||
fromSSO: false,
|
||||
isAuthEnabled: workspaceValidator.isAuthEnabled(
|
||||
'password',
|
||||
new AuthException(
|
||||
'Password auth is not enabled for this workspace',
|
||||
AuthExceptionCode.OAUTH_ACCESS_DENIED,
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
@ -124,11 +149,9 @@ export class AuthResolver {
|
||||
async exchangeAuthorizationCode(
|
||||
@Args() exchangeAuthCodeInput: ExchangeAuthCodeInput,
|
||||
) {
|
||||
const tokens = await this.oauthService.verifyAuthorizationCode(
|
||||
return await this.oauthService.verifyAuthorizationCode(
|
||||
exchangeAuthCodeInput,
|
||||
);
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
@Mutation(() => TransientToken)
|
||||
@ -156,14 +179,18 @@ export class AuthResolver {
|
||||
}
|
||||
|
||||
@Mutation(() => Verify)
|
||||
async verify(@Args() verifyInput: VerifyInput): Promise<Verify> {
|
||||
const email = await this.loginTokenService.verifyLoginToken(
|
||||
async verify(
|
||||
@Args() verifyInput: VerifyInput,
|
||||
@OriginHeader() origin: string,
|
||||
): Promise<Verify> {
|
||||
const workspace =
|
||||
await this.domainManagerService.getWorkspaceByOrigin(origin);
|
||||
|
||||
const { sub: email } = await this.loginTokenService.verifyLoginToken(
|
||||
verifyInput.loginToken,
|
||||
);
|
||||
|
||||
const result = await this.authService.verify(email);
|
||||
|
||||
return result;
|
||||
return await this.authService.verify(email, workspace?.id);
|
||||
}
|
||||
|
||||
@Mutation(() => AuthorizeApp)
|
||||
@ -172,50 +199,22 @@ export class AuthResolver {
|
||||
@Args() authorizeAppInput: AuthorizeAppInput,
|
||||
@AuthUser() user: User,
|
||||
): Promise<AuthorizeApp> {
|
||||
const authorizedApp = await this.authService.generateAuthorizationCode(
|
||||
return await this.authService.generateAuthorizationCode(
|
||||
authorizeAppInput,
|
||||
user,
|
||||
);
|
||||
|
||||
return authorizedApp;
|
||||
}
|
||||
|
||||
@Mutation(() => GenerateJWTOutput)
|
||||
@Mutation(() => PublicWorkspaceDataOutput)
|
||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
||||
async generateJWT(
|
||||
async switchWorkspace(
|
||||
@AuthUser() user: User,
|
||||
@Args() args: GenerateJwtInput,
|
||||
): Promise<GenerateJWTOutputWithAuthTokens | GenerateJWTOutputWithSSOAUTH> {
|
||||
const result = await this.switchWorkspaceService.switchWorkspace(
|
||||
@Args() args: SwitchWorkspaceInput,
|
||||
): Promise<PublicWorkspaceDataOutput> {
|
||||
return await this.switchWorkspaceService.switchWorkspace(
|
||||
user,
|
||||
args.workspaceId,
|
||||
);
|
||||
|
||||
if (result.useSSOAuth) {
|
||||
return {
|
||||
success: true,
|
||||
reason: 'WORKSPACE_USE_SSO_AUTH',
|
||||
availableSSOIDPs: result.availableSSOIdentityProviders.map(
|
||||
(identityProvider) => ({
|
||||
...identityProvider,
|
||||
workspace: {
|
||||
id: result.workspace.id,
|
||||
displayName: result.workspace.displayName,
|
||||
},
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
reason: 'WORKSPACE_AVAILABLE_FOR_SWITCH',
|
||||
authTokens:
|
||||
await this.switchWorkspaceService.generateSwitchWorkspaceToken(
|
||||
user,
|
||||
result.workspace,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@Mutation(() => AuthTokens)
|
||||
@ -278,4 +277,11 @@ export class AuthResolver {
|
||||
args.passwordResetToken,
|
||||
);
|
||||
}
|
||||
|
||||
@Query(() => [AvailableWorkspaceOutput])
|
||||
async findAvailableWorkspacesByEmail(
|
||||
@Args('email') email: string,
|
||||
): Promise<AvailableWorkspaceOutput[]> {
|
||||
return this.authService.findAvailableWorkspacesByEmail(email);
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,8 +6,10 @@ import {
|
||||
UseFilters,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Response } from 'express';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
AuthException,
|
||||
@ -21,6 +23,8 @@ import { TransientTokenService } from 'src/engine/core-modules/auth/token/servic
|
||||
import { GoogleAPIsRequest } from 'src/engine/core-modules/auth/types/google-api-request.type';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
|
||||
@Controller('auth/google-apis')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
@ -30,6 +34,9 @@ export class GoogleAPIsAuthController {
|
||||
private readonly transientTokenService: TransientTokenService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly onboardingService: OnboardingService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@ -96,10 +103,24 @@ export class GoogleAPIsAuthController {
|
||||
});
|
||||
}
|
||||
|
||||
const workspace = await this.workspaceRepository.findOneBy({
|
||||
id: workspaceId,
|
||||
});
|
||||
|
||||
if (!workspace) {
|
||||
throw new AuthException(
|
||||
'Workspace not found',
|
||||
AuthExceptionCode.WORKSPACE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return res.redirect(
|
||||
`${this.environmentService.get('FRONT_BASE_URL')}${
|
||||
redirectLocation || '/settings/accounts'
|
||||
}`,
|
||||
this.domainManagerService
|
||||
.buildWorkspaceURL({
|
||||
subdomain: workspace.subdomain,
|
||||
pathname: redirectLocation || '/settings/accounts',
|
||||
})
|
||||
.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,9 @@ import {
|
||||
UseFilters,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { Response } from 'express';
|
||||
|
||||
import { AuthOAuthExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-oauth-exception.filter';
|
||||
@ -16,6 +18,14 @@ import { GoogleProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/
|
||||
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 {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@Controller('auth/google')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
@ -23,6 +33,10 @@ export class GoogleAuthController {
|
||||
constructor(
|
||||
private readonly loginTokenService: LoginTokenService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@ -36,29 +50,81 @@ export class GoogleAuthController {
|
||||
@UseGuards(GoogleProviderEnabledGuard, GoogleOauthGuard)
|
||||
@UseFilters(AuthOAuthExceptionFilter)
|
||||
async googleAuthRedirect(@Req() req: GoogleRequest, @Res() res: Response) {
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
picture,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
} = req.user;
|
||||
try {
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
picture,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
targetWorkspaceSubdomain,
|
||||
} = req.user;
|
||||
|
||||
const user = await this.authService.signInUp({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
picture,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
fromSSO: true,
|
||||
});
|
||||
const signInUpParams = {
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
picture,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
targetWorkspaceSubdomain,
|
||||
fromSSO: true,
|
||||
isAuthEnabled: workspaceValidator.isAuthEnabled(
|
||||
'google',
|
||||
new AuthException(
|
||||
'Google auth is not enabled for this workspace',
|
||||
AuthExceptionCode.OAUTH_ACCESS_DENIED,
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
);
|
||||
if (
|
||||
this.environmentService.get('IS_MULTIWORKSPACE_ENABLED') &&
|
||||
targetWorkspaceSubdomain ===
|
||||
this.environmentService.get('DEFAULT_SUBDOMAIN')
|
||||
) {
|
||||
const workspaceWithGoogleAuthActive =
|
||||
await this.workspaceRepository.findOne({
|
||||
where: {
|
||||
isGoogleAuthEnabled: true,
|
||||
workspaceUsers: {
|
||||
user: {
|
||||
email,
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: ['userWorkspaces', 'userWorkspaces.user'],
|
||||
});
|
||||
|
||||
return res.redirect(this.authService.computeRedirectURI(loginToken.token));
|
||||
if (workspaceWithGoogleAuthActive) {
|
||||
signInUpParams.targetWorkspaceSubdomain =
|
||||
workspaceWithGoogleAuthActive.subdomain;
|
||||
}
|
||||
}
|
||||
|
||||
const user = await this.authService.signInUp(signInUpParams);
|
||||
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
);
|
||||
|
||||
return res.redirect(
|
||||
await this.authService.computeRedirectURI(
|
||||
loginToken.token,
|
||||
user.defaultWorkspace.subdomain,
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof AuthException) {
|
||||
return res.redirect(
|
||||
this.domainManagerService.computeRedirectErrorUrl({
|
||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
errorMessage: err.message,
|
||||
}),
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,8 +6,10 @@ import {
|
||||
UseFilters,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Response } from 'express';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
AuthException,
|
||||
@ -21,6 +23,9 @@ import { TransientTokenService } from 'src/engine/core-modules/auth/token/servic
|
||||
import { MicrosoftAPIsRequest } from 'src/engine/core-modules/auth/types/microsoft-api-request.type';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
|
||||
@Controller('auth/microsoft-apis')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
@ -29,7 +34,11 @@ export class MicrosoftAPIsAuthController {
|
||||
private readonly microsoftAPIsService: MicrosoftAPIsService,
|
||||
private readonly transientTokenService: TransientTokenService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly workspaceService: WorkspaceService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly onboardingService: OnboardingService,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@ -96,10 +105,24 @@ export class MicrosoftAPIsAuthController {
|
||||
});
|
||||
}
|
||||
|
||||
const workspace = await this.workspaceRepository.findOneBy({
|
||||
id: workspaceId,
|
||||
});
|
||||
|
||||
if (!workspace) {
|
||||
throw new AuthException(
|
||||
'Workspace not found',
|
||||
AuthExceptionCode.WORKSPACE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return res.redirect(
|
||||
`${this.environmentService.get('FRONT_BASE_URL')}${
|
||||
redirectLocation || '/settings/accounts'
|
||||
}`,
|
||||
this.domainManagerService
|
||||
.buildWorkspaceURL({
|
||||
subdomain: workspace.subdomain,
|
||||
pathname: redirectLocation || '/settings/accounts',
|
||||
})
|
||||
.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,13 @@ import { MicrosoftProviderEnabledGuard } from 'src/engine/core-modules/auth/guar
|
||||
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 {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
|
||||
@Controller('auth/microsoft')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
@ -22,6 +29,8 @@ export class MicrosoftAuthController {
|
||||
constructor(
|
||||
private readonly loginTokenService: LoginTokenService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@ -37,29 +46,55 @@ export class MicrosoftAuthController {
|
||||
@Req() req: MicrosoftRequest,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
picture,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
} = req.user;
|
||||
try {
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
picture,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
targetWorkspaceSubdomain,
|
||||
} = req.user;
|
||||
|
||||
const user = await this.authService.signInUp({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
picture,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
fromSSO: true,
|
||||
});
|
||||
const user = await this.authService.signInUp({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
picture,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
targetWorkspaceSubdomain,
|
||||
fromSSO: true,
|
||||
isAuthEnabled: workspaceValidator.isAuthEnabled(
|
||||
'microsoft',
|
||||
new AuthException(
|
||||
'Microsoft auth is not enabled for this workspace',
|
||||
AuthExceptionCode.OAUTH_ACCESS_DENIED,
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
);
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
);
|
||||
|
||||
return res.redirect(this.authService.computeRedirectURI(loginToken.token));
|
||||
return res.redirect(
|
||||
await this.authService.computeRedirectURI(
|
||||
loginToken.token,
|
||||
user.defaultWorkspace.subdomain,
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof AuthException) {
|
||||
return res.redirect(
|
||||
this.domainManagerService.computeRedirectErrorUrl({
|
||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
errorMessage: err.message,
|
||||
}),
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,14 +25,14 @@ import { SAMLAuthGuard } from 'src/engine/core-modules/auth/guards/saml-auth.gua
|
||||
import { SSOProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/sso-provider-enabled.guard';
|
||||
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
||||
import {
|
||||
IdentityProviderType,
|
||||
WorkspaceSSOIdentityProvider,
|
||||
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.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 { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
@Controller('auth')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
@ -40,9 +40,9 @@ export class SSOAuthController {
|
||||
constructor(
|
||||
private readonly loginTokenService: LoginTokenService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly workspaceInvitationService: WorkspaceInvitationService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly ssoService: SSOService,
|
||||
@InjectRepository(WorkspaceSSOIdentityProvider, 'core')
|
||||
private readonly workspaceSSOIdentityProviderRepository: Repository<WorkspaceSSOIdentityProvider>,
|
||||
@ -50,7 +50,7 @@ export class SSOAuthController {
|
||||
|
||||
@Get('saml/metadata/:identityProviderId')
|
||||
@UseGuards(SSOProviderEnabledGuard)
|
||||
async generateMetadata(@Req() req: any): Promise<string> {
|
||||
async generateMetadata(@Req() req: any): Promise<string | void> {
|
||||
return generateServiceProviderMetadata({
|
||||
wantAssertionsSigned: false,
|
||||
issuer: this.ssoService.buildIssuerURL({
|
||||
@ -81,14 +81,26 @@ export class SSOAuthController {
|
||||
@UseGuards(SSOProviderEnabledGuard, OIDCAuthGuard)
|
||||
async oidcAuthCallback(@Req() req: any, @Res() res: Response) {
|
||||
try {
|
||||
const loginToken = await this.generateLoginToken(req.user);
|
||||
const { loginToken, identityProvider } = await this.generateLoginToken(
|
||||
req.user,
|
||||
);
|
||||
|
||||
return res.redirect(
|
||||
this.authService.computeRedirectURI(loginToken.token),
|
||||
await this.authService.computeRedirectURI(
|
||||
loginToken.token,
|
||||
identityProvider.workspace.subdomain,
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
// TODO: improve error management
|
||||
res.status(403).send(err.message);
|
||||
if (err instanceof AuthException) {
|
||||
return res.redirect(
|
||||
this.domainManagerService.computeRedirectErrorUrl({
|
||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
errorMessage: err.message,
|
||||
}),
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,16 +108,26 @@ export class SSOAuthController {
|
||||
@UseGuards(SSOProviderEnabledGuard, SAMLAuthGuard)
|
||||
async samlAuthCallback(@Req() req: any, @Res() res: Response) {
|
||||
try {
|
||||
const loginToken = await this.generateLoginToken(req.user);
|
||||
const { loginToken, identityProvider } = await this.generateLoginToken(
|
||||
req.user,
|
||||
);
|
||||
|
||||
return res.redirect(
|
||||
this.authService.computeRedirectURI(loginToken.token),
|
||||
await this.authService.computeRedirectURI(
|
||||
loginToken.token,
|
||||
identityProvider.workspace.subdomain,
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
// TODO: improve error management
|
||||
res
|
||||
.status(403)
|
||||
.redirect(`${this.environmentService.get('FRONT_BASE_URL')}/verify`);
|
||||
if (err instanceof AuthException) {
|
||||
return res.redirect(
|
||||
this.domainManagerService.computeRedirectErrorUrl({
|
||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
errorMessage: err.message,
|
||||
}),
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,6 +138,13 @@ export class SSOAuthController {
|
||||
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 },
|
||||
@ -129,20 +158,15 @@ export class SSOAuthController {
|
||||
);
|
||||
}
|
||||
|
||||
const invitation =
|
||||
await this.workspaceInvitationService.getOneWorkspaceInvitation(
|
||||
identityProvider.workspaceId,
|
||||
user.email,
|
||||
);
|
||||
|
||||
if (invitation) {
|
||||
await this.authService.signInUp({
|
||||
...user,
|
||||
workspacePersonalInviteToken: invitation.value,
|
||||
workspaceInviteHash: identityProvider.workspace.inviteHash,
|
||||
fromSSO: true,
|
||||
});
|
||||
}
|
||||
await this.authService.signInUp({
|
||||
...user,
|
||||
...(this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')
|
||||
? {
|
||||
targetWorkspaceSubdomain: identityProvider.workspace.subdomain,
|
||||
}
|
||||
: {}),
|
||||
fromSSO: true,
|
||||
});
|
||||
|
||||
const isUserExistInWorkspace =
|
||||
await this.userWorkspaceService.checkUserWorkspaceExistsByEmail(
|
||||
@ -157,6 +181,9 @@ export class SSOAuthController {
|
||||
);
|
||||
}
|
||||
|
||||
return this.loginTokenService.generateLoginToken(user.email);
|
||||
return {
|
||||
identityProvider,
|
||||
loginToken: await this.loginTokenService.generateLoginToken(user.email),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
|
||||
import { VerifyAuthController } from './verify-auth.controller';
|
||||
|
||||
describe('VerifyAuthController', () => {
|
||||
let controller: VerifyAuthController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [VerifyAuthController],
|
||||
providers: [
|
||||
{
|
||||
provide: AuthService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: LoginTokenService,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<VerifyAuthController>(VerifyAuthController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
@ -1,26 +0,0 @@
|
||||
import { Body, Controller, Post, UseFilters } from '@nestjs/common';
|
||||
|
||||
import { Verify } from 'src/engine/core-modules/auth/dto/verify.entity';
|
||||
import { VerifyInput } from 'src/engine/core-modules/auth/dto/verify.input';
|
||||
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
|
||||
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
|
||||
@Controller('auth/verify')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
export class VerifyAuthController {
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly loginTokenService: LoginTokenService,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
async verify(@Body() verifyInput: VerifyInput): Promise<Verify> {
|
||||
const email = await this.loginTokenService.verifyLoginToken(
|
||||
verifyInput.loginToken,
|
||||
);
|
||||
const result = await this.authService.verify(email);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type';
|
||||
import {
|
||||
IdentityProviderType,
|
||||
SSOIdentityProviderStatus,
|
||||
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||
|
||||
@ObjectType()
|
||||
class SSOConnection {
|
||||
@Field(() => IdentityProviderType)
|
||||
type: SSOConfiguration['type'];
|
||||
|
||||
@Field(() => String)
|
||||
id: string;
|
||||
|
||||
@Field(() => String)
|
||||
issuer: string;
|
||||
|
||||
@Field(() => String)
|
||||
name: string;
|
||||
|
||||
@Field(() => SSOIdentityProviderStatus)
|
||||
status: SSOConfiguration['status'];
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class AvailableWorkspaceOutput {
|
||||
@Field(() => String)
|
||||
id: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
displayName?: string;
|
||||
|
||||
@Field(() => String)
|
||||
subdomain: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
logo?: string;
|
||||
|
||||
@Field(() => [SSOConnection])
|
||||
sso: SSOConnection[];
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
import { Field, ObjectType, createUnionType } from '@nestjs/graphql';
|
||||
|
||||
import { AuthTokens } from 'src/engine/core-modules/auth/dto/token.entity';
|
||||
import { FindAvailableSSOIDPOutput } from 'src/engine/core-modules/sso/dtos/find-available-SSO-IDP.output';
|
||||
|
||||
@ObjectType()
|
||||
export class GenerateJWTOutputWithAuthTokens {
|
||||
@Field(() => Boolean)
|
||||
success: boolean;
|
||||
|
||||
@Field(() => String)
|
||||
reason: 'WORKSPACE_AVAILABLE_FOR_SWITCH';
|
||||
|
||||
@Field(() => AuthTokens)
|
||||
authTokens: AuthTokens;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class GenerateJWTOutputWithSSOAUTH {
|
||||
@Field(() => Boolean)
|
||||
success: boolean;
|
||||
|
||||
@Field(() => String)
|
||||
reason: 'WORKSPACE_USE_SSO_AUTH';
|
||||
|
||||
@Field(() => [FindAvailableSSOIDPOutput])
|
||||
availableSSOIDPs: Array<FindAvailableSSOIDPOutput>;
|
||||
}
|
||||
|
||||
export const GenerateJWTOutput = createUnionType({
|
||||
name: 'GenerateJWT',
|
||||
types: () => [GenerateJWTOutputWithAuthTokens, GenerateJWTOutputWithSSOAUTH],
|
||||
resolveType(value) {
|
||||
if (value.reason === 'WORKSPACE_AVAILABLE_FOR_SWITCH') {
|
||||
return GenerateJWTOutputWithAuthTokens;
|
||||
}
|
||||
if (value.reason === 'WORKSPACE_USE_SSO_AUTH') {
|
||||
return GenerateJWTOutputWithSSOAUTH;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
@ -3,7 +3,7 @@ import { ArgsType, Field } from '@nestjs/graphql';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
@ArgsType()
|
||||
export class GenerateJwtInput {
|
||||
export class SwitchWorkspaceInput {
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ -1,7 +1,30 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
import { Field, ObjectType, createUnionType } from '@nestjs/graphql';
|
||||
|
||||
import { AvailableWorkspaceOutput } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
|
||||
|
||||
@ObjectType()
|
||||
export class UserExists {
|
||||
@Field(() => Boolean)
|
||||
exists: boolean;
|
||||
exists: true;
|
||||
|
||||
@Field(() => [AvailableWorkspaceOutput])
|
||||
availableWorkspaces: Array<AvailableWorkspaceOutput>;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class UserNotExists {
|
||||
@Field(() => Boolean)
|
||||
exists: false;
|
||||
}
|
||||
|
||||
export const UserExistsOutput = createUnionType({
|
||||
name: 'UserExistsOutput',
|
||||
types: () => [UserExists, UserNotExists] as const,
|
||||
resolveType(value) {
|
||||
if (value.exists === true) {
|
||||
return UserExists;
|
||||
}
|
||||
|
||||
return UserNotExists;
|
||||
},
|
||||
});
|
||||
|
||||
@ -11,11 +11,11 @@ import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
|
||||
@Catch(AuthException)
|
||||
export class AuthOAuthExceptionFilter implements ExceptionFilter {
|
||||
constructor(private readonly environmentService: EnvironmentService) {}
|
||||
constructor(private readonly domainManagerService: DomainManagerService) {}
|
||||
|
||||
catch(exception: AuthException, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
@ -25,7 +25,7 @@ export class AuthOAuthExceptionFilter implements ExceptionFilter {
|
||||
case AuthExceptionCode.OAUTH_ACCESS_DENIED:
|
||||
response
|
||||
.status(403)
|
||||
.redirect(this.environmentService.get('FRONT_BASE_URL'));
|
||||
.redirect(this.domainManagerService.getBaseUrl().toString());
|
||||
break;
|
||||
default:
|
||||
throw new InternalServerErrorException(exception.message);
|
||||
|
||||
@ -38,6 +38,13 @@ export class GoogleOauthGuard extends AuthGuard('google') {
|
||||
workspacePersonalInviteToken;
|
||||
}
|
||||
|
||||
if (
|
||||
request.query.workspaceSubdomain &&
|
||||
typeof request.query.workspaceSubdomain === 'string'
|
||||
) {
|
||||
request.params.workspaceSubdomain = request.query.workspaceSubdomain;
|
||||
}
|
||||
|
||||
return (await super.canActivate(context)) as boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +26,13 @@ export class MicrosoftOAuthGuard extends AuthGuard('microsoft') {
|
||||
workspacePersonalInviteToken;
|
||||
}
|
||||
|
||||
if (
|
||||
request.query.workspaceSubdomain &&
|
||||
typeof request.query.workspaceSubdomain === 'string'
|
||||
) {
|
||||
request.params.workspaceSubdomain = request.query.workspaceSubdomain;
|
||||
}
|
||||
|
||||
return (await super.canActivate(context)) as boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,17 +1,34 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import bcrypt from 'bcrypt';
|
||||
import { expect, jest } from '@jest/globals';
|
||||
|
||||
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 { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
||||
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
|
||||
import { EmailService } from 'src/engine/core-modules/email/email.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';
|
||||
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 { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
||||
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.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 { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
jest.mock('bcrypt');
|
||||
|
||||
const UserFindOneMock = jest.fn();
|
||||
const UserWorkspaceFindOneByMock = jest.fn();
|
||||
|
||||
const userWorkspaceServiceCheckUserWorkspaceExistsMock = jest.fn();
|
||||
const workspaceInvitationGetOneWorkspaceInvitationMock = jest.fn();
|
||||
const workspaceInvitationValidateInvitationMock = jest.fn();
|
||||
const userWorkspaceAddUserToWorkspaceMock = jest.fn();
|
||||
|
||||
describe('AuthService', () => {
|
||||
let service: AuthService;
|
||||
|
||||
@ -25,7 +42,9 @@ describe('AuthService', () => {
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(User, 'core'),
|
||||
useValue: {},
|
||||
useValue: {
|
||||
findOne: UserFindOneMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(AppToken, 'core'),
|
||||
@ -39,6 +58,10 @@ describe('AuthService', () => {
|
||||
provide: EnvironmentService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: DomainManagerService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: EmailService,
|
||||
useValue: {},
|
||||
@ -51,13 +74,114 @@ describe('AuthService', () => {
|
||||
provide: RefreshTokenService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: UserWorkspaceService,
|
||||
useValue: {
|
||||
checkUserWorkspaceExists:
|
||||
userWorkspaceServiceCheckUserWorkspaceExistsMock,
|
||||
addUserToWorkspace: userWorkspaceAddUserToWorkspaceMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceInvitationService,
|
||||
useValue: {
|
||||
getOneWorkspaceInvitation:
|
||||
workspaceInvitationGetOneWorkspaceInvitationMock,
|
||||
validateInvitation: workspaceInvitationValidateInvitationMock,
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<AuthService>(AuthService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
it('should be defined', async () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it('challenge - user already member of workspace', async () => {
|
||||
const workspace = { isPasswordAuthEnabled: true } as Workspace;
|
||||
const user = {
|
||||
email: 'email',
|
||||
password: 'password',
|
||||
captchaToken: 'captchaToken',
|
||||
};
|
||||
|
||||
(bcrypt.compare as jest.Mock).mockReturnValueOnce(true);
|
||||
|
||||
UserFindOneMock.mockReturnValueOnce({
|
||||
email: user.email,
|
||||
passwordHash: 'passwordHash',
|
||||
captchaToken: user.captchaToken,
|
||||
});
|
||||
|
||||
UserWorkspaceFindOneByMock.mockReturnValueOnce({});
|
||||
|
||||
userWorkspaceServiceCheckUserWorkspaceExistsMock.mockReturnValueOnce({});
|
||||
|
||||
const response = await service.challenge(
|
||||
{
|
||||
email: 'email',
|
||||
password: 'password',
|
||||
captchaToken: 'captchaToken',
|
||||
},
|
||||
workspace,
|
||||
);
|
||||
|
||||
expect(response).toStrictEqual({
|
||||
email: user.email,
|
||||
passwordHash: 'passwordHash',
|
||||
captchaToken: user.captchaToken,
|
||||
});
|
||||
});
|
||||
|
||||
it('challenge - user who have an invitation', async () => {
|
||||
const user = {
|
||||
email: 'email',
|
||||
password: 'password',
|
||||
captchaToken: 'captchaToken',
|
||||
};
|
||||
|
||||
UserFindOneMock.mockReturnValueOnce({
|
||||
email: user.email,
|
||||
passwordHash: 'passwordHash',
|
||||
captchaToken: user.captchaToken,
|
||||
});
|
||||
|
||||
(bcrypt.compare as jest.Mock).mockReturnValueOnce(true);
|
||||
userWorkspaceServiceCheckUserWorkspaceExistsMock.mockReturnValueOnce(false);
|
||||
|
||||
workspaceInvitationGetOneWorkspaceInvitationMock.mockReturnValueOnce({});
|
||||
workspaceInvitationValidateInvitationMock.mockReturnValueOnce({});
|
||||
userWorkspaceAddUserToWorkspaceMock.mockReturnValueOnce({});
|
||||
|
||||
const response = await service.challenge(
|
||||
{
|
||||
email: 'email',
|
||||
password: 'password',
|
||||
captchaToken: 'captchaToken',
|
||||
},
|
||||
{
|
||||
isPasswordAuthEnabled: true,
|
||||
} as Workspace,
|
||||
);
|
||||
|
||||
expect(response).toStrictEqual({
|
||||
email: user.email,
|
||||
passwordHash: 'passwordHash',
|
||||
captchaToken: user.captchaToken,
|
||||
});
|
||||
|
||||
expect(
|
||||
workspaceInvitationGetOneWorkspaceInvitationMock,
|
||||
).toHaveBeenCalledTimes(1);
|
||||
expect(workspaceInvitationValidateInvitationMock).toHaveBeenCalledTimes(1);
|
||||
expect(userWorkspaceAddUserToWorkspaceMock).toHaveBeenCalledTimes(1);
|
||||
expect(UserFindOneMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -28,7 +28,10 @@ import { AuthorizeApp } from 'src/engine/core-modules/auth/dto/authorize-app.ent
|
||||
import { AuthorizeAppInput } from 'src/engine/core-modules/auth/dto/authorize-app.input';
|
||||
import { ChallengeInput } from 'src/engine/core-modules/auth/dto/challenge.input';
|
||||
import { UpdatePassword } from 'src/engine/core-modules/auth/dto/update-password.entity';
|
||||
import { UserExists } from 'src/engine/core-modules/auth/dto/user-exists.entity';
|
||||
import {
|
||||
UserExists,
|
||||
UserNotExists,
|
||||
} from 'src/engine/core-modules/auth/dto/user-exists.entity';
|
||||
import { Verify } from 'src/engine/core-modules/auth/dto/verify.entity';
|
||||
import { WorkspaceInviteHashValid } from 'src/engine/core-modules/auth/dto/workspace-invite-hash-valid.entity';
|
||||
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
|
||||
@ -38,12 +41,24 @@ import { EmailService } from 'src/engine/core-modules/email/email.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';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
|
||||
import { AvailableWorkspaceOutput } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
|
||||
@Injectable()
|
||||
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private readonly accessTokenService: AccessTokenService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly refreshTokenService: RefreshTokenService,
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
private readonly userService: UserService,
|
||||
private readonly workspaceInvitationService: WorkspaceInvitationService,
|
||||
private readonly signInUpService: SignInUpService,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
@ -55,9 +70,54 @@ export class AuthService {
|
||||
private readonly appTokenRepository: Repository<AppToken>,
|
||||
) {}
|
||||
|
||||
async challenge(challengeInput: ChallengeInput) {
|
||||
const user = await this.userRepository.findOneBy({
|
||||
email: challengeInput.email,
|
||||
private async checkAccessAndUseInvitationOrThrow(
|
||||
workspace: Workspace,
|
||||
user: User,
|
||||
) {
|
||||
if (
|
||||
await this.userWorkspaceService.checkUserWorkspaceExists(
|
||||
user.id,
|
||||
workspace.id,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const invitation =
|
||||
await this.workspaceInvitationService.getOneWorkspaceInvitation(
|
||||
workspace.id,
|
||||
user.email,
|
||||
);
|
||||
|
||||
if (invitation) {
|
||||
await this.workspaceInvitationService.validateInvitation({
|
||||
workspacePersonalInviteToken: invitation.value,
|
||||
email: user.email,
|
||||
});
|
||||
await this.userWorkspaceService.addUserToWorkspace(user, workspace);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw new AuthException(
|
||||
"You're not member of this workspace.",
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
async challenge(challengeInput: ChallengeInput, targetWorkspace: Workspace) {
|
||||
if (!targetWorkspace.isPasswordAuthEnabled) {
|
||||
throw new AuthException(
|
||||
'Email/Password auth is not enabled for this workspace',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
email: challengeInput.email,
|
||||
},
|
||||
relations: ['workspaces'],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
@ -67,6 +127,8 @@ export class AuthService {
|
||||
);
|
||||
}
|
||||
|
||||
await this.checkAccessAndUseInvitationOrThrow(targetWorkspace, user);
|
||||
|
||||
if (!user.passwordHash) {
|
||||
throw new AuthException(
|
||||
'Incorrect login method',
|
||||
@ -94,19 +156,23 @@ export class AuthService {
|
||||
password,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
targetWorkspaceSubdomain,
|
||||
firstName,
|
||||
lastName,
|
||||
picture,
|
||||
fromSSO,
|
||||
isAuthEnabled,
|
||||
}: {
|
||||
email: string;
|
||||
password?: string;
|
||||
firstName?: string | null;
|
||||
lastName?: string | null;
|
||||
workspaceInviteHash?: string | null;
|
||||
workspacePersonalInviteToken?: string | null;
|
||||
workspaceInviteHash?: string;
|
||||
workspacePersonalInviteToken?: string;
|
||||
picture?: string | null;
|
||||
fromSSO: boolean;
|
||||
targetWorkspaceSubdomain?: string;
|
||||
isAuthEnabled?: ReturnType<(typeof workspaceValidator)['isAuthEnabled']>;
|
||||
}) {
|
||||
return await this.signInUpService.signInUp({
|
||||
email,
|
||||
@ -115,12 +181,14 @@ export class AuthService {
|
||||
lastName,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
targetWorkspaceSubdomain,
|
||||
picture,
|
||||
fromSSO,
|
||||
isAuthEnabled,
|
||||
});
|
||||
}
|
||||
|
||||
async verify(email: string): Promise<Verify> {
|
||||
async verify(email: string, workspaceId?: string): Promise<Verify> {
|
||||
if (!email) {
|
||||
throw new AuthException(
|
||||
'Email is required',
|
||||
@ -128,6 +196,26 @@ export class AuthService {
|
||||
);
|
||||
}
|
||||
|
||||
const userWithIdAndDefaultWorkspaceId = await this.userRepository.findOne({
|
||||
select: ['defaultWorkspaceId', 'id'],
|
||||
where: { email },
|
||||
});
|
||||
|
||||
userValidator.assertIsExist(
|
||||
userWithIdAndDefaultWorkspaceId,
|
||||
new AuthException('User not found', AuthExceptionCode.USER_NOT_FOUND),
|
||||
);
|
||||
|
||||
if (
|
||||
workspaceId &&
|
||||
userWithIdAndDefaultWorkspaceId.defaultWorkspaceId !== workspaceId
|
||||
) {
|
||||
await this.userService.saveDefaultWorkspace(
|
||||
userWithIdAndDefaultWorkspaceId.id,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
email,
|
||||
@ -135,19 +223,10 @@ export class AuthService {
|
||||
relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new AuthException(
|
||||
'User not found',
|
||||
AuthExceptionCode.USER_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (!user.defaultWorkspace) {
|
||||
throw new AuthException(
|
||||
'User has no default workspace',
|
||||
AuthExceptionCode.INVALID_DATA,
|
||||
);
|
||||
}
|
||||
userValidator.assertIsExist(
|
||||
user,
|
||||
new AuthException('User not found', AuthExceptionCode.USER_NOT_FOUND),
|
||||
);
|
||||
|
||||
// passwordHash is hidden for security reasons
|
||||
user.passwordHash = '';
|
||||
@ -170,12 +249,19 @@ export class AuthService {
|
||||
};
|
||||
}
|
||||
|
||||
async checkUserExists(email: string): Promise<UserExists> {
|
||||
async checkUserExists(email: string): Promise<UserExists | UserNotExists> {
|
||||
const user = await this.userRepository.findOneBy({
|
||||
email,
|
||||
});
|
||||
|
||||
return { exists: !!user };
|
||||
if (userValidator.isExist(user)) {
|
||||
return {
|
||||
exists: true,
|
||||
availableWorkspaces: await this.findAvailableWorkspacesByEmail(email),
|
||||
};
|
||||
}
|
||||
|
||||
return { exists: false };
|
||||
}
|
||||
|
||||
async checkWorkspaceInviteHashIsValid(
|
||||
@ -312,7 +398,7 @@ export class AuthService {
|
||||
const emailTemplate = PasswordUpdateNotifyEmail({
|
||||
userName: `${user.firstName} ${user.lastName}`,
|
||||
email: user.email,
|
||||
link: this.environmentService.get('FRONT_BASE_URL'),
|
||||
link: this.domainManagerService.getBaseUrl().toString(),
|
||||
});
|
||||
|
||||
const html = render(emailTemplate, {
|
||||
@ -352,9 +438,55 @@ export class AuthService {
|
||||
return workspace;
|
||||
}
|
||||
|
||||
computeRedirectURI(loginToken: string): string {
|
||||
return `${this.environmentService.get(
|
||||
'FRONT_BASE_URL',
|
||||
)}/verify?loginToken=${loginToken}`;
|
||||
async computeRedirectURI(loginToken: string, subdomain?: string) {
|
||||
const url = this.domainManagerService.buildWorkspaceURL({
|
||||
subdomain,
|
||||
pathname: '/verify',
|
||||
searchParams: { loginToken },
|
||||
});
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
async findAvailableWorkspacesByEmail(email: string) {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
relations: [
|
||||
'workspaces',
|
||||
'workspaces.workspace',
|
||||
'workspaces.workspace.workspaceSSOIdentityProviders',
|
||||
],
|
||||
});
|
||||
|
||||
userValidator.assertIsExist(
|
||||
user,
|
||||
new AuthException('User not found', AuthExceptionCode.USER_NOT_FOUND),
|
||||
);
|
||||
|
||||
return user.workspaces.map<AvailableWorkspaceOutput>((userWorkspace) => ({
|
||||
id: userWorkspace.workspaceId,
|
||||
displayName: userWorkspace.workspace.displayName,
|
||||
subdomain: userWorkspace.workspace.subdomain,
|
||||
logo: userWorkspace.workspace.logo,
|
||||
sso: userWorkspace.workspace.workspaceSSOIdentityProviders.reduce(
|
||||
(acc, identityProvider) =>
|
||||
acc.concat(
|
||||
identityProvider.status === 'Inactive'
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: identityProvider.id,
|
||||
name: identityProvider.name,
|
||||
issuer: identityProvider.issuer,
|
||||
type: identityProvider.type,
|
||||
status: identityProvider.status,
|
||||
},
|
||||
],
|
||||
),
|
||||
[] as AvailableWorkspaceOutput['sso'],
|
||||
),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import { EmailService } from 'src/engine/core-modules/email/email.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';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
|
||||
import { ResetPasswordService } from './reset-password.service';
|
||||
|
||||
@ -45,6 +46,14 @@ describe('ResetPasswordService', () => {
|
||||
send: jest.fn().mockResolvedValue({ success: true }),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: DomainManagerService,
|
||||
useValue: {
|
||||
getBaseUrl: jest
|
||||
.fn()
|
||||
.mockResolvedValue(new URL('http://localhost:3001')),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: EnvironmentService,
|
||||
useValue: {
|
||||
|
||||
@ -24,11 +24,13 @@ import { ValidatePasswordResetToken } from 'src/engine/core-modules/auth/dto/val
|
||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
|
||||
@Injectable()
|
||||
export class ResetPasswordService {
|
||||
constructor(
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
@InjectRepository(AppToken, 'core')
|
||||
@ -116,11 +118,12 @@ export class ResetPasswordService {
|
||||
);
|
||||
}
|
||||
|
||||
const frontBaseURL = this.environmentService.get('FRONT_BASE_URL');
|
||||
const resetLink = `${frontBaseURL}/reset-password/${resetToken.passwordResetToken}`;
|
||||
const frontBaseURL = this.domainManagerService.getBaseUrl();
|
||||
|
||||
frontBaseURL.pathname = `/reset-password/${resetToken.passwordResetToken}`;
|
||||
|
||||
const emailData = {
|
||||
link: resetLink,
|
||||
link: frontBaseURL.toString(),
|
||||
duration: ms(
|
||||
differenceInMilliseconds(
|
||||
resetToken.passwordResetTokenExpiresAt,
|
||||
|
||||
@ -2,18 +2,45 @@ import { HttpService } from '@nestjs/axios';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import bcrypt from 'bcrypt';
|
||||
import { expect, jest } from '@jest/globals';
|
||||
|
||||
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.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 { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import {
|
||||
Workspace,
|
||||
WorkspaceActivationStatus,
|
||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
|
||||
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
|
||||
|
||||
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();
|
||||
|
||||
describe('SignInUpService', () => {
|
||||
let service: SignInUpService;
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
@ -24,23 +51,48 @@ describe('SignInUpService', () => {
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(Workspace, 'core'),
|
||||
useValue: {},
|
||||
useValue: {
|
||||
count: WorkspaceCountMock,
|
||||
create: WorkspaceCreateMock,
|
||||
save: WorkspaceSaveMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(User, 'core'),
|
||||
useValue: {},
|
||||
useValue: {
|
||||
findOne: UserFindOneMock,
|
||||
create: UserCreateMock,
|
||||
save: UserSaveMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(AppToken, 'core'),
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: UserWorkspaceService,
|
||||
provide: WorkspaceInvitationService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceService,
|
||||
useValue: {
|
||||
generateSubdomain: jest.fn().mockReturnValue('tartanpion'),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserWorkspaceService,
|
||||
useValue: {
|
||||
addUserToWorkspace: userWorkspaceServiceAddUserToWorkspaceMock,
|
||||
create: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: OnboardingService,
|
||||
useValue: {},
|
||||
useValue: {
|
||||
setOnboardingConnectAccountPending: jest.fn(),
|
||||
setOnboardingInviteTeamPending: jest.fn(),
|
||||
setOnboardingCreateProfilePending: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: HttpService,
|
||||
@ -48,7 +100,19 @@ describe('SignInUpService', () => {
|
||||
},
|
||||
{
|
||||
provide: EnvironmentService,
|
||||
useValue: {},
|
||||
useValue: {
|
||||
get: EnvironmentServiceGetMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceInvitationService,
|
||||
useValue: {
|
||||
validateInvitation: workspaceInvitationValidateInvitationMock,
|
||||
invalidateWorkspaceInvitation:
|
||||
workspaceInvitationInvalidateWorkspaceInvitationMock,
|
||||
findInvitationByWorkspaceSubdomainAndUserEmail:
|
||||
workspaceInvitationFindInvitationByWorkspaceSubdomainAndUserEmailMock,
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
@ -59,4 +123,363 @@ describe('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,
|
||||
);
|
||||
|
||||
const spy = jest
|
||||
.spyOn(service, 'signUpOnNewWorkspace')
|
||||
.mockResolvedValueOnce({} as User);
|
||||
|
||||
await service.signInUp({
|
||||
email: 'test@test.com',
|
||||
fromSSO: true,
|
||||
targetWorkspaceSubdomain: 'tartanpion',
|
||||
});
|
||||
|
||||
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,
|
||||
defaultWorkspace: { id: 'workspace-id' },
|
||||
};
|
||||
|
||||
UserFindOneMock.mockReturnValueOnce(existingUser);
|
||||
workspaceInvitationFindInvitationByWorkspaceSubdomainAndUserEmailMock.mockReturnValueOnce(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const result = await service.signInUp({
|
||||
email,
|
||||
fromSSO: true,
|
||||
targetWorkspaceSubdomain: 'tartanpion',
|
||||
});
|
||||
|
||||
expect(result).toEqual(existingUser);
|
||||
});
|
||||
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: 'tartanpion',
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
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,
|
||||
defaultWorkspace: { id: 'workspace-id' },
|
||||
};
|
||||
|
||||
UserFindOneMock.mockReturnValueOnce(existingUser);
|
||||
workspaceInvitationFindInvitationByWorkspaceSubdomainAndUserEmailMock.mockReturnValueOnce(
|
||||
{
|
||||
value: 'personal-token-value',
|
||||
},
|
||||
);
|
||||
workspaceInvitationValidateInvitationMock.mockReturnValueOnce({
|
||||
isValid: true,
|
||||
workspace: {
|
||||
id: workspaceId,
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
},
|
||||
});
|
||||
|
||||
workspaceInvitationInvalidateWorkspaceInvitationMock.mockReturnValueOnce(
|
||||
true,
|
||||
);
|
||||
|
||||
userWorkspaceServiceAddUserToWorkspaceMock.mockReturnValueOnce({});
|
||||
|
||||
const result = await service.signInUp({
|
||||
email,
|
||||
fromSSO: true,
|
||||
targetWorkspaceSubdomain: 'tartanpion',
|
||||
});
|
||||
|
||||
expect(result).toEqual(existingUser);
|
||||
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';
|
||||
|
||||
UserFindOneMock.mockReturnValueOnce(null);
|
||||
|
||||
workspaceInvitationValidateInvitationMock.mockReturnValueOnce({
|
||||
isValid: true,
|
||||
workspace: {
|
||||
id: workspaceId,
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
},
|
||||
});
|
||||
|
||||
workspaceInvitationInvalidateWorkspaceInvitationMock.mockReturnValueOnce(
|
||||
true,
|
||||
);
|
||||
|
||||
const spySignInUpOnExistingWorkspace = jest
|
||||
.spyOn(service, 'signInUpOnExistingWorkspace')
|
||||
.mockResolvedValueOnce(
|
||||
{} as Awaited<
|
||||
ReturnType<(typeof service)['signInUpOnExistingWorkspace']>
|
||||
>,
|
||||
);
|
||||
|
||||
await service.signInUp({
|
||||
email,
|
||||
fromSSO: true,
|
||||
workspacePersonalInviteToken,
|
||||
targetWorkspaceSubdomain: 'tartanpion',
|
||||
});
|
||||
|
||||
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,
|
||||
defaultWorkspace: { id: 'workspace-id' },
|
||||
};
|
||||
|
||||
UserFindOneMock.mockReturnValueOnce(existingUser);
|
||||
workspaceInvitationValidateInvitationMock.mockReturnValueOnce({
|
||||
isValid: true,
|
||||
workspace: {
|
||||
id: workspaceId,
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
},
|
||||
});
|
||||
|
||||
workspaceInvitationInvalidateWorkspaceInvitationMock.mockReturnValueOnce(
|
||||
true,
|
||||
);
|
||||
|
||||
await service.signInUp({
|
||||
email,
|
||||
fromSSO: true,
|
||||
workspacePersonalInviteToken,
|
||||
targetWorkspaceSubdomain: 'tartanpion',
|
||||
});
|
||||
|
||||
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',
|
||||
defaultWorkspace: { id: 'workspace-id' },
|
||||
};
|
||||
|
||||
UserFindOneMock.mockReturnValueOnce(existingUser);
|
||||
|
||||
EnvironmentServiceGetMock.mockReturnValueOnce(false);
|
||||
|
||||
(bcrypt.compare as jest.Mock).mockReturnValueOnce(true);
|
||||
|
||||
await service.signInUp({
|
||||
email,
|
||||
password,
|
||||
fromSSO: false,
|
||||
targetWorkspaceSubdomain: 'tartanpion',
|
||||
});
|
||||
|
||||
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: 'tartanpion',
|
||||
});
|
||||
|
||||
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: 'tartanpion',
|
||||
});
|
||||
|
||||
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,
|
||||
workspaceInviteHash,
|
||||
});
|
||||
|
||||
expect(UserCreateMock).toHaveBeenCalledTimes(1);
|
||||
expect(UserSaveMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(
|
||||
workspaceInvitationInvalidateWorkspaceInvitationMock,
|
||||
).toHaveBeenCalledWith(workspaceId, email);
|
||||
});
|
||||
});
|
||||
|
||||
@ -9,20 +9,15 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||
|
||||
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import {
|
||||
PASSWORD_REGEX,
|
||||
compareHash,
|
||||
hashPassword,
|
||||
PASSWORD_REGEX,
|
||||
} from 'src/engine/core-modules/auth/auth.util';
|
||||
import {
|
||||
EnvironmentException,
|
||||
EnvironmentExceptionCode,
|
||||
} from 'src/engine/core-modules/environment/environment.exception';
|
||||
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';
|
||||
@ -33,44 +28,56 @@ import {
|
||||
WorkspaceActivationStatus,
|
||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { getImageBufferFromUrl } from 'src/utils/image';
|
||||
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
|
||||
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
import {
|
||||
EnvironmentException,
|
||||
EnvironmentExceptionCode,
|
||||
} from 'src/engine/core-modules/environment/environment.exception';
|
||||
|
||||
export type SignInUpServiceInput = {
|
||||
email: string;
|
||||
password?: string;
|
||||
firstName?: string | null;
|
||||
lastName?: string | null;
|
||||
workspaceInviteHash?: string | null;
|
||||
workspacePersonalInviteToken?: string | null;
|
||||
workspaceInviteHash?: string;
|
||||
workspacePersonalInviteToken?: string;
|
||||
picture?: string | null;
|
||||
fromSSO: boolean;
|
||||
targetWorkspaceSubdomain?: string;
|
||||
isAuthEnabled?: ReturnType<(typeof workspaceValidator)['isAuthEnabled']>;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
||||
export class SignInUpService {
|
||||
constructor(
|
||||
private readonly fileUploadService: FileUploadService,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(AppToken, 'core')
|
||||
private readonly appTokenRepository: Repository<AppToken>,
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly fileUploadService: FileUploadService,
|
||||
private readonly workspaceInvitationService: WorkspaceInvitationService,
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
private readonly onboardingService: OnboardingService,
|
||||
private readonly httpService: HttpService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
) {}
|
||||
|
||||
async signInUp({
|
||||
email,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
workspaceInviteHash,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
picture,
|
||||
fromSSO,
|
||||
targetWorkspaceSubdomain,
|
||||
isAuthEnabled,
|
||||
}: SignInUpServiceInput) {
|
||||
if (!firstName) firstName = '';
|
||||
if (!lastName) lastName = '';
|
||||
@ -96,9 +103,7 @@ export class SignInUpService {
|
||||
const passwordHash = password ? await hashPassword(password) : undefined;
|
||||
|
||||
const existingUser = await this.userRepository.findOne({
|
||||
where: {
|
||||
email: email,
|
||||
},
|
||||
where: { email },
|
||||
relations: ['defaultWorkspace'],
|
||||
});
|
||||
|
||||
@ -116,18 +121,55 @@ export class SignInUpService {
|
||||
}
|
||||
}
|
||||
|
||||
if (workspaceInviteHash) {
|
||||
return await this.signInUpOnExistingWorkspace({
|
||||
email,
|
||||
passwordHash,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
firstName,
|
||||
lastName,
|
||||
picture,
|
||||
existingUser,
|
||||
});
|
||||
const maybeInvitation =
|
||||
fromSSO && !workspacePersonalInviteToken && !workspaceInviteHash
|
||||
? await this.workspaceInvitationService.findInvitationByWorkspaceSubdomainAndUserEmail(
|
||||
{
|
||||
subdomain: targetWorkspaceSubdomain,
|
||||
email,
|
||||
},
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (
|
||||
workspacePersonalInviteToken ||
|
||||
workspaceInviteHash ||
|
||||
maybeInvitation
|
||||
) {
|
||||
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({
|
||||
email,
|
||||
passwordHash,
|
||||
workspace: invitationValidation.workspace,
|
||||
firstName,
|
||||
lastName,
|
||||
picture,
|
||||
existingUser,
|
||||
isAuthEnabled,
|
||||
});
|
||||
|
||||
await this.workspaceInvitationService.invalidateWorkspaceInvitation(
|
||||
invitationValidation.workspace.id,
|
||||
email,
|
||||
);
|
||||
|
||||
return updatedUser;
|
||||
}
|
||||
}
|
||||
|
||||
if (!existingUser) {
|
||||
return await this.signUpOnNewWorkspace({
|
||||
email,
|
||||
@ -141,47 +183,46 @@ export class SignInUpService {
|
||||
return existingUser;
|
||||
}
|
||||
|
||||
private async signInUpOnExistingWorkspace({
|
||||
async signInUpOnExistingWorkspace({
|
||||
email,
|
||||
passwordHash,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
workspace,
|
||||
firstName,
|
||||
lastName,
|
||||
picture,
|
||||
existingUser,
|
||||
isAuthEnabled,
|
||||
}: {
|
||||
email: string;
|
||||
passwordHash: string | undefined;
|
||||
workspaceInviteHash: string | null;
|
||||
workspacePersonalInviteToken: string | null | undefined;
|
||||
workspace: Workspace;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
picture: SignInUpServiceInput['picture'];
|
||||
existingUser: User | null;
|
||||
isAuthEnabled?: ReturnType<(typeof workspaceValidator)['isAuthEnabled']>;
|
||||
}) {
|
||||
const isNewUser = !isDefined(existingUser);
|
||||
let user = existingUser;
|
||||
|
||||
const workspace = await this.findWorkspaceAndValidateInvitation({
|
||||
workspacePersonalInviteToken,
|
||||
workspaceInviteHash,
|
||||
email,
|
||||
});
|
||||
|
||||
if (!workspace) {
|
||||
throw new AuthException(
|
||||
workspaceValidator.assertIsExist(
|
||||
workspace,
|
||||
new AuthException(
|
||||
'Workspace not found',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
|
||||
if (!(workspace.activationStatus === WorkspaceActivationStatus.ACTIVE)) {
|
||||
throw new AuthException(
|
||||
workspaceValidator.assertIsActive(
|
||||
workspace,
|
||||
new AuthException(
|
||||
'Workspace is not ready to welcome new members',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
|
||||
if (isAuthEnabled)
|
||||
workspaceValidator.validateAuth(isAuthEnabled, workspace);
|
||||
|
||||
if (isNewUser) {
|
||||
const imagePath = await this.uploadPicture(picture, workspace.id);
|
||||
@ -199,19 +240,18 @@ export class SignInUpService {
|
||||
user = await this.userRepository.save(userToCreate);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new AuthException(
|
||||
userValidator.assertIsExist(
|
||||
user,
|
||||
new AuthException(
|
||||
'User not found',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
|
||||
const updatedUser = workspacePersonalInviteToken
|
||||
? await this.userWorkspaceService.addUserToWorkspaceByInviteToken(
|
||||
workspacePersonalInviteToken,
|
||||
user,
|
||||
)
|
||||
: await this.userWorkspaceService.addUserToWorkspace(user, workspace);
|
||||
const updatedUser = await this.userWorkspaceService.addUserToWorkspace(
|
||||
user,
|
||||
workspace,
|
||||
);
|
||||
|
||||
await this.activateOnboardingForUser(user, workspace, {
|
||||
firstName,
|
||||
@ -221,53 +261,6 @@ export class SignInUpService {
|
||||
return Object.assign(user, updatedUser);
|
||||
}
|
||||
|
||||
private async findWorkspaceAndValidateInvitation({
|
||||
workspacePersonalInviteToken,
|
||||
workspaceInviteHash,
|
||||
email,
|
||||
}) {
|
||||
if (!workspacePersonalInviteToken && !workspaceInviteHash) {
|
||||
throw new AuthException(
|
||||
'No invite token or hash provided',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
const workspace = await this.workspaceRepository.findOneBy({
|
||||
inviteHash: workspaceInviteHash,
|
||||
});
|
||||
|
||||
if (!workspace) {
|
||||
throw new AuthException(
|
||||
'Workspace not found',
|
||||
AuthExceptionCode.WORKSPACE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (!workspacePersonalInviteToken && !workspace.isPublicInviteLinkEnabled) {
|
||||
throw new AuthException(
|
||||
'Workspace does not allow public invites',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
if (workspacePersonalInviteToken && workspace.isPublicInviteLinkEnabled) {
|
||||
try {
|
||||
await this.userWorkspaceService.validateInvitation(
|
||||
workspacePersonalInviteToken,
|
||||
email,
|
||||
);
|
||||
} catch (err) {
|
||||
throw new AuthException(
|
||||
err.message,
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return workspace;
|
||||
}
|
||||
|
||||
private async activateOnboardingForUser(
|
||||
user: User,
|
||||
workspace: Workspace,
|
||||
@ -288,7 +281,7 @@ export class SignInUpService {
|
||||
}
|
||||
}
|
||||
|
||||
private async signUpOnNewWorkspace({
|
||||
async signUpOnNewWorkspace({
|
||||
email,
|
||||
passwordHash,
|
||||
firstName,
|
||||
@ -301,14 +294,20 @@ export class SignInUpService {
|
||||
lastName: string;
|
||||
picture: SignInUpServiceInput['picture'];
|
||||
}) {
|
||||
if (this.environmentService.get('IS_SIGN_UP_DISABLED')) {
|
||||
throw new EnvironmentException(
|
||||
'Sign up is disabled',
|
||||
EnvironmentExceptionCode.ENVIRONMENT_VARIABLES_NOT_FOUND,
|
||||
);
|
||||
if (!this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')) {
|
||||
const workspacesCount = await this.workspaceRepository.count();
|
||||
|
||||
// let the creation of the first workspace
|
||||
if (workspacesCount > 0) {
|
||||
throw new EnvironmentException(
|
||||
'New workspace setup is disabled',
|
||||
EnvironmentExceptionCode.ENVIRONMENT_VARIABLES_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const workspaceToCreate = this.workspaceRepository.create({
|
||||
subdomain: await this.domainManagerService.generateSubdomain(),
|
||||
displayName: '',
|
||||
domainName: '',
|
||||
inviteHash: v4(),
|
||||
|
||||
@ -6,9 +6,9 @@ import { Repository } from 'typeorm';
|
||||
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
||||
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
|
||||
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
|
||||
import { SwitchWorkspaceService } from './switch-workspace.service';
|
||||
|
||||
@ -16,7 +16,7 @@ describe('SwitchWorkspaceService', () => {
|
||||
let service: SwitchWorkspaceService;
|
||||
let userRepository: Repository<User>;
|
||||
let workspaceRepository: Repository<Workspace>;
|
||||
let ssoService: SSOService;
|
||||
let userService: UserService;
|
||||
let accessTokenService: AccessTokenService;
|
||||
let refreshTokenService: RefreshTokenService;
|
||||
|
||||
@ -32,12 +32,6 @@ describe('SwitchWorkspaceService', () => {
|
||||
provide: getRepositoryToken(Workspace, 'core'),
|
||||
useClass: Repository,
|
||||
},
|
||||
{
|
||||
provide: SSOService,
|
||||
useValue: {
|
||||
listSSOIdentityProvidersByWorkspaceId: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: AccessTokenService,
|
||||
useValue: {
|
||||
@ -50,6 +44,12 @@ describe('SwitchWorkspaceService', () => {
|
||||
generateRefreshToken: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
saveDefaultWorkspace: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@ -60,9 +60,9 @@ describe('SwitchWorkspaceService', () => {
|
||||
workspaceRepository = module.get<Repository<Workspace>>(
|
||||
getRepositoryToken(Workspace, 'core'),
|
||||
);
|
||||
ssoService = module.get<SSOService>(SSOService);
|
||||
accessTokenService = module.get<AccessTokenService>(AccessTokenService);
|
||||
refreshTokenService = module.get<RefreshTokenService>(RefreshTokenService);
|
||||
userService = module.get<UserService>(UserService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
@ -101,7 +101,6 @@ describe('SwitchWorkspaceService', () => {
|
||||
const mockWorkspace = {
|
||||
id: 'workspace-id',
|
||||
workspaceUsers: [{ userId: 'other-user-id' }],
|
||||
workspaceSSOIdentityProviders: [],
|
||||
};
|
||||
|
||||
jest
|
||||
@ -121,19 +120,25 @@ describe('SwitchWorkspaceService', () => {
|
||||
const mockWorkspace = {
|
||||
id: 'workspace-id',
|
||||
workspaceUsers: [{ userId: 'user-id' }],
|
||||
workspaceSSOIdentityProviders: [{}],
|
||||
logo: 'logo',
|
||||
displayName: 'displayName',
|
||||
isGoogleAuthEnabled: true,
|
||||
isPasswordAuthEnabled: true,
|
||||
isMicrosoftAuthEnabled: false,
|
||||
workspaceSSOIdentityProviders: [
|
||||
{
|
||||
id: 'sso-id',
|
||||
},
|
||||
],
|
||||
};
|
||||
const mockSSOProviders = [{ id: 'sso-provider-id' }];
|
||||
|
||||
jest
|
||||
.spyOn(userRepository, 'findBy')
|
||||
.mockResolvedValue([mockUser as User]);
|
||||
jest.spyOn(userRepository, 'save').mockResolvedValue(mockUser as User);
|
||||
jest
|
||||
.spyOn(workspaceRepository, 'findOne')
|
||||
.mockResolvedValue(mockWorkspace as any);
|
||||
jest
|
||||
.spyOn(ssoService, 'listSSOIdentityProvidersByWorkspaceId')
|
||||
.mockResolvedValue(mockSSOProviders as any);
|
||||
|
||||
const result = await service.switchWorkspace(
|
||||
mockUser as User,
|
||||
@ -141,9 +146,10 @@ describe('SwitchWorkspaceService', () => {
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
useSSOAuth: true,
|
||||
workspace: mockWorkspace,
|
||||
availableSSOIdentityProviders: mockSSOProviders,
|
||||
id: mockWorkspace.id,
|
||||
logo: expect.any(String),
|
||||
displayName: expect.any(String),
|
||||
authProviders: expect.any(Object),
|
||||
});
|
||||
});
|
||||
|
||||
@ -153,6 +159,8 @@ describe('SwitchWorkspaceService', () => {
|
||||
id: 'workspace-id',
|
||||
workspaceUsers: [{ userId: 'user-id' }],
|
||||
workspaceSSOIdentityProviders: [],
|
||||
logo: 'logo',
|
||||
displayName: 'displayName',
|
||||
};
|
||||
|
||||
jest
|
||||
@ -161,6 +169,7 @@ describe('SwitchWorkspaceService', () => {
|
||||
jest
|
||||
.spyOn(workspaceRepository, 'findOne')
|
||||
.mockResolvedValue(mockWorkspace as any);
|
||||
jest.spyOn(userRepository, 'save').mockResolvedValue({} as User);
|
||||
|
||||
const result = await service.switchWorkspace(
|
||||
mockUser as User,
|
||||
@ -168,8 +177,10 @@ describe('SwitchWorkspaceService', () => {
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
useSSOAuth: false,
|
||||
workspace: mockWorkspace,
|
||||
id: mockWorkspace.id,
|
||||
logo: expect.any(String),
|
||||
displayName: expect.any(String),
|
||||
authProviders: expect.any(Object),
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -200,10 +211,10 @@ describe('SwitchWorkspaceService', () => {
|
||||
refreshToken: mockRefreshToken,
|
||||
},
|
||||
});
|
||||
expect(userRepository.save).toHaveBeenCalledWith({
|
||||
id: mockUser.id,
|
||||
defaultWorkspace: mockWorkspace,
|
||||
});
|
||||
expect(userService.saveDefaultWorkspace).toHaveBeenCalledWith(
|
||||
mockUser.id,
|
||||
mockWorkspace.id,
|
||||
);
|
||||
expect(accessTokenService.generateAccessToken).toHaveBeenCalledWith(
|
||||
mockUser.id,
|
||||
mockWorkspace.id,
|
||||
|
||||
@ -7,12 +7,13 @@ import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { AuthTokens } from 'src/engine/core-modules/auth/dto/token.entity';
|
||||
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
||||
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
|
||||
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthTokens } from 'src/engine/core-modules/auth/dto/token.entity';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { getAuthProvidersByWorkspace } from 'src/engine/core-modules/workspace/utils/getAuthProvidersByWorkspace';
|
||||
|
||||
@Injectable()
|
||||
export class SwitchWorkspaceService {
|
||||
@ -21,7 +22,7 @@ export class SwitchWorkspaceService {
|
||||
private readonly userRepository: Repository<User>,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly ssoService: SSOService,
|
||||
private readonly userService: UserService,
|
||||
private readonly accessTokenService: AccessTokenService,
|
||||
private readonly refreshTokenService: RefreshTokenService,
|
||||
) {}
|
||||
@ -59,31 +60,17 @@ export class SwitchWorkspaceService {
|
||||
);
|
||||
}
|
||||
|
||||
if (workspace.workspaceSSOIdentityProviders.length > 0) {
|
||||
return {
|
||||
useSSOAuth: true,
|
||||
workspace,
|
||||
availableSSOIdentityProviders:
|
||||
await this.ssoService.listSSOIdentityProvidersByWorkspaceId(
|
||||
workspaceId,
|
||||
),
|
||||
} as {
|
||||
useSSOAuth: true;
|
||||
workspace: Workspace;
|
||||
availableSSOIdentityProviders: Awaited<
|
||||
ReturnType<
|
||||
typeof this.ssoService.listSSOIdentityProvidersByWorkspaceId
|
||||
>
|
||||
>;
|
||||
};
|
||||
}
|
||||
await this.userRepository.save({
|
||||
id: user.id,
|
||||
defaultWorkspace: workspace,
|
||||
});
|
||||
|
||||
return {
|
||||
useSSOAuth: false,
|
||||
workspace,
|
||||
} as {
|
||||
useSSOAuth: false;
|
||||
workspace: Workspace;
|
||||
id: workspace.id,
|
||||
subdomain: workspace.subdomain,
|
||||
logo: workspace.logo,
|
||||
displayName: workspace.displayName,
|
||||
authProviders: getAuthProvidersByWorkspace(workspace),
|
||||
};
|
||||
}
|
||||
|
||||
@ -91,10 +78,7 @@ export class SwitchWorkspaceService {
|
||||
user: User,
|
||||
workspace: Workspace,
|
||||
): Promise<AuthTokens> {
|
||||
await this.userRepository.save({
|
||||
id: user.id,
|
||||
defaultWorkspace: workspace,
|
||||
});
|
||||
await this.userService.saveDefaultWorkspace(user.id, workspace.id);
|
||||
|
||||
const token = await this.accessTokenService.generateAccessToken(
|
||||
user.id,
|
||||
|
||||
@ -17,6 +17,7 @@ export type GoogleRequest = Omit<
|
||||
picture: string | null;
|
||||
workspaceInviteHash?: string;
|
||||
workspacePersonalInviteToken?: string;
|
||||
targetWorkspaceSubdomain?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@ -37,6 +38,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
|
||||
...options,
|
||||
state: JSON.stringify({
|
||||
workspaceInviteHash: req.params.workspaceInviteHash,
|
||||
workspaceSubdomain: req.params.workspaceSubdomain,
|
||||
...(req.params.workspacePersonalInviteToken
|
||||
? {
|
||||
workspacePersonalInviteToken:
|
||||
@ -69,6 +71,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
|
||||
picture: photos?.[0]?.value,
|
||||
workspaceInviteHash: state.workspaceInviteHash,
|
||||
workspacePersonalInviteToken: state.workspacePersonalInviteToken,
|
||||
targetWorkspaceSubdomain: state.workspaceSubdomain,
|
||||
};
|
||||
|
||||
done(null, user);
|
||||
|
||||
@ -21,6 +21,7 @@ export type MicrosoftRequest = Omit<
|
||||
picture: string | null;
|
||||
workspaceInviteHash?: string;
|
||||
workspacePersonalInviteToken?: string;
|
||||
targetWorkspaceSubdomain?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@ -41,6 +42,7 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy, 'microsoft') {
|
||||
...options,
|
||||
state: JSON.stringify({
|
||||
workspaceInviteHash: req.params.workspaceInviteHash,
|
||||
workspaceSubdomain: req.params.workspaceSubdomain,
|
||||
...(req.params.workspacePersonalInviteToken
|
||||
? {
|
||||
workspacePersonalInviteToken:
|
||||
@ -83,6 +85,7 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy, 'microsoft') {
|
||||
picture: photos?.[0]?.value,
|
||||
workspaceInviteHash: state.workspaceInviteHash,
|
||||
workspacePersonalInviteToken: state.workspacePersonalInviteToken,
|
||||
targetWorkspaceSubdomain: state.workspaceSubdomain,
|
||||
};
|
||||
|
||||
done(null, user);
|
||||
|
||||
@ -69,30 +69,34 @@ export class SamlAuthStrategy extends PassportStrategy(
|
||||
}
|
||||
|
||||
validate: VerifyWithRequest = async (request, profile, done) => {
|
||||
if (!profile) {
|
||||
return done(new Error('Profile is must be provided'));
|
||||
try {
|
||||
if (!profile) {
|
||||
return done(new Error('Profile is must be provided'));
|
||||
}
|
||||
|
||||
const email = profile.email ?? profile.mail ?? profile.nameID;
|
||||
|
||||
if (!isEmail(email)) {
|
||||
return done(new Error('Invalid email'));
|
||||
}
|
||||
|
||||
const result: {
|
||||
user: Record<string, string>;
|
||||
identityProviderId?: string;
|
||||
} = { user: { email } };
|
||||
|
||||
if (
|
||||
'RelayState' in request.body &&
|
||||
typeof request.body.RelayState === 'string'
|
||||
) {
|
||||
const RelayState = JSON.parse(request.body.RelayState);
|
||||
|
||||
result.identityProviderId = RelayState.identityProviderId;
|
||||
}
|
||||
|
||||
done(null, result);
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
|
||||
const email = profile.email ?? profile.mail ?? profile.nameID;
|
||||
|
||||
if (!isEmail(email)) {
|
||||
return done(new Error('Invalid email'));
|
||||
}
|
||||
|
||||
const result: {
|
||||
user: Record<string, string>;
|
||||
identityProviderId?: string;
|
||||
} = { user: { email } };
|
||||
|
||||
if (
|
||||
'RelayState' in request.body &&
|
||||
typeof request.body.RelayState === 'string'
|
||||
) {
|
||||
const RelayState = JSON.parse(request.body.RelayState);
|
||||
|
||||
result.identityProviderId = RelayState.identityProviderId;
|
||||
}
|
||||
|
||||
done(null, result);
|
||||
};
|
||||
}
|
||||
|
||||
@ -166,7 +166,7 @@ describe('AccessTokenService', () => {
|
||||
.spyOn(service['jwtStrategy'], 'validate')
|
||||
.mockReturnValue(mockAuthContext as any);
|
||||
|
||||
const result = await service.validateToken(mockRequest);
|
||||
const result = await service.validateTokenByRequest(mockRequest);
|
||||
|
||||
expect(result).toEqual(mockAuthContext);
|
||||
expect(jwtWrapperService.verifyWorkspaceToken).toHaveBeenCalledWith(
|
||||
@ -184,7 +184,7 @@ describe('AccessTokenService', () => {
|
||||
headers: {},
|
||||
} as Request;
|
||||
|
||||
await expect(service.validateToken(mockRequest)).rejects.toThrow(
|
||||
await expect(service.validateTokenByRequest(mockRequest)).rejects.toThrow(
|
||||
AuthException,
|
||||
);
|
||||
});
|
||||
|
||||
@ -116,16 +116,7 @@ export class AccessTokenService {
|
||||
};
|
||||
}
|
||||
|
||||
async validateToken(request: Request): Promise<AuthContext> {
|
||||
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
|
||||
|
||||
if (!token) {
|
||||
throw new AuthException(
|
||||
'missing authentication token',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
async validateToken(token: string): Promise<AuthContext> {
|
||||
await this.jwtWrapperService.verifyWorkspaceToken(token, 'ACCESS');
|
||||
|
||||
const decoded = await this.jwtWrapperService.decode(token);
|
||||
@ -135,4 +126,17 @@ export class AccessTokenService {
|
||||
|
||||
return { user, apiKey, workspace, workspaceMemberId };
|
||||
}
|
||||
|
||||
async validateTokenByRequest(request: Request): Promise<AuthContext> {
|
||||
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
|
||||
|
||||
if (!token) {
|
||||
throw new AuthException(
|
||||
'missing authentication token',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
return this.validateToken(token);
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,7 +94,7 @@ describe('LoginTokenService', () => {
|
||||
|
||||
const result = await service.verifyLoginToken(mockToken);
|
||||
|
||||
expect(result).toEqual(mockEmail);
|
||||
expect(result).toEqual({ sub: mockEmail });
|
||||
expect(jwtWrapperService.verifyWorkspaceToken).toHaveBeenCalledWith(
|
||||
mockToken,
|
||||
'LOGIN',
|
||||
|
||||
@ -20,6 +20,7 @@ export class LoginTokenService {
|
||||
|
||||
async generateLoginToken(email: string): Promise<AuthToken> {
|
||||
const secret = this.jwtWrapperService.generateAppSecret('LOGIN');
|
||||
|
||||
const expiresIn = this.environmentService.get('LOGIN_TOKEN_EXPIRES_IN');
|
||||
|
||||
if (!expiresIn) {
|
||||
@ -43,11 +44,11 @@ export class LoginTokenService {
|
||||
};
|
||||
}
|
||||
|
||||
async verifyLoginToken(loginToken: string): Promise<string> {
|
||||
async verifyLoginToken(loginToken: string): Promise<{ sub: string }> {
|
||||
await this.jwtWrapperService.verifyWorkspaceToken(loginToken, 'LOGIN');
|
||||
|
||||
return this.jwtWrapperService.decode(loginToken, {
|
||||
json: true,
|
||||
}).sub;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user