refacto(*): remove everything about default workspace (#9157)
## Summary - [x] Remove defaultWorkspace in user - [x] Remove all occurrence of defaultWorkspace and defaultWorkspaceId - [x] Improve activate workspace flow - [x] Improve security on social login - [x] Add `ImpersonateGuard` - [x] Allow to use impersonation with couple `User/Workspace` - [x] Prevent unexpected reload on activate workspace - [x] Scope login token with workspaceId Fix https://github.com/twentyhq/twenty/issues/9033#event-15714863042
This commit is contained in:
@ -6,49 +6,41 @@ import { ImpersonateInput } from 'src/engine/core-modules/admin-panel/dtos/imper
|
||||
import { UpdateWorkspaceFeatureFlagInput } from 'src/engine/core-modules/admin-panel/dtos/update-workspace-feature-flag.input';
|
||||
import { UserLookup } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.entity';
|
||||
import { UserLookupInput } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.input';
|
||||
import { Verify } from 'src/engine/core-modules/auth/dto/verify.entity';
|
||||
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { ImpersonateGuard } from 'src/engine/guards/impersonate-guard';
|
||||
import { ImpersonateOutput } from 'src/engine/core-modules/admin-panel/dtos/impersonate.output';
|
||||
|
||||
@Resolver()
|
||||
@UseFilters(AuthGraphqlApiExceptionFilter)
|
||||
export class AdminPanelResolver {
|
||||
constructor(private adminService: AdminPanelService) {}
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
||||
@Mutation(() => Verify)
|
||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, ImpersonateGuard)
|
||||
@Mutation(() => ImpersonateOutput)
|
||||
async impersonate(
|
||||
@Args() impersonateInput: ImpersonateInput,
|
||||
@AuthUser() user: User,
|
||||
): Promise<Verify> {
|
||||
return await this.adminService.impersonate(impersonateInput.userId, user);
|
||||
@Args() { workspaceId, userId }: ImpersonateInput,
|
||||
): Promise<ImpersonateOutput> {
|
||||
return await this.adminService.impersonate(userId, workspaceId);
|
||||
}
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, ImpersonateGuard)
|
||||
@Mutation(() => UserLookup)
|
||||
async userLookupAdminPanel(
|
||||
@Args() userLookupInput: UserLookupInput,
|
||||
@AuthUser() user: User,
|
||||
): Promise<UserLookup> {
|
||||
return await this.adminService.userLookup(
|
||||
userLookupInput.userIdentifier,
|
||||
user,
|
||||
);
|
||||
return await this.adminService.userLookup(userLookupInput.userIdentifier);
|
||||
}
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, ImpersonateGuard)
|
||||
@Mutation(() => Boolean)
|
||||
async updateWorkspaceFeatureFlag(
|
||||
@Args() updateFlagInput: UpdateWorkspaceFeatureFlagInput,
|
||||
@AuthUser() user: User,
|
||||
): Promise<boolean> {
|
||||
await this.adminService.updateWorkspaceFeatureFlags(
|
||||
updateFlagInput.workspaceId,
|
||||
updateFlagInput.featureFlag,
|
||||
user,
|
||||
updateFlagInput.value,
|
||||
);
|
||||
|
||||
|
||||
@ -8,18 +8,18 @@ import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} 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 { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
|
||||
@Injectable()
|
||||
export class AdminPanelService {
|
||||
constructor(
|
||||
private readonly accessTokenService: AccessTokenService,
|
||||
private readonly refreshTokenService: RefreshTokenService,
|
||||
private readonly loginTokenService: LoginTokenService,
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
@ -28,64 +28,48 @@ export class AdminPanelService {
|
||||
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
|
||||
) {}
|
||||
|
||||
async impersonate(userIdentifier: string, userImpersonating: User) {
|
||||
if (!userImpersonating.canImpersonate) {
|
||||
throw new AuthException(
|
||||
'User cannot impersonate',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
const isEmail = userIdentifier.includes('@');
|
||||
|
||||
async impersonate(userId: string, workspaceId: string) {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: isEmail ? { email: userIdentifier } : { id: userIdentifier },
|
||||
relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'],
|
||||
where: {
|
||||
id: userId,
|
||||
workspaces: {
|
||||
workspaceId,
|
||||
workspace: {
|
||||
allowImpersonation: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: ['workspaces', 'workspaces.workspace'],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new AuthException(
|
||||
'User not found',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
userValidator.assertIsDefinedOrThrow(
|
||||
user,
|
||||
new AuthException('User not found', AuthExceptionCode.INVALID_INPUT),
|
||||
);
|
||||
|
||||
if (!user.defaultWorkspace.allowImpersonation) {
|
||||
throw new AuthException(
|
||||
workspaceValidator.assertIsDefinedOrThrow(
|
||||
user.workspaces[0].workspace,
|
||||
new AuthException(
|
||||
'Impersonation not allowed',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
const accessToken = await this.accessTokenService.generateAccessToken(
|
||||
user.id,
|
||||
user.defaultWorkspaceId,
|
||||
),
|
||||
);
|
||||
const refreshToken = await this.refreshTokenService.generateRefreshToken(
|
||||
user.id,
|
||||
user.defaultWorkspaceId,
|
||||
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
user.workspaces[0].workspace.id,
|
||||
);
|
||||
|
||||
return {
|
||||
user,
|
||||
tokens: {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
workspace: {
|
||||
id: user.workspaces[0].workspace.id,
|
||||
subdomain: user.workspaces[0].workspace.subdomain,
|
||||
},
|
||||
loginToken,
|
||||
};
|
||||
}
|
||||
|
||||
async userLookup(
|
||||
userIdentifier: string,
|
||||
userImpersonating: User,
|
||||
): Promise<UserLookup> {
|
||||
if (!userImpersonating.canImpersonate) {
|
||||
throw new AuthException(
|
||||
'User cannot access user info',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
async userLookup(userIdentifier: string): Promise<UserLookup> {
|
||||
const isEmail = userIdentifier.includes('@');
|
||||
|
||||
const targetUser = await this.userRepository.findOne({
|
||||
@ -99,12 +83,10 @@ export class AdminPanelService {
|
||||
],
|
||||
});
|
||||
|
||||
if (!targetUser) {
|
||||
throw new AuthException(
|
||||
'User not found',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
userValidator.assertIsDefinedOrThrow(
|
||||
targetUser,
|
||||
new AuthException('User not found', AuthExceptionCode.INVALID_INPUT),
|
||||
);
|
||||
|
||||
const allFeatureFlagKeys = Object.values(FeatureFlagKey);
|
||||
|
||||
@ -120,6 +102,7 @@ export class AdminPanelService {
|
||||
name: userWorkspace.workspace.displayName ?? '',
|
||||
totalUsers: userWorkspace.workspace.workspaceUsers.length,
|
||||
logo: userWorkspace.workspace.logo,
|
||||
allowImpersonation: userWorkspace.workspace.allowImpersonation,
|
||||
users: userWorkspace.workspace.workspaceUsers.map((workspaceUser) => ({
|
||||
id: workspaceUser.user.id,
|
||||
email: workspaceUser.user.email,
|
||||
@ -140,27 +123,17 @@ export class AdminPanelService {
|
||||
async updateWorkspaceFeatureFlags(
|
||||
workspaceId: string,
|
||||
featureFlag: FeatureFlagKey,
|
||||
userImpersonating: User,
|
||||
value: boolean,
|
||||
) {
|
||||
if (!userImpersonating.canImpersonate) {
|
||||
throw new AuthException(
|
||||
'User cannot update feature flags',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
const workspace = await this.workspaceRepository.findOne({
|
||||
where: { id: workspaceId },
|
||||
relations: ['featureFlags'],
|
||||
});
|
||||
|
||||
if (!workspace) {
|
||||
throw new AuthException(
|
||||
'Workspace not found',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
workspaceValidator.assertIsDefinedOrThrow(
|
||||
workspace,
|
||||
new AuthException('Workspace not found', AuthExceptionCode.INVALID_INPUT),
|
||||
);
|
||||
|
||||
const existingFlag = workspace.featureFlags?.find(
|
||||
(flag) => flag.key === featureFlag,
|
||||
|
||||
@ -8,4 +8,9 @@ export class ImpersonateInput {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
userId: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { AuthToken } from 'src/engine/core-modules/auth/dto/token.entity';
|
||||
import { WorkspaceSubdomainAndId } from 'src/engine/core-modules/workspace/dtos/workspace-subdomain-id.dto';
|
||||
|
||||
@ObjectType()
|
||||
export class ImpersonateOutput {
|
||||
@Field(() => AuthToken)
|
||||
loginToken: AuthToken;
|
||||
|
||||
@Field(() => WorkspaceSubdomainAndId)
|
||||
workspace: WorkspaceSubdomainAndId;
|
||||
}
|
||||
@ -25,6 +25,9 @@ class WorkspaceInfo {
|
||||
@Field(() => String)
|
||||
name: string;
|
||||
|
||||
@Field(() => Boolean)
|
||||
allowImpersonation: boolean;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
logo?: string;
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ import { SSOAuthController } from 'src/engine/core-modules/auth/controllers/sso-
|
||||
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';
|
||||
import { OAuthService } from 'src/engine/core-modules/auth/services/oauth.service';
|
||||
// import { OAuthService } from 'src/engine/core-modules/auth/services/oauth.service';
|
||||
import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service';
|
||||
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
|
||||
import { SwitchWorkspaceService } from 'src/engine/core-modules/auth/services/switch-workspace.service';
|
||||
@ -103,7 +103,8 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
||||
SwitchWorkspaceService,
|
||||
TransientTokenService,
|
||||
ApiKeyService,
|
||||
OAuthService,
|
||||
// reenable when working on: https://github.com/twentyhq/twenty/issues/9143
|
||||
// OAuthService,
|
||||
],
|
||||
exports: [AccessTokenService, LoginTokenService, RefreshTokenService],
|
||||
})
|
||||
|
||||
@ -13,7 +13,7 @@ import { AuthResolver } from './auth.resolver';
|
||||
|
||||
import { ApiKeyService } from './services/api-key.service';
|
||||
import { AuthService } from './services/auth.service';
|
||||
import { OAuthService } from './services/oauth.service';
|
||||
// import { OAuthService } from './services/oauth.service';
|
||||
import { ResetPasswordService } from './services/reset-password.service';
|
||||
import { SwitchWorkspaceService } from './services/switch-workspace.service';
|
||||
import { LoginTokenService } from './token/services/login-token.service';
|
||||
@ -80,10 +80,10 @@ describe('AuthResolver', () => {
|
||||
provide: TransientTokenService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: OAuthService,
|
||||
useValue: {},
|
||||
},
|
||||
// {
|
||||
// provide: OAuthService,
|
||||
// useValue: {},
|
||||
// },
|
||||
],
|
||||
})
|
||||
.overrideGuard(CaptchaGuard)
|
||||
|
||||
@ -7,8 +7,6 @@ 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 { EmailPasswordResetLink } from 'src/engine/core-modules/auth/dto/email-password-reset-link.entity';
|
||||
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 { 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';
|
||||
@ -16,7 +14,7 @@ import { ValidatePasswordResetToken } from 'src/engine/core-modules/auth/dto/val
|
||||
import { ValidatePasswordResetTokenInput } from 'src/engine/core-modules/auth/dto/validate-password-reset-token.input';
|
||||
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
|
||||
import { ApiKeyService } from 'src/engine/core-modules/auth/services/api-key.service';
|
||||
import { OAuthService } from 'src/engine/core-modules/auth/services/oauth.service';
|
||||
// import { OAuthService } from 'src/engine/core-modules/auth/services/oauth.service';
|
||||
import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service';
|
||||
import { SwitchWorkspaceService } from 'src/engine/core-modules/auth/services/switch-workspace.service';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
@ -31,7 +29,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat
|
||||
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 { PublicWorkspaceDataOutput } from 'src/engine/core-modules/workspace/dtos/public-workspace-data-output';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
@ -39,6 +37,8 @@ import {
|
||||
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
|
||||
import { AvailableWorkspaceOutput } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
import { SignUpOutput } from 'src/engine/core-modules/auth/dto/sign-up.output';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
|
||||
import { ChallengeInput } from './dto/challenge.input';
|
||||
import { LoginToken } from './dto/login-token.entity';
|
||||
@ -46,7 +46,6 @@ import { SignUpInput } from './dto/sign-up.input';
|
||||
import { ApiKeyToken, AuthTokens } from './dto/token.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';
|
||||
import { WorkspaceInviteHashValid } from './dto/workspace-invite-hash-valid.entity';
|
||||
import { WorkspaceInviteHashValidInput } from './dto/workspace-invite-hash.input';
|
||||
@ -64,7 +63,7 @@ export class AuthResolver {
|
||||
private loginTokenService: LoginTokenService,
|
||||
private switchWorkspaceService: SwitchWorkspaceService,
|
||||
private transientTokenService: TransientTokenService,
|
||||
private oauthService: OAuthService,
|
||||
// private oauthService: OAuthService,
|
||||
private domainManagerService: DomainManagerService,
|
||||
) {}
|
||||
|
||||
@ -101,7 +100,9 @@ export class AuthResolver {
|
||||
@OriginHeader() origin: string,
|
||||
): Promise<LoginToken> {
|
||||
const workspace =
|
||||
await this.domainManagerService.getWorkspaceByOrigin(origin);
|
||||
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
|
||||
origin,
|
||||
);
|
||||
|
||||
if (!workspace) {
|
||||
throw new AuthException(
|
||||
@ -112,18 +113,19 @@ export class AuthResolver {
|
||||
const user = await this.authService.challenge(challengeInput, workspace);
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
return { loginToken };
|
||||
}
|
||||
|
||||
@UseGuards(CaptchaGuard)
|
||||
@Mutation(() => LoginToken)
|
||||
@Mutation(() => SignUpOutput)
|
||||
async signUp(
|
||||
@Args() signUpInput: SignUpInput,
|
||||
@OriginHeader() origin: string,
|
||||
): Promise<LoginToken> {
|
||||
const user = await this.authService.signInUp({
|
||||
): Promise<SignUpOutput> {
|
||||
const { user, workspace } = await this.authService.signInUp({
|
||||
...signUpInput,
|
||||
targetWorkspaceSubdomain:
|
||||
this.domainManagerService.getWorkspaceSubdomainByOrigin(origin),
|
||||
@ -133,19 +135,26 @@ export class AuthResolver {
|
||||
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
return { loginToken };
|
||||
return {
|
||||
loginToken,
|
||||
workspace: {
|
||||
id: workspace.id,
|
||||
subdomain: workspace.subdomain,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Mutation(() => ExchangeAuthCode)
|
||||
async exchangeAuthorizationCode(
|
||||
@Args() exchangeAuthCodeInput: ExchangeAuthCodeInput,
|
||||
) {
|
||||
return await this.oauthService.verifyAuthorizationCode(
|
||||
exchangeAuthCodeInput,
|
||||
);
|
||||
}
|
||||
// @Mutation(() => ExchangeAuthCode)
|
||||
// async exchangeAuthorizationCode(
|
||||
// @Args() exchangeAuthCodeInput: ExchangeAuthCodeInput,
|
||||
// ) {
|
||||
// return await this.oauthService.verifyAuthorizationCode(
|
||||
// exchangeAuthCodeInput,
|
||||
// );
|
||||
// }
|
||||
|
||||
@Mutation(() => TransientToken)
|
||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
||||
@ -165,25 +174,35 @@ export class AuthResolver {
|
||||
await this.transientTokenService.generateTransientToken(
|
||||
workspaceMember.id,
|
||||
user.id,
|
||||
user.defaultWorkspaceId,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
return { transientToken };
|
||||
}
|
||||
|
||||
@Mutation(() => Verify)
|
||||
@Mutation(() => AuthTokens)
|
||||
async verify(
|
||||
@Args() verifyInput: VerifyInput,
|
||||
@OriginHeader() origin: string,
|
||||
): Promise<Verify> {
|
||||
): Promise<AuthTokens> {
|
||||
const workspace =
|
||||
await this.domainManagerService.getWorkspaceByOrigin(origin);
|
||||
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
|
||||
origin,
|
||||
);
|
||||
|
||||
const { sub: email } = await this.loginTokenService.verifyLoginToken(
|
||||
verifyInput.loginToken,
|
||||
);
|
||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||
|
||||
return await this.authService.verify(email, workspace?.id);
|
||||
const { sub: email, workspaceId } =
|
||||
await this.loginTokenService.verifyLoginToken(verifyInput.loginToken);
|
||||
|
||||
if (workspaceId !== workspace.id) {
|
||||
throw new AuthException(
|
||||
'Token is not valid for this workspace',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
return await this.authService.verify(email, workspace.id);
|
||||
}
|
||||
|
||||
@Mutation(() => AuthorizeApp)
|
||||
@ -191,10 +210,12 @@ export class AuthResolver {
|
||||
async authorizeApp(
|
||||
@Args() authorizeAppInput: AuthorizeAppInput,
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<AuthorizeApp> {
|
||||
return await this.authService.generateAuthorizationCode(
|
||||
authorizeAppInput,
|
||||
user,
|
||||
workspace,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -71,8 +71,9 @@ export class GoogleAuthController {
|
||||
|
||||
if (
|
||||
this.environmentService.get('IS_MULTIWORKSPACE_ENABLED') &&
|
||||
targetWorkspaceSubdomain ===
|
||||
this.environmentService.get('DEFAULT_SUBDOMAIN')
|
||||
(targetWorkspaceSubdomain ===
|
||||
this.environmentService.get('DEFAULT_SUBDOMAIN') ||
|
||||
!targetWorkspaceSubdomain)
|
||||
) {
|
||||
const workspaceWithGoogleAuthActive =
|
||||
await this.workspaceRepository.findOne({
|
||||
@ -84,7 +85,7 @@ export class GoogleAuthController {
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: ['userWorkspaces', 'userWorkspaces.user'],
|
||||
relations: ['workspaceUsers', 'workspaceUsers.user'],
|
||||
});
|
||||
|
||||
if (workspaceWithGoogleAuthActive) {
|
||||
@ -93,16 +94,18 @@ export class GoogleAuthController {
|
||||
}
|
||||
}
|
||||
|
||||
const user = await this.authService.signInUp(signInUpParams);
|
||||
const { user, workspace } =
|
||||
await this.authService.signInUp(signInUpParams);
|
||||
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
return res.redirect(
|
||||
await this.authService.computeRedirectURI(
|
||||
this.authService.computeRedirectURI(
|
||||
loginToken.token,
|
||||
user.defaultWorkspace.subdomain,
|
||||
workspace.subdomain,
|
||||
),
|
||||
);
|
||||
} catch (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 { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
|
||||
import { MicrosoftOAuthGuard } from 'src/engine/core-modules/auth/guards/microsoft-oauth.guard';
|
||||
@ -18,6 +20,7 @@ import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/l
|
||||
import { AuthException } 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 { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@Controller('auth/microsoft')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
@ -27,6 +30,8 @@ export class MicrosoftAuthController {
|
||||
private readonly authService: AuthService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@ -43,43 +48,57 @@ export class MicrosoftAuthController {
|
||||
@Res() res: Response,
|
||||
) {
|
||||
try {
|
||||
const {
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
picture,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
targetWorkspaceSubdomain,
|
||||
} = req.user;
|
||||
const signInUpParams = req.user;
|
||||
|
||||
const user = await this.authService.signInUp({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
picture,
|
||||
workspaceInviteHash,
|
||||
workspacePersonalInviteToken,
|
||||
targetWorkspaceSubdomain,
|
||||
if (
|
||||
this.environmentService.get('IS_MULTIWORKSPACE_ENABLED') &&
|
||||
(signInUpParams.targetWorkspaceSubdomain ===
|
||||
this.environmentService.get('DEFAULT_SUBDOMAIN') ||
|
||||
!signInUpParams.targetWorkspaceSubdomain)
|
||||
) {
|
||||
const workspaceWithGoogleAuthActive =
|
||||
await this.workspaceRepository.findOne({
|
||||
where: {
|
||||
isMicrosoftAuthEnabled: true,
|
||||
workspaceUsers: {
|
||||
user: {
|
||||
email: signInUpParams.email,
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: ['userWorkspaces', 'userWorkspaces.user'],
|
||||
});
|
||||
|
||||
if (workspaceWithGoogleAuthActive) {
|
||||
signInUpParams.targetWorkspaceSubdomain =
|
||||
workspaceWithGoogleAuthActive.subdomain;
|
||||
}
|
||||
}
|
||||
|
||||
const { user, workspace } = await this.authService.signInUp({
|
||||
...signInUpParams,
|
||||
fromSSO: true,
|
||||
authProvider: 'microsoft',
|
||||
});
|
||||
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
return res.redirect(
|
||||
await this.authService.computeRedirectURI(
|
||||
this.authService.computeRedirectURI(
|
||||
loginToken.token,
|
||||
user.defaultWorkspace.subdomain,
|
||||
workspace.subdomain,
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof AuthException) {
|
||||
return res.redirect(
|
||||
this.domainManagerService.computeRedirectErrorUrl({
|
||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
subdomain:
|
||||
req.user.targetWorkspaceSubdomain ??
|
||||
this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||
errorMessage: err.message,
|
||||
}),
|
||||
);
|
||||
|
||||
@ -86,7 +86,7 @@ export class SSOAuthController {
|
||||
);
|
||||
|
||||
return res.redirect(
|
||||
await this.authService.computeRedirectURI(
|
||||
this.authService.computeRedirectURI(
|
||||
loginToken.token,
|
||||
identityProvider.workspace.subdomain,
|
||||
),
|
||||
@ -113,7 +113,7 @@ export class SSOAuthController {
|
||||
);
|
||||
|
||||
return res.redirect(
|
||||
await this.authService.computeRedirectURI(
|
||||
this.authService.computeRedirectURI(
|
||||
loginToken.token,
|
||||
identityProvider.workspace.subdomain,
|
||||
),
|
||||
@ -183,7 +183,10 @@ export class SSOAuthController {
|
||||
|
||||
return {
|
||||
identityProvider,
|
||||
loginToken: await this.loginTokenService.generateLoginToken(user.email),
|
||||
loginToken: await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
identityProvider.workspace.id,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { WorkspaceSubdomainAndId } from 'src/engine/core-modules/workspace/dtos/workspace-subdomain-id.dto';
|
||||
|
||||
import { AuthToken } from './token.entity';
|
||||
|
||||
@ObjectType()
|
||||
export class SignUpOutput {
|
||||
@Field(() => AuthToken)
|
||||
loginToken: AuthToken;
|
||||
|
||||
@Field(() => WorkspaceSubdomainAndId)
|
||||
workspace: WorkspaceSubdomainAndId;
|
||||
}
|
||||
@ -7,9 +7,6 @@ export class UserExists {
|
||||
@Field(() => Boolean)
|
||||
exists: true;
|
||||
|
||||
@Field(() => String)
|
||||
defaultWorkspaceId: string;
|
||||
|
||||
@Field(() => [AvailableWorkspaceOutput])
|
||||
availableWorkspaces: Array<AvailableWorkspaceOutput>;
|
||||
}
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
|
||||
import { AuthTokens } from './token.entity';
|
||||
|
||||
@ObjectType()
|
||||
export class Verify extends AuthTokens {
|
||||
@Field(() => User)
|
||||
user: DeepPartial<User>;
|
||||
}
|
||||
@ -33,7 +33,6 @@ 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';
|
||||
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
||||
@ -42,12 +41,12 @@ import { DomainManagerService } from 'src/engine/core-modules/domain-manager/ser
|
||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
||||
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
|
||||
import { WorkspaceAuthProvider } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthTokens } from 'src/engine/core-modules/auth/dto/token.entity';
|
||||
|
||||
@Injectable()
|
||||
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
||||
@ -57,7 +56,6 @@ export class AuthService {
|
||||
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')
|
||||
@ -188,7 +186,7 @@ export class AuthService {
|
||||
});
|
||||
}
|
||||
|
||||
async verify(email: string, workspaceId?: string): Promise<Verify> {
|
||||
async verify(email: string, workspaceId: string): Promise<AuthTokens> {
|
||||
if (!email) {
|
||||
throw new AuthException(
|
||||
'Email is required',
|
||||
@ -196,31 +194,8 @@ export class AuthService {
|
||||
);
|
||||
}
|
||||
|
||||
const userWithIdAndDefaultWorkspaceId = await this.userRepository.findOne({
|
||||
select: ['defaultWorkspaceId', 'id'],
|
||||
where: { email },
|
||||
});
|
||||
|
||||
userValidator.assertIsDefinedOrThrow(
|
||||
userWithIdAndDefaultWorkspaceId,
|
||||
new AuthException('User not found', AuthExceptionCode.USER_NOT_FOUND),
|
||||
);
|
||||
|
||||
if (
|
||||
workspaceId &&
|
||||
userWithIdAndDefaultWorkspaceId.defaultWorkspaceId !== workspaceId
|
||||
) {
|
||||
await this.userService.saveDefaultWorkspaceIfUserHasAccessOrThrow(
|
||||
userWithIdAndDefaultWorkspaceId.id,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'],
|
||||
where: { email },
|
||||
});
|
||||
|
||||
userValidator.assertIsDefinedOrThrow(
|
||||
@ -233,15 +208,14 @@ export class AuthService {
|
||||
|
||||
const accessToken = await this.accessTokenService.generateAccessToken(
|
||||
user.id,
|
||||
user.defaultWorkspaceId,
|
||||
workspaceId,
|
||||
);
|
||||
const refreshToken = await this.refreshTokenService.generateRefreshToken(
|
||||
user.id,
|
||||
user.defaultWorkspaceId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return {
|
||||
user,
|
||||
tokens: {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
@ -257,7 +231,6 @@ export class AuthService {
|
||||
if (userValidator.isDefined(user)) {
|
||||
return {
|
||||
exists: true,
|
||||
defaultWorkspaceId: user.defaultWorkspaceId,
|
||||
availableWorkspaces: await this.findAvailableWorkspacesByEmail(email),
|
||||
};
|
||||
}
|
||||
@ -278,6 +251,7 @@ export class AuthService {
|
||||
async generateAuthorizationCode(
|
||||
authorizeAppInput: AuthorizeAppInput,
|
||||
user: User,
|
||||
workspace: Workspace,
|
||||
): Promise<AuthorizeApp> {
|
||||
// TODO: replace with db call to - third party app table
|
||||
const apps = [
|
||||
@ -329,14 +303,14 @@ export class AuthService {
|
||||
value: codeChallenge,
|
||||
type: AppTokenType.CodeChallenge,
|
||||
userId: user.id,
|
||||
workspaceId: user.defaultWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
expiresAt,
|
||||
},
|
||||
{
|
||||
value: authorizationCode,
|
||||
type: AppTokenType.AuthorizationCode,
|
||||
userId: user.id,
|
||||
workspaceId: user.defaultWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
expiresAt,
|
||||
},
|
||||
]);
|
||||
@ -347,7 +321,7 @@ export class AuthService {
|
||||
value: authorizationCode,
|
||||
type: AppTokenType.AuthorizationCode,
|
||||
userId: user.id,
|
||||
workspaceId: user.defaultWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
expiresAt,
|
||||
});
|
||||
|
||||
@ -439,7 +413,7 @@ export class AuthService {
|
||||
return workspace;
|
||||
}
|
||||
|
||||
async computeRedirectURI(loginToken: string, subdomain?: string) {
|
||||
computeRedirectURI(loginToken: string, subdomain?: string) {
|
||||
const url = this.domainManagerService.buildWorkspaceURL({
|
||||
subdomain,
|
||||
pathname: '/verify',
|
||||
|
||||
@ -1,155 +1,157 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
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 { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
||||
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 { User } from 'src/engine/core-modules/user/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class OAuthService {
|
||||
constructor(
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
@InjectRepository(AppToken, 'core')
|
||||
private readonly appTokenRepository: Repository<AppToken>,
|
||||
private readonly accessTokenService: AccessTokenService,
|
||||
private readonly refreshTokenService: RefreshTokenService,
|
||||
private readonly loginTokenService: LoginTokenService,
|
||||
) {}
|
||||
|
||||
async verifyAuthorizationCode(
|
||||
exchangeAuthCodeInput: ExchangeAuthCodeInput,
|
||||
): Promise<ExchangeAuthCode> {
|
||||
const { authorizationCode, codeVerifier } = exchangeAuthCodeInput;
|
||||
|
||||
if (!authorizationCode) {
|
||||
throw new AuthException(
|
||||
'Authorization code not found',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
let userId = '';
|
||||
|
||||
if (codeVerifier) {
|
||||
const authorizationCodeAppToken = await this.appTokenRepository.findOne({
|
||||
where: {
|
||||
value: authorizationCode,
|
||||
},
|
||||
});
|
||||
|
||||
if (!authorizationCodeAppToken) {
|
||||
throw new AuthException(
|
||||
'Authorization code does not exist',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
if (!(authorizationCodeAppToken.expiresAt.getTime() >= Date.now())) {
|
||||
throw new AuthException(
|
||||
'Authorization code expired.',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
const codeChallenge = crypto
|
||||
.createHash('sha256')
|
||||
.update(codeVerifier)
|
||||
.digest()
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '');
|
||||
|
||||
const codeChallengeAppToken = await this.appTokenRepository.findOne({
|
||||
where: {
|
||||
value: codeChallenge,
|
||||
},
|
||||
});
|
||||
|
||||
if (!codeChallengeAppToken || !codeChallengeAppToken.userId) {
|
||||
throw new AuthException(
|
||||
'code verifier doesnt match the challenge',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
if (!(codeChallengeAppToken.expiresAt.getTime() >= Date.now())) {
|
||||
throw new AuthException(
|
||||
'code challenge expired.',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
if (codeChallengeAppToken.userId !== authorizationCodeAppToken.userId) {
|
||||
throw new AuthException(
|
||||
'authorization code / code verifier was not created by same client',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
if (codeChallengeAppToken.revokedAt) {
|
||||
throw new AuthException(
|
||||
'Token has been revoked.',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
await this.appTokenRepository.save({
|
||||
id: codeChallengeAppToken.id,
|
||||
revokedAt: new Date(),
|
||||
});
|
||||
|
||||
userId = codeChallengeAppToken.userId;
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id: userId },
|
||||
relations: ['defaultWorkspace'],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new AuthException(
|
||||
'User who generated the token does not exist',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
if (!user.defaultWorkspace) {
|
||||
throw new AuthException(
|
||||
'User does not have a default workspace',
|
||||
AuthExceptionCode.INVALID_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
const accessToken = await this.accessTokenService.generateAccessToken(
|
||||
user.id,
|
||||
user.defaultWorkspaceId,
|
||||
);
|
||||
const refreshToken = await this.refreshTokenService.generateRefreshToken(
|
||||
user.id,
|
||||
user.defaultWorkspaceId,
|
||||
);
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
);
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
loginToken,
|
||||
};
|
||||
}
|
||||
}
|
||||
// import { Injectable } from '@nestjs/common';
|
||||
// import { InjectRepository } from '@nestjs/typeorm';
|
||||
//
|
||||
// import crypto from 'crypto';
|
||||
//
|
||||
// import { Repository } from 'typeorm';
|
||||
//
|
||||
// import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
// import {
|
||||
// AuthException,
|
||||
// AuthExceptionCode,
|
||||
// } from 'src/engine/core-modules/auth/auth.exception';
|
||||
// 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 { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
||||
// 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 { User } from 'src/engine/core-modules/user/user.entity';
|
||||
// import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
||||
//
|
||||
// @Injectable()
|
||||
// export class OAuthService {
|
||||
// constructor(
|
||||
// @InjectRepository(User, 'core')
|
||||
// private readonly userRepository: Repository<User>,
|
||||
// @InjectRepository(AppToken, 'core')
|
||||
// private readonly appTokenRepository: Repository<AppToken>,
|
||||
// private readonly accessTokenService: AccessTokenService,
|
||||
// private readonly refreshTokenService: RefreshTokenService,
|
||||
// private readonly loginTokenService: LoginTokenService,
|
||||
// ) {}
|
||||
//
|
||||
// async verifyAuthorizationCode(
|
||||
// exchangeAuthCodeInput: ExchangeAuthCodeInput,
|
||||
// ): Promise<ExchangeAuthCode> {
|
||||
// const { authorizationCode, codeVerifier } = exchangeAuthCodeInput;
|
||||
//
|
||||
// if (!authorizationCode) {
|
||||
// throw new AuthException(
|
||||
// 'Authorization code not found',
|
||||
// AuthExceptionCode.INVALID_INPUT,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// let userId = '';
|
||||
//
|
||||
// if (codeVerifier) {
|
||||
// const authorizationCodeAppToken = await this.appTokenRepository.findOne({
|
||||
// where: {
|
||||
// value: authorizationCode,
|
||||
// },
|
||||
// });
|
||||
//
|
||||
// if (!authorizationCodeAppToken) {
|
||||
// throw new AuthException(
|
||||
// 'Authorization code does not exist',
|
||||
// AuthExceptionCode.INVALID_INPUT,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// if (!(authorizationCodeAppToken.expiresAt.getTime() >= Date.now())) {
|
||||
// throw new AuthException(
|
||||
// 'Authorization code expired.',
|
||||
// AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// const codeChallenge = crypto
|
||||
// .createHash('sha256')
|
||||
// .update(codeVerifier)
|
||||
// .digest()
|
||||
// .toString('base64')
|
||||
// .replace(/\+/g, '-')
|
||||
// .replace(/\//g, '_')
|
||||
// .replace(/=/g, '');
|
||||
//
|
||||
// const codeChallengeAppToken = await this.appTokenRepository.findOne({
|
||||
// where: {
|
||||
// value: codeChallenge,
|
||||
// },
|
||||
// });
|
||||
//
|
||||
// if (!codeChallengeAppToken || !codeChallengeAppToken.userId) {
|
||||
// throw new AuthException(
|
||||
// 'code verifier doesnt match the challenge',
|
||||
// AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// if (!(codeChallengeAppToken.expiresAt.getTime() >= Date.now())) {
|
||||
// throw new AuthException(
|
||||
// 'code challenge expired.',
|
||||
// AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// if (codeChallengeAppToken.userId !== authorizationCodeAppToken.userId) {
|
||||
// throw new AuthException(
|
||||
// 'authorization code / code verifier was not created by same client',
|
||||
// AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// if (codeChallengeAppToken.revokedAt) {
|
||||
// throw new AuthException(
|
||||
// 'Token has been revoked.',
|
||||
// AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// await this.appTokenRepository.save({
|
||||
// id: codeChallengeAppToken.id,
|
||||
// revokedAt: new Date(),
|
||||
// });
|
||||
//
|
||||
// userId = codeChallengeAppToken.userId;
|
||||
// }
|
||||
//
|
||||
// const user = await this.userRepository.findOne({
|
||||
// where: { id: userId },
|
||||
// relations: ['defaultWorkspace'],
|
||||
// });
|
||||
//
|
||||
// userValidator.assertIsDefinedOrThrow(
|
||||
// user,
|
||||
// new AuthException(
|
||||
// 'User who generated the token does not exist',
|
||||
// AuthExceptionCode.INVALID_INPUT,
|
||||
// ),
|
||||
// );
|
||||
//
|
||||
// if (!user.defaultWorkspace) {
|
||||
// throw new AuthException(
|
||||
// 'User does not have a default workspace',
|
||||
// AuthExceptionCode.INVALID_DATA,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// const accessToken = await this.accessTokenService.generateAccessToken(
|
||||
// user.id,
|
||||
// user.defaultWorkspaceId,
|
||||
// );
|
||||
// const refreshToken = await this.refreshTokenService.generateRefreshToken(
|
||||
// user.id,
|
||||
// user.defaultWorkspaceId,
|
||||
// );
|
||||
// const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
// user.email,
|
||||
// );
|
||||
//
|
||||
// return {
|
||||
// accessToken,
|
||||
// refreshToken,
|
||||
// loginToken,
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
|
||||
@ -15,11 +15,11 @@ import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/use
|
||||
import { User } from 'src/engine/core-modules/user/user.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';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import {
|
||||
Workspace,
|
||||
WorkspaceActivationStatus,
|
||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
|
||||
jest.mock('bcrypt');
|
||||
|
||||
@ -120,12 +120,15 @@ describe('SignInUpService', () => {
|
||||
provide: DomainManagerService,
|
||||
useValue: {
|
||||
generateSubdomain: jest.fn().mockReturnValue('testSubDomain'),
|
||||
getWorkspaceBySubdomainOrDefaultWorkspace: jest
|
||||
.fn()
|
||||
.mockReturnValue({}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
saveDefaultWorkspaceIfUserHasAccessOrThrow: jest.fn(),
|
||||
hasUserAccessToWorkspaceOrThrow: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -148,7 +151,10 @@ describe('SignInUpService', () => {
|
||||
|
||||
const spy = jest
|
||||
.spyOn(service, 'signUpOnNewWorkspace')
|
||||
.mockResolvedValueOnce({} as User);
|
||||
.mockResolvedValueOnce({ user: {}, workspace: {} } as {
|
||||
user: User;
|
||||
workspace: Workspace;
|
||||
});
|
||||
|
||||
await service.signInUp({
|
||||
email: 'test@test.com',
|
||||
@ -172,7 +178,6 @@ describe('SignInUpService', () => {
|
||||
id: 'user-id',
|
||||
email,
|
||||
passwordHash: undefined,
|
||||
defaultWorkspace: { id: 'workspace-id' },
|
||||
};
|
||||
|
||||
UserFindOneMock.mockReturnValueOnce(existingUser);
|
||||
@ -189,7 +194,7 @@ describe('SignInUpService', () => {
|
||||
targetWorkspaceSubdomain: 'testSubDomain',
|
||||
});
|
||||
|
||||
expect(result).toEqual(existingUser);
|
||||
expect(result).toEqual({ user: existingUser, workspace: {} });
|
||||
});
|
||||
it('signInUp - sso - new user - existing invitation', async () => {
|
||||
const email = 'newuser@test.com';
|
||||
@ -248,7 +253,11 @@ describe('SignInUpService', () => {
|
||||
id: 'user-id',
|
||||
email,
|
||||
passwordHash: undefined,
|
||||
defaultWorkspace: { id: 'workspace-id' },
|
||||
};
|
||||
|
||||
const workspace = {
|
||||
id: workspaceId,
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
};
|
||||
|
||||
UserFindOneMock.mockReturnValueOnce(existingUser);
|
||||
@ -259,10 +268,7 @@ describe('SignInUpService', () => {
|
||||
);
|
||||
workspaceInvitationValidateInvitationMock.mockReturnValueOnce({
|
||||
isValid: true,
|
||||
workspace: {
|
||||
id: workspaceId,
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
},
|
||||
workspace,
|
||||
});
|
||||
|
||||
workspaceInvitationInvalidateWorkspaceInvitationMock.mockReturnValueOnce(
|
||||
@ -277,7 +283,7 @@ describe('SignInUpService', () => {
|
||||
targetWorkspaceSubdomain: 'testSubDomain',
|
||||
});
|
||||
|
||||
expect(result).toEqual(existingUser);
|
||||
expect(result).toEqual({ user: existingUser, workspace });
|
||||
expect(userWorkspaceServiceAddUserToWorkspaceMock).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
workspaceInvitationInvalidateWorkspaceInvitationMock,
|
||||
@ -287,15 +293,16 @@ describe('SignInUpService', () => {
|
||||
const email = 'newuser@test.com';
|
||||
const workspaceId = 'workspace-id';
|
||||
const workspacePersonalInviteToken = 'personal-token-value';
|
||||
const workspace = {
|
||||
id: workspaceId,
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
};
|
||||
|
||||
UserFindOneMock.mockReturnValueOnce(null);
|
||||
|
||||
workspaceInvitationValidateInvitationMock.mockReturnValueOnce({
|
||||
isValid: true,
|
||||
workspace: {
|
||||
id: workspaceId,
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
},
|
||||
workspace,
|
||||
});
|
||||
|
||||
workspaceInvitationInvalidateWorkspaceInvitationMock.mockReturnValueOnce(
|
||||
@ -345,7 +352,6 @@ describe('SignInUpService', () => {
|
||||
id: 'user-id',
|
||||
email,
|
||||
passwordHash: undefined,
|
||||
defaultWorkspace: { id: 'workspace-id' },
|
||||
};
|
||||
|
||||
UserFindOneMock.mockReturnValueOnce(existingUser);
|
||||
@ -379,7 +385,6 @@ describe('SignInUpService', () => {
|
||||
id: 'user-id',
|
||||
email,
|
||||
passwordHash: 'hash-of-validPassword123',
|
||||
defaultWorkspace: { id: 'workspace-id' },
|
||||
};
|
||||
|
||||
UserFindOneMock.mockReturnValueOnce(existingUser);
|
||||
|
||||
@ -2,7 +2,6 @@ import { HttpService } from '@nestjs/axios';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'class-validator';
|
||||
import FileType from 'file-type';
|
||||
import { TWENTY_ICONS_BASE_URL } from 'twenty-shared';
|
||||
import { Repository } from 'typeorm';
|
||||
@ -37,6 +36,7 @@ import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.
|
||||
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
|
||||
import { getImageBufferFromUrl } from 'src/utils/image';
|
||||
import { isWorkEmail } from 'src/utils/is-work-email';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
|
||||
export type SignInUpServiceInput = {
|
||||
email: string;
|
||||
@ -106,7 +106,6 @@ export class SignInUpService {
|
||||
|
||||
const existingUser = await this.userRepository.findOne({
|
||||
where: { email },
|
||||
relations: ['defaultWorkspace'],
|
||||
});
|
||||
|
||||
if (existingUser && !fromSSO) {
|
||||
@ -123,53 +122,22 @@ export class SignInUpService {
|
||||
}
|
||||
}
|
||||
|
||||
const maybeInvitation =
|
||||
fromSSO && !workspacePersonalInviteToken && !workspaceInviteHash
|
||||
? await this.workspaceInvitationService.findInvitationByWorkspaceSubdomainAndUserEmail(
|
||||
{
|
||||
subdomain: targetWorkspaceSubdomain,
|
||||
email,
|
||||
},
|
||||
)
|
||||
: undefined;
|
||||
const signInUpWithInvitationResult = await this.signInUpWithInvitation({
|
||||
email,
|
||||
workspacePersonalInviteToken,
|
||||
workspaceInviteHash,
|
||||
targetWorkspaceSubdomain,
|
||||
fromSSO,
|
||||
firstName,
|
||||
lastName,
|
||||
picture,
|
||||
authProvider,
|
||||
passwordHash,
|
||||
existingUser,
|
||||
});
|
||||
|
||||
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,
|
||||
authProvider,
|
||||
});
|
||||
|
||||
await this.workspaceInvitationService.invalidateWorkspaceInvitation(
|
||||
invitationValidation.workspace.id,
|
||||
email,
|
||||
);
|
||||
|
||||
return updatedUser;
|
||||
}
|
||||
if (isDefined(signInUpWithInvitationResult)) {
|
||||
return signInUpWithInvitationResult;
|
||||
}
|
||||
|
||||
if (!existingUser) {
|
||||
@ -182,27 +150,111 @@ export class SignInUpService {
|
||||
});
|
||||
}
|
||||
|
||||
if (targetWorkspaceSubdomain) {
|
||||
const workspace = await this.workspaceRepository.findOne({
|
||||
where: { subdomain: targetWorkspaceSubdomain },
|
||||
select: ['id'],
|
||||
const workspace =
|
||||
await this.domainManagerService.getWorkspaceBySubdomainOrDefaultWorkspace(
|
||||
targetWorkspaceSubdomain,
|
||||
);
|
||||
|
||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||
|
||||
return await this.validateSignIn({
|
||||
user: existingUser,
|
||||
workspace,
|
||||
authProvider,
|
||||
});
|
||||
}
|
||||
|
||||
private async signInUpWithInvitation({
|
||||
email,
|
||||
workspacePersonalInviteToken,
|
||||
workspaceInviteHash,
|
||||
firstName,
|
||||
lastName,
|
||||
picture,
|
||||
fromSSO,
|
||||
targetWorkspaceSubdomain,
|
||||
authProvider,
|
||||
passwordHash,
|
||||
existingUser,
|
||||
}: {
|
||||
email: string;
|
||||
workspacePersonalInviteToken?: string;
|
||||
workspaceInviteHash?: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
picture?: string | null;
|
||||
authProvider?: WorkspaceAuthProvider;
|
||||
passwordHash?: string;
|
||||
existingUser: User | null;
|
||||
fromSSO: boolean;
|
||||
targetWorkspaceSubdomain?: string;
|
||||
}) {
|
||||
const maybeInvitation =
|
||||
fromSSO && !workspacePersonalInviteToken && !workspaceInviteHash
|
||||
? await this.workspaceInvitationService.findInvitationByWorkspaceSubdomainAndUserEmail(
|
||||
{
|
||||
subdomain: targetWorkspaceSubdomain,
|
||||
email,
|
||||
},
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const invitationValidation =
|
||||
workspacePersonalInviteToken || workspaceInviteHash || maybeInvitation
|
||||
? await this.workspaceInvitationService.validateInvitation({
|
||||
workspacePersonalInviteToken:
|
||||
workspacePersonalInviteToken ?? maybeInvitation?.value,
|
||||
workspaceInviteHash,
|
||||
email,
|
||||
})
|
||||
: null;
|
||||
|
||||
if (
|
||||
invitationValidation?.isValid === true &&
|
||||
invitationValidation.workspace
|
||||
) {
|
||||
const updatedUser = await this.signInUpOnExistingWorkspace({
|
||||
email,
|
||||
passwordHash,
|
||||
workspace: invitationValidation.workspace,
|
||||
firstName,
|
||||
lastName,
|
||||
picture,
|
||||
existingUser,
|
||||
authProvider,
|
||||
});
|
||||
|
||||
workspaceValidator.assertIsExist(
|
||||
workspace,
|
||||
new AuthException(
|
||||
'Workspace not found',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
),
|
||||
await this.workspaceInvitationService.invalidateWorkspaceInvitation(
|
||||
invitationValidation.workspace.id,
|
||||
email,
|
||||
);
|
||||
|
||||
await this.userService.saveDefaultWorkspaceIfUserHasAccessOrThrow(
|
||||
existingUser.id,
|
||||
workspace.id,
|
||||
);
|
||||
return {
|
||||
user: updatedUser,
|
||||
workspace: invitationValidation.workspace,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async validateSignIn({
|
||||
user,
|
||||
workspace,
|
||||
authProvider,
|
||||
}: {
|
||||
user: User;
|
||||
workspace: Workspace;
|
||||
authProvider: SignInUpServiceInput['authProvider'];
|
||||
}) {
|
||||
if (authProvider) {
|
||||
workspaceValidator.isAuthEnabledOrThrow(authProvider, workspace);
|
||||
}
|
||||
|
||||
return existingUser;
|
||||
await this.userService.hasUserAccessToWorkspaceOrThrow(
|
||||
user.id,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
return { user, workspace };
|
||||
}
|
||||
|
||||
async signInUpOnExistingWorkspace({
|
||||
@ -227,7 +279,7 @@ export class SignInUpService {
|
||||
const isNewUser = !isDefined(existingUser);
|
||||
let user = existingUser;
|
||||
|
||||
workspaceValidator.assertIsExist(
|
||||
workspaceValidator.assertIsDefinedOrThrow(
|
||||
workspace,
|
||||
new AuthException(
|
||||
'Workspace not found',
|
||||
@ -244,14 +296,7 @@ export class SignInUpService {
|
||||
);
|
||||
|
||||
if (authProvider) {
|
||||
workspaceValidator.isAuthEnabledOrThrow(
|
||||
authProvider,
|
||||
workspace,
|
||||
new AuthException(
|
||||
`${authProvider} auth is not enabled for this workspace`,
|
||||
AuthExceptionCode.OAUTH_ACCESS_DENIED,
|
||||
),
|
||||
);
|
||||
workspaceValidator.isAuthEnabledOrThrow(authProvider, workspace);
|
||||
}
|
||||
|
||||
if (isNewUser) {
|
||||
@ -264,7 +309,6 @@ export class SignInUpService {
|
||||
defaultAvatarUrl: imagePath,
|
||||
canImpersonate: false,
|
||||
passwordHash,
|
||||
defaultWorkspace: workspace,
|
||||
});
|
||||
|
||||
user = await this.userRepository.save(userToCreate);
|
||||
@ -364,10 +408,7 @@ export class SignInUpService {
|
||||
|
||||
user.defaultAvatarUrl = await this.uploadPicture(picture, workspace.id);
|
||||
|
||||
const userCreated = this.userRepository.create({
|
||||
...user,
|
||||
defaultWorkspace: workspace,
|
||||
});
|
||||
const userCreated = this.userRepository.create(user);
|
||||
|
||||
const newUser = await this.userRepository.save(userCreated);
|
||||
|
||||
@ -383,7 +424,7 @@ export class SignInUpService {
|
||||
value: true,
|
||||
});
|
||||
|
||||
return newUser;
|
||||
return { user: newUser, workspace };
|
||||
}
|
||||
|
||||
async uploadPicture(
|
||||
|
||||
@ -17,9 +17,6 @@ describe('SwitchWorkspaceService', () => {
|
||||
let service: SwitchWorkspaceService;
|
||||
let userRepository: Repository<User>;
|
||||
let workspaceRepository: Repository<Workspace>;
|
||||
let userService: UserService;
|
||||
let accessTokenService: AccessTokenService;
|
||||
let refreshTokenService: RefreshTokenService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@ -45,18 +42,16 @@ describe('SwitchWorkspaceService', () => {
|
||||
generateRefreshToken: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
saveDefaultWorkspaceIfUserHasAccessOrThrow: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: EnvironmentService,
|
||||
useValue: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@ -67,9 +62,6 @@ describe('SwitchWorkspaceService', () => {
|
||||
workspaceRepository = module.get<Repository<Workspace>>(
|
||||
getRepositoryToken(Workspace, 'core'),
|
||||
);
|
||||
accessTokenService = module.get<AccessTokenService>(AccessTokenService);
|
||||
refreshTokenService = module.get<RefreshTokenService>(RefreshTokenService);
|
||||
userService = module.get<UserService>(UserService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
@ -191,44 +183,4 @@ describe('SwitchWorkspaceService', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateSwitchWorkspaceToken', () => {
|
||||
it('should generate and return auth tokens', async () => {
|
||||
const mockUser = { id: 'user-id' };
|
||||
const mockWorkspace = { id: 'workspace-id' };
|
||||
const mockAccessToken = { token: 'access-token', expiresAt: new Date() };
|
||||
const mockRefreshToken = 'refresh-token';
|
||||
|
||||
jest.spyOn(userRepository, 'save').mockResolvedValue({} as User);
|
||||
jest
|
||||
.spyOn(accessTokenService, 'generateAccessToken')
|
||||
.mockResolvedValue(mockAccessToken);
|
||||
jest
|
||||
.spyOn(refreshTokenService, 'generateRefreshToken')
|
||||
.mockResolvedValue(mockRefreshToken as any);
|
||||
|
||||
const result = await service.generateSwitchWorkspaceToken(
|
||||
mockUser as User,
|
||||
mockWorkspace as Workspace,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
tokens: {
|
||||
accessToken: mockAccessToken,
|
||||
refreshToken: mockRefreshToken,
|
||||
},
|
||||
});
|
||||
expect(
|
||||
userService.saveDefaultWorkspaceIfUserHasAccessOrThrow,
|
||||
).toHaveBeenCalledWith(mockUser.id, mockWorkspace.id);
|
||||
expect(accessTokenService.generateAccessToken).toHaveBeenCalledWith(
|
||||
mockUser.id,
|
||||
mockWorkspace.id,
|
||||
);
|
||||
expect(refreshTokenService.generateRefreshToken).toHaveBeenCalledWith(
|
||||
mockUser.id,
|
||||
mockWorkspace.id,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -7,50 +7,31 @@ 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 { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { AuthProviders } from 'src/engine/core-modules/workspace/dtos/public-workspace-data.output';
|
||||
import { AuthProviders } from 'src/engine/core-modules/workspace/dtos/public-workspace-data-output';
|
||||
import { getAuthProvidersByWorkspace } from 'src/engine/core-modules/workspace/utils/get-auth-providers-by-workspace.util';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
|
||||
@Injectable()
|
||||
export class SwitchWorkspaceService {
|
||||
constructor(
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly userService: UserService,
|
||||
private readonly accessTokenService: AccessTokenService,
|
||||
private readonly refreshTokenService: RefreshTokenService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
) {}
|
||||
|
||||
async switchWorkspace(user: User, workspaceId: string) {
|
||||
const userExists = await this.userRepository.findBy({ id: user.id });
|
||||
|
||||
if (!userExists) {
|
||||
throw new AuthException(
|
||||
'User not found',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const workspace = await this.workspaceRepository.findOne({
|
||||
where: { id: workspaceId },
|
||||
relations: ['workspaceUsers', 'workspaceSSOIdentityProviders'],
|
||||
});
|
||||
|
||||
if (!workspace) {
|
||||
throw new AuthException(
|
||||
'workspace doesnt exist',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
workspaceValidator.assertIsDefinedOrThrow(
|
||||
workspace,
|
||||
new AuthException('Workspace not found', AuthExceptionCode.INVALID_INPUT),
|
||||
);
|
||||
|
||||
if (
|
||||
!workspace.workspaceUsers
|
||||
@ -63,11 +44,6 @@ export class SwitchWorkspaceService {
|
||||
);
|
||||
}
|
||||
|
||||
await this.userRepository.save({
|
||||
id: user.id,
|
||||
defaultWorkspace: workspace,
|
||||
});
|
||||
|
||||
const systemEnabledProviders: AuthProviders = {
|
||||
google: this.environmentService.get('AUTH_GOOGLE_ENABLED'),
|
||||
magicLink: false,
|
||||
@ -87,30 +63,4 @@ export class SwitchWorkspaceService {
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async generateSwitchWorkspaceToken(
|
||||
user: User,
|
||||
workspace: Workspace,
|
||||
): Promise<AuthTokens> {
|
||||
await this.userService.saveDefaultWorkspaceIfUserHasAccessOrThrow(
|
||||
user.id,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
const token = await this.accessTokenService.generateAccessToken(
|
||||
user.id,
|
||||
workspace.id,
|
||||
);
|
||||
const refreshToken = await this.refreshTokenService.generateRefreshToken(
|
||||
user.id,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
return {
|
||||
tokens: {
|
||||
accessToken: token,
|
||||
refreshToken,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,10 @@ import { EnvironmentService } from 'src/engine/core-modules/environment/environm
|
||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.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 {
|
||||
Workspace,
|
||||
WorkspaceActivationStatus,
|
||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
import { AccessTokenService } from './access-token.service';
|
||||
@ -22,6 +25,7 @@ describe('AccessTokenService', () => {
|
||||
let jwtWrapperService: JwtWrapperService;
|
||||
let environmentService: EnvironmentService;
|
||||
let userRepository: Repository<User>;
|
||||
let workspaceRepository: Repository<Workspace>;
|
||||
let twentyORMGlobalManager: TwentyORMGlobalManager;
|
||||
|
||||
beforeEach(async () => {
|
||||
@ -84,6 +88,9 @@ describe('AccessTokenService', () => {
|
||||
userRepository = module.get<Repository<User>>(
|
||||
getRepositoryToken(User, 'core'),
|
||||
);
|
||||
workspaceRepository = module.get<Repository<Workspace>>(
|
||||
getRepositoryToken(Workspace, 'core'),
|
||||
);
|
||||
twentyORMGlobalManager = module.get<TwentyORMGlobalManager>(
|
||||
TwentyORMGlobalManager,
|
||||
);
|
||||
@ -99,14 +106,19 @@ describe('AccessTokenService', () => {
|
||||
const workspaceId = 'workspace-id';
|
||||
const mockUser = {
|
||||
id: userId,
|
||||
defaultWorkspace: { id: workspaceId, activationStatus: 'ACTIVE' },
|
||||
defaultWorkspaceId: workspaceId,
|
||||
};
|
||||
const mockWorkspace = {
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
id: workspaceId,
|
||||
};
|
||||
const mockWorkspaceMember = { id: 'workspace-member-id' };
|
||||
const mockToken = 'mock-token';
|
||||
|
||||
jest.spyOn(environmentService, 'get').mockReturnValue('1h');
|
||||
jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser as User);
|
||||
jest
|
||||
.spyOn(workspaceRepository, 'findOne')
|
||||
.mockResolvedValue(mockWorkspace as Workspace);
|
||||
jest
|
||||
.spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace')
|
||||
.mockResolvedValue({
|
||||
|
||||
@ -20,9 +20,14 @@ import {
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceActivationStatus } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import {
|
||||
Workspace,
|
||||
WorkspaceActivationStatus,
|
||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
||||
|
||||
@Injectable()
|
||||
export class AccessTokenService {
|
||||
@ -32,6 +37,8 @@ export class AccessTokenService {
|
||||
private readonly environmentService: EnvironmentService,
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
@ -45,33 +52,25 @@ export class AccessTokenService {
|
||||
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id: userId },
|
||||
relations: ['defaultWorkspace'],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new AuthException(
|
||||
'User is not found',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
userValidator.assertIsDefinedOrThrow(
|
||||
user,
|
||||
new AuthException('User is not found', AuthExceptionCode.INVALID_INPUT),
|
||||
);
|
||||
|
||||
if (!user.defaultWorkspace) {
|
||||
throw new AuthException(
|
||||
'User does not have a default workspace',
|
||||
AuthExceptionCode.INVALID_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
const tokenWorkspaceId = workspaceId ?? user.defaultWorkspaceId;
|
||||
let tokenWorkspaceMemberId: string | undefined;
|
||||
|
||||
if (
|
||||
user.defaultWorkspace.activationStatus ===
|
||||
WorkspaceActivationStatus.ACTIVE
|
||||
) {
|
||||
const workspace = await this.workspaceRepository.findOne({
|
||||
where: { id: workspaceId },
|
||||
});
|
||||
|
||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||
|
||||
if (workspace.activationStatus === WorkspaceActivationStatus.ACTIVE) {
|
||||
const workspaceMemberRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
|
||||
tokenWorkspaceId,
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
);
|
||||
|
||||
@ -93,7 +92,7 @@ export class AccessTokenService {
|
||||
|
||||
const jwtPayload: JwtPayload = {
|
||||
sub: user.id,
|
||||
workspaceId: workspaceId ? workspaceId : user.defaultWorkspaceId,
|
||||
workspaceId,
|
||||
workspaceMemberId: tokenWorkspaceMemberId,
|
||||
};
|
||||
|
||||
|
||||
@ -47,6 +47,7 @@ describe('LoginTokenService', () => {
|
||||
const mockSecret = 'mock-secret';
|
||||
const mockExpiresIn = '1h';
|
||||
const mockToken = 'mock-token';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest
|
||||
.spyOn(jwtWrapperService, 'generateAppSecret')
|
||||
@ -54,18 +55,21 @@ describe('LoginTokenService', () => {
|
||||
jest.spyOn(environmentService, 'get').mockReturnValue(mockExpiresIn);
|
||||
jest.spyOn(jwtWrapperService, 'sign').mockReturnValue(mockToken);
|
||||
|
||||
const result = await service.generateLoginToken(email);
|
||||
const result = await service.generateLoginToken(email, workspaceId);
|
||||
|
||||
expect(result).toEqual({
|
||||
token: mockToken,
|
||||
expiresAt: expect.any(Date),
|
||||
});
|
||||
expect(jwtWrapperService.generateAppSecret).toHaveBeenCalledWith('LOGIN');
|
||||
expect(jwtWrapperService.generateAppSecret).toHaveBeenCalledWith(
|
||||
'LOGIN',
|
||||
workspaceId,
|
||||
);
|
||||
expect(environmentService.get).toHaveBeenCalledWith(
|
||||
'LOGIN_TOKEN_EXPIRES_IN',
|
||||
);
|
||||
expect(jwtWrapperService.sign).toHaveBeenCalledWith(
|
||||
{ sub: email },
|
||||
{ sub: email, workspaceId },
|
||||
{ secret: mockSecret, expiresIn: mockExpiresIn },
|
||||
);
|
||||
});
|
||||
|
||||
@ -14,14 +14,21 @@ export class LoginTokenService {
|
||||
private readonly environmentService: EnvironmentService,
|
||||
) {}
|
||||
|
||||
async generateLoginToken(email: string): Promise<AuthToken> {
|
||||
const secret = this.jwtWrapperService.generateAppSecret('LOGIN');
|
||||
async generateLoginToken(
|
||||
email: string,
|
||||
workspaceId: string,
|
||||
): Promise<AuthToken> {
|
||||
const secret = this.jwtWrapperService.generateAppSecret(
|
||||
'LOGIN',
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const expiresIn = this.environmentService.get('LOGIN_TOKEN_EXPIRES_IN');
|
||||
|
||||
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
|
||||
const jwtPayload = {
|
||||
sub: email,
|
||||
workspaceId,
|
||||
};
|
||||
|
||||
return {
|
||||
@ -33,7 +40,9 @@ export class LoginTokenService {
|
||||
};
|
||||
}
|
||||
|
||||
async verifyLoginToken(loginToken: string): Promise<{ sub: string }> {
|
||||
async verifyLoginToken(
|
||||
loginToken: string,
|
||||
): Promise<{ sub: string; workspaceId: string }> {
|
||||
await this.jwtWrapperService.verifyWorkspaceToken(loginToken, 'LOGIN');
|
||||
|
||||
return this.jwtWrapperService.decode(loginToken, {
|
||||
|
||||
@ -15,6 +15,7 @@ import { WorkspaceSSOModule } from 'src/engine/core-modules/sso/sso.module';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -24,6 +25,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
|
||||
DataSourceModule,
|
||||
EmailModule,
|
||||
WorkspaceSSOModule,
|
||||
UserWorkspaceModule,
|
||||
],
|
||||
providers: [
|
||||
RenewTokenService,
|
||||
|
||||
@ -40,11 +40,12 @@ export class BillingResolver {
|
||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
||||
async billingPortalSession(
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@Args() { returnUrlPath }: BillingSessionInput,
|
||||
) {
|
||||
return {
|
||||
url: await this.billingPortalWorkspaceService.computeBillingPortalSessionURLOrThrow(
|
||||
user.defaultWorkspaceId,
|
||||
workspace.id,
|
||||
returnUrlPath,
|
||||
),
|
||||
};
|
||||
|
||||
@ -45,12 +45,13 @@ export class BillingPortalWorkspaceService {
|
||||
|
||||
const stripeCustomerId = (
|
||||
await this.billingSubscriptionRepository.findOneBy({
|
||||
workspaceId: user.defaultWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
})
|
||||
)?.stripeCustomerId;
|
||||
|
||||
const session = await this.stripeService.createCheckoutSession(
|
||||
user,
|
||||
workspace.id,
|
||||
priceId,
|
||||
quantity,
|
||||
successUrl,
|
||||
|
||||
@ -84,6 +84,7 @@ export class StripeService {
|
||||
|
||||
async createCheckoutSession(
|
||||
user: User,
|
||||
workspaceId: string,
|
||||
priceId: string,
|
||||
quantity: number,
|
||||
successUrl?: string,
|
||||
@ -100,7 +101,7 @@ export class StripeService {
|
||||
mode: 'subscription',
|
||||
subscription_data: {
|
||||
metadata: {
|
||||
workspaceId: user.defaultWorkspaceId,
|
||||
workspaceId,
|
||||
},
|
||||
trial_period_days: this.environmentService.get(
|
||||
'BILLING_FREE_TRIAL_DURATION_IN_DAYS',
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
|
||||
import { AuthProviders } from 'src/engine/core-modules/workspace/dtos/public-workspace-data.output';
|
||||
import { AuthProviders } from 'src/engine/core-modules/workspace/dtos/public-workspace-data-output';
|
||||
|
||||
@ObjectType()
|
||||
class Billing {
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class DomainManagerException extends CustomException {
|
||||
constructor(message: string, code: DomainManagerExceptionCode) {
|
||||
super(message, code);
|
||||
}
|
||||
}
|
||||
|
||||
export enum DomainManagerExceptionCode {
|
||||
SUBDOMAIN_REQUIRED = 'SUBDOMAIN_REQUIRED',
|
||||
}
|
||||
@ -117,6 +117,14 @@ export class DomainManagerService {
|
||||
return subdomain;
|
||||
};
|
||||
|
||||
async getWorkspaceBySubdomainOrDefaultWorkspace(subdomain?: string) {
|
||||
return subdomain
|
||||
? await this.workspaceRepository.findOne({
|
||||
where: { subdomain },
|
||||
})
|
||||
: await this.getDefaultWorkspace();
|
||||
}
|
||||
|
||||
isDefaultSubdomain(subdomain: string) {
|
||||
return subdomain === this.environmentService.get('DEFAULT_SUBDOMAIN');
|
||||
}
|
||||
@ -137,7 +145,7 @@ export class DomainManagerService {
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
async getDefaultWorkspace() {
|
||||
private async getDefaultWorkspace() {
|
||||
if (!this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')) {
|
||||
const workspaces = await this.workspaceRepository.find({
|
||||
order: {
|
||||
@ -147,7 +155,6 @@ export class DomainManagerService {
|
||||
});
|
||||
|
||||
if (workspaces.length > 1) {
|
||||
// TODO AMOREAUX: this logger is trigger twice and the second time the message is undefined for an unknown reason
|
||||
Logger.warn(
|
||||
`In single-workspace mode, there should be only one workspace. Today there are ${workspaces.length} workspaces`,
|
||||
);
|
||||
@ -161,7 +168,7 @@ export class DomainManagerService {
|
||||
);
|
||||
}
|
||||
|
||||
async getWorkspaceByOrigin(origin: string) {
|
||||
async getWorkspaceByOriginOrDefaultWorkspace(origin: string) {
|
||||
try {
|
||||
if (!this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')) {
|
||||
return this.getDefaultWorkspace();
|
||||
@ -171,12 +178,10 @@ export class DomainManagerService {
|
||||
|
||||
if (!isDefined(subdomain)) return;
|
||||
|
||||
const workspace = await this.workspaceRepository.findOne({
|
||||
return await this.workspaceRepository.findOne({
|
||||
where: { subdomain },
|
||||
relations: ['workspaceSSOIdentityProviders'],
|
||||
});
|
||||
|
||||
return workspace;
|
||||
} catch (e) {
|
||||
throw new WorkspaceException(
|
||||
'Workspace not found',
|
||||
|
||||
@ -4,7 +4,10 @@ import { BillingService } from 'src/engine/core-modules/billing/services/billing
|
||||
import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
|
||||
import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceActivationStatus } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import {
|
||||
Workspace,
|
||||
WorkspaceActivationStatus,
|
||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
export enum OnboardingStepKeys {
|
||||
ONBOARDING_CONNECT_ACCOUNT_PENDING = 'ONBOARDING_CONNECT_ACCOUNT_PENDING',
|
||||
@ -25,34 +28,33 @@ export class OnboardingService {
|
||||
private readonly userVarsService: UserVarsService<OnboardingKeyValueTypeMap>,
|
||||
) {}
|
||||
|
||||
private async isSubscriptionIncompleteOnboardingStatus(user: User) {
|
||||
private async isSubscriptionIncompleteOnboardingStatus(workspace: Workspace) {
|
||||
const hasSubscription =
|
||||
await this.billingService.hasWorkspaceActiveSubscriptionOrFreeAccessOrEntitlement(
|
||||
user.defaultWorkspaceId,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
return !hasSubscription;
|
||||
}
|
||||
|
||||
private isWorkspaceActivationPending(user: User) {
|
||||
private isWorkspaceActivationPending(workspace: Workspace) {
|
||||
return (
|
||||
user.defaultWorkspace.activationStatus ===
|
||||
WorkspaceActivationStatus.PENDING_CREATION
|
||||
workspace.activationStatus === WorkspaceActivationStatus.PENDING_CREATION
|
||||
);
|
||||
}
|
||||
|
||||
async getOnboardingStatus(user: User) {
|
||||
if (await this.isSubscriptionIncompleteOnboardingStatus(user)) {
|
||||
async getOnboardingStatus(user: User, workspace: Workspace) {
|
||||
if (await this.isSubscriptionIncompleteOnboardingStatus(workspace)) {
|
||||
return OnboardingStatus.PLAN_REQUIRED;
|
||||
}
|
||||
|
||||
if (this.isWorkspaceActivationPending(user)) {
|
||||
if (this.isWorkspaceActivationPending(workspace)) {
|
||||
return OnboardingStatus.WORKSPACE_ACTIVATION;
|
||||
}
|
||||
|
||||
const userVars = await this.userVarsService.getAll({
|
||||
userId: user.id,
|
||||
workspaceId: user.defaultWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
const isProfileCreationPending =
|
||||
|
||||
@ -110,18 +110,12 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
|
||||
await this.createWorkspaceMember(workspace.id, user);
|
||||
}
|
||||
|
||||
const savedUser = await this.userRepository.save({
|
||||
id: user.id,
|
||||
defaultWorkspace: workspace,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
await this.workspaceInvitationService.invalidateWorkspaceInvitation(
|
||||
workspace.id,
|
||||
user.email,
|
||||
);
|
||||
|
||||
return savedUser;
|
||||
return user;
|
||||
}
|
||||
|
||||
async addUserToWorkspaceByInviteToken(inviteToken: string, user: User) {
|
||||
|
||||
@ -72,18 +72,13 @@ export class UserService extends TypeOrmQueryService<User> {
|
||||
return workspaceMemberRepository.find();
|
||||
}
|
||||
|
||||
async deleteUser(userId: string): Promise<User> {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
relations: ['defaultWorkspace'],
|
||||
});
|
||||
|
||||
assert(user, 'User not found');
|
||||
|
||||
const workspaceId = user.defaultWorkspaceId;
|
||||
|
||||
private async deleteUserFromWorkspace({
|
||||
userId,
|
||||
workspaceId,
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
}) {
|
||||
const dataSourceMetadata =
|
||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||
workspaceId,
|
||||
@ -103,8 +98,6 @@ export class UserService extends TypeOrmQueryService<User> {
|
||||
|
||||
if (workspaceMembers.length === 1) {
|
||||
await this.workspaceService.deleteWorkspace(workspaceId);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
await workspaceDataSource?.query(
|
||||
@ -131,6 +124,19 @@ export class UserService extends TypeOrmQueryService<User> {
|
||||
],
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteUser(userId: string): Promise<User> {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
relations: ['workspaces'],
|
||||
});
|
||||
|
||||
userValidator.assertIsDefinedOrThrow(user);
|
||||
|
||||
await Promise.all(user.workspaces.map(this.deleteUserFromWorkspace));
|
||||
|
||||
return user;
|
||||
}
|
||||
@ -154,16 +160,4 @@ export class UserService extends TypeOrmQueryService<User> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async saveDefaultWorkspaceIfUserHasAccessOrThrow(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
await this.hasUserAccessToWorkspaceOrThrow(userId, workspaceId);
|
||||
|
||||
return await this.userRepository.save({
|
||||
id: userId,
|
||||
defaultWorkspaceId: workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,6 @@ import {
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
Relation,
|
||||
@ -81,16 +80,6 @@ export class User {
|
||||
@Column({ nullable: true, type: 'timestamptz' })
|
||||
deletedAt: Date;
|
||||
|
||||
@Field(() => Workspace, { nullable: false })
|
||||
@ManyToOne(() => Workspace, (workspace) => workspace.users, {
|
||||
onDelete: 'RESTRICT',
|
||||
})
|
||||
defaultWorkspace: Relation<Workspace>;
|
||||
|
||||
@Field()
|
||||
@Column()
|
||||
defaultWorkspaceId: string;
|
||||
|
||||
@OneToMany(() => AppToken, (appToken) => appToken.user, {
|
||||
cascade: true,
|
||||
})
|
||||
@ -110,4 +99,7 @@ export class User {
|
||||
|
||||
@Field(() => OnboardingStatus, { nullable: true })
|
||||
onboardingStatus: OnboardingStatus;
|
||||
|
||||
@Field(() => Workspace, { nullable: true })
|
||||
currentWorkspace: Relation<Workspace>;
|
||||
}
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class UserException extends CustomException {
|
||||
constructor(message: string, code: UserExceptionCode) {
|
||||
super(message, code);
|
||||
}
|
||||
}
|
||||
|
||||
export enum UserExceptionCode {
|
||||
USER_NOT_FOUND = 'USER_NOT_FOUND',
|
||||
}
|
||||
@ -18,6 +18,7 @@ import { UserResolver } from 'src/engine/core-modules/user/user.resolver';
|
||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
||||
|
||||
import { userAutoResolverOpts } from './user.auto-resolver-opts';
|
||||
|
||||
@ -41,6 +42,7 @@ import { UserService } from './services/user.service';
|
||||
TypeOrmModule.forFeature([KeyValuePair], 'core'),
|
||||
UserVarsModule,
|
||||
AnalyticsModule,
|
||||
DomainManagerModule,
|
||||
],
|
||||
exports: [UserService],
|
||||
providers: [UserService, UserResolver, TypeORMService],
|
||||
|
||||
@ -44,6 +44,9 @@ import {
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
||||
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
|
||||
const getHMACKey = (email?: string, key?: string | null) => {
|
||||
if (!email || !key) return null;
|
||||
@ -66,28 +69,31 @@ export class UserResolver {
|
||||
private readonly userVarService: UserVarsService,
|
||||
private readonly fileService: FileService,
|
||||
private readonly analyticsService: AnalyticsService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
) {}
|
||||
|
||||
@Query(() => User)
|
||||
async currentUser(
|
||||
@AuthUser() { id: userId }: User,
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
@OriginHeader() origin: string,
|
||||
): Promise<User> {
|
||||
if (
|
||||
this.environmentService.get('IS_MULTIWORKSPACE_ENABLED') &&
|
||||
workspaceId
|
||||
) {
|
||||
await this.userService.saveDefaultWorkspaceIfUserHasAccessOrThrow(
|
||||
userId,
|
||||
workspaceId,
|
||||
const workspace =
|
||||
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
|
||||
origin,
|
||||
);
|
||||
}
|
||||
|
||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||
|
||||
await this.userService.hasUserAccessToWorkspaceOrThrow(
|
||||
userId,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'],
|
||||
relations: ['workspaces', 'workspaces.workspace'],
|
||||
});
|
||||
|
||||
userValidator.assertIsDefinedOrThrow(
|
||||
@ -95,14 +101,17 @@ export class UserResolver {
|
||||
new AuthException('User not found', AuthExceptionCode.USER_NOT_FOUND),
|
||||
);
|
||||
|
||||
return user;
|
||||
return { ...user, currentWorkspace: workspace };
|
||||
}
|
||||
|
||||
@ResolveField(() => GraphQLJSONObject)
|
||||
async userVars(@Parent() user: User): Promise<Record<string, any>> {
|
||||
async userVars(
|
||||
@Parent() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<Record<string, any>> {
|
||||
const userVars = await this.userVarService.getAll({
|
||||
userId: user.id,
|
||||
workspaceId: user.defaultWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
const userVarAllowList = [
|
||||
@ -127,13 +136,13 @@ export class UserResolver {
|
||||
): Promise<WorkspaceMember | null> {
|
||||
const workspaceMember = await this.userService.loadWorkspaceMember(
|
||||
user,
|
||||
workspace ?? user.defaultWorkspace,
|
||||
workspace,
|
||||
);
|
||||
|
||||
if (workspaceMember && workspaceMember.avatarUrl) {
|
||||
const avatarUrlToken = await this.fileService.encodeFileToken({
|
||||
workspaceMemberId: workspaceMember.id,
|
||||
workspaceId: user.defaultWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
workspaceMember.avatarUrl = `${workspaceMember.avatarUrl}?token=${avatarUrlToken}`;
|
||||
@ -146,16 +155,18 @@ export class UserResolver {
|
||||
@ResolveField(() => [WorkspaceMember], {
|
||||
nullable: true,
|
||||
})
|
||||
async workspaceMembers(@Parent() user: User): Promise<WorkspaceMember[]> {
|
||||
const workspaceMembers = await this.userService.loadWorkspaceMembers(
|
||||
user.defaultWorkspace,
|
||||
);
|
||||
async workspaceMembers(
|
||||
@Parent() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<WorkspaceMember[]> {
|
||||
const workspaceMembers =
|
||||
await this.userService.loadWorkspaceMembers(workspace);
|
||||
|
||||
for (const workspaceMember of workspaceMembers) {
|
||||
if (workspaceMember.avatarUrl) {
|
||||
const avatarUrlToken = await this.fileService.encodeFileToken({
|
||||
workspaceMemberId: workspaceMember.id,
|
||||
workspaceId: user.defaultWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
workspaceMember.avatarUrl = `${workspaceMember.avatarUrl}?token=${avatarUrlToken}`;
|
||||
@ -221,7 +232,17 @@ export class UserResolver {
|
||||
}
|
||||
|
||||
@ResolveField(() => OnboardingStatus)
|
||||
async onboardingStatus(@Parent() user: User): Promise<OnboardingStatus> {
|
||||
return this.onboardingService.getOnboardingStatus(user);
|
||||
async onboardingStatus(
|
||||
@Parent() user: User,
|
||||
@OriginHeader() origin: string,
|
||||
): Promise<OnboardingStatus> {
|
||||
const workspace =
|
||||
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
|
||||
origin,
|
||||
);
|
||||
|
||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||
|
||||
return this.onboardingService.getOnboardingStatus(user, workspace);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,17 @@
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
import {
|
||||
UserException,
|
||||
UserExceptionCode,
|
||||
} from 'src/engine/core-modules/user/user.exception';
|
||||
|
||||
const assertIsDefinedOrThrow = (
|
||||
user: User | undefined | null,
|
||||
exceptionToThrow: CustomException,
|
||||
exceptionToThrow: CustomException = new UserException(
|
||||
'User not found',
|
||||
UserExceptionCode.USER_NOT_FOUND,
|
||||
),
|
||||
): asserts user is User => {
|
||||
if (!isDefined(user)) {
|
||||
throw exceptionToThrow;
|
||||
|
||||
@ -142,11 +142,10 @@ export class WorkspaceInvitationService {
|
||||
subdomain?: string;
|
||||
email: string;
|
||||
}) {
|
||||
const workspace = this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')
|
||||
? await this.workspaceRepository.findOneBy({
|
||||
subdomain,
|
||||
})
|
||||
: await this.domainManagerService.getDefaultWorkspace();
|
||||
const workspace =
|
||||
await this.domainManagerService.getWorkspaceBySubdomainOrDefaultWorkspace(
|
||||
subdomain,
|
||||
);
|
||||
|
||||
if (!workspace) return;
|
||||
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthToken } from 'src/engine/core-modules/auth/dto/token.entity';
|
||||
|
||||
@ObjectType()
|
||||
export class ActivateWorkspaceOutput {
|
||||
@Field(() => Workspace)
|
||||
workspace: Workspace;
|
||||
|
||||
@Field(() => AuthToken)
|
||||
loginToken: AuthToken;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType()
|
||||
export class WorkspaceSubdomainAndId {
|
||||
@Field()
|
||||
subdomain: string;
|
||||
|
||||
@Field()
|
||||
id: string;
|
||||
}
|
||||
@ -51,7 +51,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
||||
id: payload.id,
|
||||
});
|
||||
|
||||
workspaceValidator.assertIsExist(
|
||||
workspaceValidator.assertIsDefinedOrThrow(
|
||||
workspace,
|
||||
new WorkspaceException(
|
||||
'Workspace not found',
|
||||
@ -81,55 +81,46 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
||||
});
|
||||
}
|
||||
|
||||
async activateWorkspace(user: User, data: ActivateWorkspaceInput) {
|
||||
async activateWorkspace(
|
||||
user: User,
|
||||
workspace: Workspace,
|
||||
data: ActivateWorkspaceInput,
|
||||
) {
|
||||
if (!data.displayName || !data.displayName.length) {
|
||||
throw new BadRequestException("'displayName' not provided");
|
||||
}
|
||||
|
||||
const existingWorkspace = await this.workspaceRepository.findOneBy({
|
||||
id: user.defaultWorkspaceId,
|
||||
});
|
||||
|
||||
if (!existingWorkspace) {
|
||||
throw new Error('Workspace not found');
|
||||
}
|
||||
|
||||
if (
|
||||
existingWorkspace.activationStatus ===
|
||||
WorkspaceActivationStatus.ONGOING_CREATION
|
||||
workspace.activationStatus === WorkspaceActivationStatus.ONGOING_CREATION
|
||||
) {
|
||||
throw new Error('Workspace is already being created');
|
||||
}
|
||||
|
||||
if (
|
||||
existingWorkspace.activationStatus !==
|
||||
WorkspaceActivationStatus.PENDING_CREATION
|
||||
workspace.activationStatus !== WorkspaceActivationStatus.PENDING_CREATION
|
||||
) {
|
||||
throw new Error('Workspace is not pending creation');
|
||||
}
|
||||
|
||||
await this.workspaceRepository.update(user.defaultWorkspaceId, {
|
||||
await this.workspaceRepository.update(workspace.id, {
|
||||
activationStatus: WorkspaceActivationStatus.ONGOING_CREATION,
|
||||
});
|
||||
|
||||
await this.featureFlagService.enableFeatureFlags(
|
||||
DEFAULT_FEATURE_FLAGS,
|
||||
user.defaultWorkspaceId,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
await this.workspaceManagerService.init(user.defaultWorkspaceId);
|
||||
await this.userWorkspaceService.createWorkspaceMember(
|
||||
user.defaultWorkspaceId,
|
||||
user,
|
||||
);
|
||||
await this.workspaceManagerService.init(workspace.id);
|
||||
await this.userWorkspaceService.createWorkspaceMember(workspace.id, user);
|
||||
|
||||
await this.workspaceRepository.update(user.defaultWorkspaceId, {
|
||||
await this.workspaceRepository.update(workspace.id, {
|
||||
displayName: data.displayName,
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
});
|
||||
|
||||
return await this.workspaceRepository.findOneBy({
|
||||
id: user.defaultWorkspaceId,
|
||||
id: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
@ -170,41 +161,6 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
||||
userId,
|
||||
workspaceId,
|
||||
});
|
||||
await this.reassignOrRemoveUserDefaultWorkspace(workspaceId, userId);
|
||||
}
|
||||
|
||||
private async reassignOrRemoveUserDefaultWorkspace(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
) {
|
||||
const userWorkspaces = await this.userWorkspaceRepository.find({
|
||||
where: { userId: userId },
|
||||
});
|
||||
|
||||
if (userWorkspaces.length === 0) {
|
||||
await this.userRepository.delete({ id: userId });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error(`User ${userId} not found in workspace ${workspaceId}`);
|
||||
}
|
||||
|
||||
if (user.defaultWorkspaceId === workspaceId) {
|
||||
await this.userRepository.update(
|
||||
{ id: userId },
|
||||
{
|
||||
defaultWorkspaceId: userWorkspaces[0].workspaceId,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async isSubdomainAvailable(subdomain: string) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { AuthProviders } from 'src/engine/core-modules/workspace/dtos/public-workspace-data.output';
|
||||
import { AuthProviders } from 'src/engine/core-modules/workspace/dtos/public-workspace-data-output';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
export const getAuthProvidersByWorkspace = ({
|
||||
|
||||
@ -21,7 +21,6 @@ import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-p
|
||||
import { PostgresCredentials } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.entity';
|
||||
import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
|
||||
export enum WorkspaceActivationStatus {
|
||||
ONGOING_CREATION = 'ONGOING_CREATION',
|
||||
@ -89,9 +88,6 @@ export class Workspace {
|
||||
})
|
||||
keyValuePairs: Relation<KeyValuePair[]>;
|
||||
|
||||
@OneToMany(() => User, (user) => user.defaultWorkspace)
|
||||
users: Relation<User[]>;
|
||||
|
||||
@OneToMany(() => UserWorkspace, (userWorkspace) => userWorkspace.workspace, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
|
||||
@ -22,11 +22,10 @@ import { FileService } from 'src/engine/core-modules/file/services/file.service'
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { ActivateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/activate-workspace-input';
|
||||
import { ActivateWorkspaceOutput } from 'src/engine/core-modules/workspace/dtos/activate-workspace-output';
|
||||
import {
|
||||
AuthProviders,
|
||||
PublicWorkspaceDataOutput,
|
||||
} from 'src/engine/core-modules/workspace/dtos/public-workspace-data.output';
|
||||
} from 'src/engine/core-modules/workspace/dtos/public-workspace-data-output';
|
||||
import { UpdateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/update-workspace-input';
|
||||
import { getAuthProvidersByWorkspace } from 'src/engine/core-modules/workspace/utils/get-auth-providers-by-workspace.util';
|
||||
import { workspaceGraphqlApiExceptionHandler } from 'src/engine/core-modules/workspace/utils/workspace-graphql-api-exception-handler.util';
|
||||
@ -74,21 +73,21 @@ export class WorkspaceResolver {
|
||||
return workspace;
|
||||
}
|
||||
|
||||
@Mutation(() => ActivateWorkspaceOutput)
|
||||
@Mutation(() => Workspace)
|
||||
@UseGuards(UserAuthGuard)
|
||||
async activateWorkspace(
|
||||
@Args('data') data: ActivateWorkspaceInput,
|
||||
@AuthUser() user: User,
|
||||
@OriginHeader() origin: string,
|
||||
) {
|
||||
const workspace = await this.workspaceService.activateWorkspace(user, data);
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
);
|
||||
const workspace =
|
||||
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
|
||||
origin,
|
||||
);
|
||||
|
||||
return {
|
||||
workspace,
|
||||
loginToken,
|
||||
};
|
||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||
|
||||
return await this.workspaceService.activateWorkspace(user, workspace, data);
|
||||
}
|
||||
|
||||
@Mutation(() => Workspace)
|
||||
@ -188,9 +187,11 @@ export class WorkspaceResolver {
|
||||
@Query(() => PublicWorkspaceDataOutput)
|
||||
async getPublicWorkspaceDataBySubdomain(@OriginHeader() origin: string) {
|
||||
const workspace =
|
||||
await this.domainManagerService.getWorkspaceByOrigin(origin);
|
||||
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
|
||||
origin,
|
||||
);
|
||||
|
||||
workspaceValidator.assertIsExist(
|
||||
workspaceValidator.assertIsDefinedOrThrow(
|
||||
workspace,
|
||||
new WorkspaceException(
|
||||
'Workspace not found',
|
||||
|
||||
@ -3,12 +3,22 @@ import {
|
||||
WorkspaceActivationStatus,
|
||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { WorkspaceAuthProvider } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
import {
|
||||
WorkspaceException,
|
||||
WorkspaceExceptionCode,
|
||||
} from 'src/engine/core-modules/workspace/workspace.exception';
|
||||
|
||||
const assertIsExist = (
|
||||
const assertIsDefinedOrThrow = (
|
||||
workspace: Workspace | undefined | null,
|
||||
exceptionToThrow?: CustomException,
|
||||
exceptionToThrow: CustomException = new WorkspaceException(
|
||||
'Workspace not found',
|
||||
WorkspaceExceptionCode.WORKSPACE_NOT_FOUND,
|
||||
),
|
||||
): asserts workspace is Workspace => {
|
||||
if (!workspace) {
|
||||
throw exceptionToThrow;
|
||||
@ -28,7 +38,10 @@ const assertIsActive = (
|
||||
const isAuthEnabledOrThrow = (
|
||||
provider: WorkspaceAuthProvider,
|
||||
workspace: Workspace,
|
||||
exceptionToThrowCustom: AuthException,
|
||||
exceptionToThrowCustom: AuthException = new AuthException(
|
||||
`${provider} auth is not enabled for this workspace`,
|
||||
AuthExceptionCode.OAUTH_ACCESS_DENIED,
|
||||
),
|
||||
) => {
|
||||
if (provider === 'google' && workspace.isGoogleAuthEnabled) return true;
|
||||
if (provider === 'microsoft' && workspace.isMicrosoftAuthEnabled) return true;
|
||||
@ -38,11 +51,11 @@ const isAuthEnabledOrThrow = (
|
||||
};
|
||||
|
||||
export const workspaceValidator: {
|
||||
assertIsExist: typeof assertIsExist;
|
||||
assertIsDefinedOrThrow: typeof assertIsDefinedOrThrow;
|
||||
assertIsActive: typeof assertIsActive;
|
||||
isAuthEnabledOrThrow: typeof isAuthEnabledOrThrow;
|
||||
} = {
|
||||
assertIsExist: assertIsExist,
|
||||
assertIsDefinedOrThrow: assertIsDefinedOrThrow,
|
||||
assertIsActive: assertIsActive,
|
||||
isAuthEnabledOrThrow: isAuthEnabledOrThrow,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user