CVE-2022-2037 (TypeScript, published 2022-06-09T17:15Z)
Description: value: Excessive Attack Surface in GitHub repository tooljet/tooljet prior to v1.16.0.
Severity: MEDIUM (CVSS v3 base score: 8)
Weakness: NVD-CWE-Other \u2014 Other
NVD is only using a subset of CWE for mapping instead of the entire CWE, and the weakness type is not covered by that subset.
Repository: https://github.com/tooljet/tooljet
Files changed:
frontend/src/App/App.jsx
frontend/src/ConfirmationPage/ConfirmationPage.jsx
frontend/src/ConfirmationPage/OrganizationInvitationPage.jsx
frontend/src/ManageOrgUsers/ManageOrgUsers.jsx
frontend/src/_services/app.service.js
frontend/src/_services/user.service.js
server/src/controllers/app.controller.ts
server/src/controllers/organization_users.controller.ts
server/src/controllers/users.controller.ts
server/src/dto/accept-organization-invite.dto.ts
server/src/dto/user.dto.ts
server/src/services/auth.service.ts
server/src/services/email.service.ts
server/src/services/organization_users.service.ts
server/src/services/organizations.service.ts
server/src/services/users.service.ts
server/test/controllers/app.e2e-spec.ts
server/test/controllers/users.e2e-spec.ts
Commit message: [BUG] Account level security issue (#3150)

* bug fixes

* changes

* changes for single workspace support

* added guards for signup route

* test cases fixes

* Workspace invite and user onboarding flow changes (#3190)

* invite user flow changes

* review comments

* cleanup

* testcase fix

Vulnerable code:
import { userService } from '@/_services';
    const token = this.props.location.state.token;
    userService
import { userService } from '@/_services';
    userService
      .then(() => {
        toast.success(`Added to the workspace${isSetPassword ? ' and password has been set ' : ' '}successfully.`, {
          position: 'top-center',
        this.props.history.push('/login');
      })
      .catch(({ error }) => {
        this.setState({ isLoading: false });
        toast.error(error, { position: 'top-center' });
              {!this.single_organization && (
                  <div className="org-invite-or">
                    <h2>
                      <span>OR</span>
                    </h2>
              <h2 className="card-title text-center mb-4">Set up your account</h2>
              <div className="mb-3">
                <label className="form-label">Password</label>
                <div className="input-group input-group-flat">
                  <input
                    onChange={this.handleChange}
                    name="password"
                    type="password"
                    className="form-control"
                    autoComplete="off"
                  />
                  <span className="input-group-text"></span>
                </div>
              </div>
              <div className="mb-3">
                <label className="form-label">Confirm Password</label>
                <div className="input-group input-group-flat">
                  <input
                    onChange={this.handleChange}
                    name="password_confirmation"
                    type="password"
                    className="form-control"
                    autoComplete="off"
                  />
                  <span className="input-group-text"></span>
                </div>
              </div>
              <div className="form-footer">
                <p>
                  By clicking the button below, you agree to our{' '}
                  <a href="https://tooljet.io/terms">Terms and Conditions</a>.
                </p>
                <button
                  className={`btn mt-2 btn-primary w-100 ${isLoading ? ' btn-loading' : ''}`}
                  onClick={(e) => this.acceptInvite(e, true)}
                  disabled={isLoading}
                >
                  Finish account setup and accept invite
                </button>
              </div>
import { history } from '@/_helpers';
    console.log(email);
  logout = () => {
    authenticationService.logout();
    history.push('/login');
  };

  setPasswordFromToken,
  acceptInvite,
function setPasswordFromToken({ token, password, organization, role, firstName, lastName }) {
  const body = {
    token,
    password,
    organization,
    role,
    first_name: firstName,
    last_name: lastName,
  };

  const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body) };
  return fetch(`${config.apiUrl}/users/set_password_from_token`, requestOptions).then(handleResponse);
}

function acceptInvite({ token, password }) {
  const body = {
    token,
    password,
  };

  const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body) };
  return fetch(`${config.apiUrl}/users/accept-invite`, requestOptions).then(handleResponse);
}

  constructor(private organizationUsersService: OrganizationUsersService) {}
    const result = await this.organizationUsersService.inviteNewUser(user, inviteNewUserDto);
import { Body, Controller, Post, Patch, UseGuards, UseInterceptors, Req, UploadedFile } from '@nestjs/common';
import { SignupDisableGuard } from 'src/modules/auth/signup-disable.guard';
import { CreateUserDto, UpdateUserDto } from '@dto/user.dto';
import { AcceptInviteDto } from '@dto/accept-organization-invite.dto';
import { MultiOrganizationGuard } from 'src/modules/auth/multi-organization.guard';
  @UseGuards(MultiOrganizationGuard, SignupDisableGuard)
  @Post('set_password_from_token')
  async create(@Body() userCreateDto: CreateUserDto) {
    await this.usersService.setupAccountFromInvitationToken(userCreateDto);
    return {};
  }

  @Post('accept-invite')
  async acceptInvite(@Body() acceptInviteDto: AcceptInviteDto) {
    await this.usersService.acceptOrganizationInvite(acceptInviteDto);
    return {};
  }

  async addAvatar(@Req() req, @UploadedFile() file: Express.Multer.File) {
    return this.usersService.addAvatar(req.user.id, file.buffer, file.originalname);
import { IsString, IsOptional, IsNotEmpty } from 'class-validator';
import { Injectable, NotAcceptableException, NotFoundException, UnauthorizedException } from '@nestjs/common';
  private async validateUser(email: string, password: string, organisationId?: string): Promise<User> {
    const user = await this.usersService.findByEmail(email, organisationId);
    if (existingUser?.invitationToken || existingUser?.organizationUsers?.some((ou) => ou.status === 'active')) {
  async sendWelcomeEmail(to: string, name: string, invitationtoken: string) {
    const inviteUrl = `${this.TOOLJET_HOST}/invitations/${invitationtoken}`;
    organisationName: string
          ${sender} has invited you to use ToolJet workspace ${organisationName}. Use the link below to set up your account and get started.
import { InviteNewUserDto } from '@dto/invite-new-user.dto';
  async inviteNewUser(currentUser: User, inviteNewUserDto: InviteNewUserDto): Promise<OrganizationUser> {
    const userParams = <User>{
      firstName: inviteNewUserDto.first_name,
      lastName: inviteNewUserDto.last_name,
      email: inviteNewUserDto.email,
    };

    let user = await this.usersService.findByEmail(userParams.email);

    if (user?.organizationUsers?.some((ou) => ou.organizationId === currentUser.organizationId)) {
      throw new BadRequestException('User with such email already exists.');
    }

    if (user?.invitationToken) {
      // user sign up not completed, name will be empty - updating name
      await this.usersService.update(user.id, { firstName: userParams.firstName, lastName: userParams.lastName });
    }

    user = await this.usersService.create(userParams, currentUser.organizationId, ['all_users'], user);

    const currentOrganization: Organization = (
      await this.organizationUsersRepository.findOne({
        where: { userId: currentUser.id, organizationId: currentUser.organizationId },
        relations: ['organization'],
      })
    )?.organization;

    const organizationUser: OrganizationUser = await this.create(user, currentOrganization, true);

    await this.emailService.sendOrganizationUserWelcomeEmail(
      user.email,
      user.firstName,
      currentUser.firstName,
      organizationUser.invitationToken,
      currentOrganization.name
    );

    return organizationUser;
  }

    private encryptionService: EncryptionService
import { Organization } from 'src/entities/organization.entity';
import { CreateUserDto } from '@dto/user.dto';
    @InjectRepository(Organization)
    private organizationsRepository: Repository<Organization>,
    isInvite?: boolean
          defaultOrganizationId: organizationId,
        if (isInvite) {
          // user already invited to an organization, but not active - user tries to sign up
          await manager.save(
            Object.assign(existingUser, {
              invitationToken: uuid.v4(),
              defaultOrganizationId: organizationId,
            })
          );
        }
            userId: user.id,

    return user;
  async setupAccountFromInvitationToken(userCreateDto: CreateUserDto) {
    const { organization, password, token, role, first_name: firstName, last_name: lastName } = userCreateDto;

    if (!token) {
      throw new BadRequestException('Invalid token');
    }

    const user: User = await this.usersRepository.findOne({ where: { invitationToken: token } });

    if (!user?.organizationUsers) {
      throw new BadRequestException('Invalid invitation link');
    }
    const organizationUser: OrganizationUser = user.organizationUsers.find(
      (ou) => ou.organizationId === user.defaultOrganizationId
    );

    if (!organizationUser) {
      throw new BadRequestException('Invalid invitation link');
    }

    await this.usersRepository.save(
      Object.assign(user, {
        firstName,
        lastName,
        password,
        role,
        invitationToken: null,
      })
    );

    await this.organizationUsersRepository.save(
      Object.assign(organizationUser, {
        invitationToken: null,
        status: 'active',
      })
    );

    if (organization) {
      await this.organizationsRepository.update(user.defaultOrganizationId, {
        name: organization,
      });
    }
  }

  async acceptOrganizationInvite(params: any) {
    const { password, token } = params;

    const organizationUser = await this.organizationUsersRepository.findOne({
      where: { invitationToken: token },
      relations: ['user'],
    });

    if (!organizationUser?.user) {
      throw new BadRequestException('Invalid invitation link');
    }
    const user: User = organizationUser.user;

    if (user.invitationToken) {
      // User sign up link send - not activated account
      const defaultOrganizationUser = await this.organizationUsersRepository.findOne({
        where: { organizationId: user.defaultOrganizationId, status: 'invited' },
      });

      if (defaultOrganizationUser) {
        await this.organizationUsersRepository.save(
          Object.assign(defaultOrganizationUser, {
            invitationToken: null,
            status: 'active',
          })
        );
      }
    }

    // set new password if entered
    await this.usersRepository.save(
      Object.assign(user, {
        ...(password ? { password } : {}),
        invitationToken: null,
      })
    );

    await this.organizationUsersRepository.save(
      Object.assign(organizationUser, {
        invitationToken: null,
        status: 'active',
      })
    );
  }

import { getManager, Repository } from 'typeorm';
        expect(response.statusCode).toBe(406);
import { authHeaderForUser, clearDB, createUser, createNestAppInstanceWithEnvMock } from '../test.helper';
import { v4 as uuidv4 } from 'uuid';
import { OrganizationUser } from 'src/entities/organization_user.entity';
  let mockConfig;
    ({ app, mockConfig } = await createNestAppInstanceWithEnvMock());
  describe('POST /api/users/set_password_from_token', () => {
    it('should allow users to setup account after sign up using  Multi-Workspace', async () => {
      const invitationToken = uuidv4();
      const userData = await createUser(app, {
        email: 'signup@tooljet.io',
        invitationToken,
        status: 'invited',
      });
      const { user, organization } = userData;

      const response = await request(app.getHttpServer()).post('/api/users/set_password_from_token').send({
        first_name: 'signupuser',
        last_name: 'user',
        organization: 'org1',
        password: uuidv4(),
        token: invitationToken,
        role: 'developer',
      });

      expect(response.statusCode).toBe(201);

      const updatedUser = await getManager().findOneOrFail(User, { where: { email: user.email } });
      expect(updatedUser.firstName).toEqual('signupuser');
      expect(updatedUser.lastName).toEqual('user');
      expect(updatedUser.defaultOrganizationId).toEqual(organization.id);
      const organizationUser = await getManager().findOneOrFail(OrganizationUser, { where: { userId: user.id } });
      expect(organizationUser.status).toEqual('active');
    });

    it('should return error if required params are not present - Multi-Workspace', async () => {
      const invitationToken = uuidv4();
      await createUser(app, {
        email: 'signup@tooljet.io',
        invitationToken,
        status: 'invited',
      });

      const response = await request(app.getHttpServer()).post('/api/users/set_password_from_token');

      expect(response.statusCode).toBe(400);
      expect(response.body.message).toStrictEqual([
        'password should not be empty',
        'password must be a string',
        'token should not be empty',
        'token must be a string',
      ]);
    });

    it('should allow users to setup account for single organization only once', async () => {
      jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
        switch (key) {
          case 'DISABLE_MULTI_WORKSPACE':
            return 'true';
          default:
            return process.env[key];
        }
      });
      const invitationToken = uuidv4();
      await createUser(app, {
        email: 'signup@tooljet.io',
        invitationToken,
        status: 'invited',
      });

      let response = await request(app.getHttpServer()).post('/api/users/set_password_from_token').send({
        first_name: 'signupuser',
        last_name: 'user',
        organization: 'org1',
        password: uuidv4(),
        token: invitationToken,
        role: 'developer',
      });

      expect(response.statusCode).toBe(201);

      await createUser(app, {
        email: 'signup2@tooljet.io',
        invitationToken,
        status: 'invited',
      });

      response = await request(app.getHttpServer()).post('/api/users/set_password_from_token').send({
        first_name: 'signupuser2',
        last_name: 'user2',
        organization: 'org1',
        password: uuidv4(),
        token: invitationToken,
        role: 'developer',
      });

      expect(response.statusCode).toBe(403);
    });

    it('should not allow users to setup account for Multi-Workspace and sign up disabled', async () => {
      jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
        switch (key) {
          case 'DISABLE_SIGNUPS':
            return 'true';
          default:
            return process.env[key];
        }
      });
      const invitationToken = uuidv4();
      await createUser(app, {
        email: 'signup@tooljet.io',
        invitationToken,
        status: 'invited',
      });

      const response = await request(app.getHttpServer()).post('/api/users/set_password_from_token').send({
        first_name: 'signupuser',
        last_name: 'user',
        organization: 'org1',
        password: uuidv4(),
        token: invitationToken,
        role: 'developer',
      });

      expect(response.statusCode).toBe(403);
    });

    it('should allow users to setup account if already invited to an organization but not activated', async () => {
      const org = (
        await createUser(app, {
          email: 'admin@tooljet.io',
        })
      ).organization;
      const invitedUser = await createUser(app, {
        email: 'invited@tooljet.io',
        status: 'invited',
        organization: org,
      });

      const signUpResponse = await request(app.getHttpServer())
        .post('/api/signup')
        .send({ email: 'invited@tooljet.io' });

      expect(signUpResponse.statusCode).toBe(201);

      const invitedUserDetails = await getManager().findOneOrFail(User, { where: { email: invitedUser.user.email } });

      expect(invitedUserDetails.defaultOrganizationId).not.toBe(org.id);

      const response = await request(app.getHttpServer()).post('/api/users/set_password_from_token').send({
        first_name: 'signupuser',
        last_name: 'user',
        organization: 'org1',
        password: uuidv4(),
        token: invitedUserDetails.invitationToken,
        role: 'developer',
      });

      expect(response.statusCode).toBe(201);
      const updatedUser = await getManager().findOneOrFail(User, { where: { email: invitedUser.user.email } });
      expect(updatedUser.firstName).toEqual('signupuser');
      expect(updatedUser.lastName).toEqual('user');
      expect(updatedUser.defaultOrganizationId).not.toBe(org.id);
      const organizationUser = await getManager().findOneOrFail(OrganizationUser, {
        where: { userId: invitedUser.user.id, organizationId: org.id },
      });
      const defaultOrganizationUser = await getManager().findOneOrFail(OrganizationUser, {
        where: { userId: invitedUser.user.id, organizationId: invitedUserDetails.defaultOrganizationId },
      });
      expect(organizationUser.status).toEqual('invited');
      expect(defaultOrganizationUser.status).toEqual('active');
    });

    it('should not allow users to setup account if already invited to an organization and activated account through invite link after sign up', async () => {
      const { organization: org } = await createUser(app, {
        email: 'admin@tooljet.io',
      });
      const invitedUser = await createUser(app, {
        email: 'invited@tooljet.io',
        status: 'invited',
        organization: org,
      });

      const signUpResponse = await request(app.getHttpServer())
        .post('/api/signup')
        .send({ email: 'invited@tooljet.io' });

      expect(signUpResponse.statusCode).toBe(201);

      const invitedUserDetails = await getManager().findOneOrFail(User, { where: { email: invitedUser.user.email } });

      expect(invitedUserDetails.defaultOrganizationId).not.toBe(org.id);

      const acceptInviteResponse = await request(app.getHttpServer()).post('/api/users/accept-invite').send({
        token: invitedUser.orgUser.invitationToken,
        password: 'new-password',
      });

      expect(acceptInviteResponse.statusCode).toBe(201);

      const organizationUser = await getManager().findOneOrFail(OrganizationUser, {
        where: { userId: invitedUser.user.id, organizationId: org.id },
      });
      const defaultOrganizationUser = await getManager().findOneOrFail(OrganizationUser, {
        where: { userId: invitedUser.user.id, organizationId: invitedUserDetails.defaultOrganizationId },
      });
      expect(organizationUser.status).toEqual('active');
      expect(defaultOrganizationUser.status).toEqual('active');

      const updatedUser = await getManager().findOneOrFail(User, { where: { email: invitedUser.user.email } });
      expect(updatedUser.defaultOrganizationId).toBe(defaultOrganizationUser.organizationId);

      const response = await request(app.getHttpServer()).post('/api/users/set_password_from_token').send({
        first_name: 'signupuser',
        last_name: 'user',
        organization: 'org1',
        password: uuidv4(),
        token: invitedUserDetails.invitationToken,
        role: 'developer',
      });

      expect(response.statusCode).toBe(400);
    });
  });

  describe('POST /api/users/accept-invite', () => {
    it('should allow users to accept invitation when Multi-Workspace is enabled', async () => {
      const userData = await createUser(app, {
        email: 'organizationUser@tooljet.io',
        status: 'invited',
      });
      const { user, orgUser } = userData;

      const response = await request(app.getHttpServer()).post('/api/users/accept-invite').send({
        token: orgUser.invitationToken,
        password: uuidv4(),
      });

      expect(response.statusCode).toBe(201);

      const organizationUser = await getManager().findOneOrFail(OrganizationUser, { where: { userId: user.id } });
      expect(organizationUser.status).toEqual('active');
    });

    it('should allow users to accept invitation when Multi-Workspace is disabled', async () => {
      jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
        switch (key) {
          case 'DISABLE_MULTI_WORKSPACE':
            return 'true';
          default:
            return process.env[key];
        }
      });
      const userData = await createUser(app, {
        email: 'organizationUser@tooljet.io',
        status: 'invited',
      });
      const { user, orgUser } = userData;

      const response = await request(app.getHttpServer()).post('/api/users/accept-invite').send({
        token: orgUser.invitationToken,
        password: uuidv4(),
      });

      expect(response.statusCode).toBe(201);

      const organizationUser = await getManager().findOneOrFail(OrganizationUser, { where: { userId: user.id } });
      expect(organizationUser.status).toEqual('active');
    });
  });


Fixed code:
            <Route
              path="/invitations/:token/workspaces/:organizationToken"
              render={(props) => (
                <Redirect
                  to={{
                    pathname: '/confirm',
                    state: {
                      token: props.match.params.token,
                      organizationToken: props.match.params.organizationToken,
                    },
                  }}
                />
              )}
            />
import { appService } from '@/_services';
    const { token, organizationToken } = this.props.location.state;
    appService
        organizationToken,
import { appService } from '@/_services';
    appService
      .then((response) => {
        response.json().then((data) => {
          if (!response.ok) {
            return toast.error(data?.message || 'Error while setting up your account.', { position: 'top-center' });
          }
          toast.success(`Added to the workspace${isSetPassword ? ' and password has been set ' : ' '}successfully.`, {
            position: 'top-center',
          });
          this.props.history.push('/login');
              {!this.single_organization ? (
                </>
              ) : (
                <>
                  <h2 className="card-title text-center mb-4">Set up your account</h2>
                  <div className="mb-3">
                    <label className="form-label">Password</label>
                    <div className="input-group input-group-flat">
                      <input
                        onChange={this.handleChange}
                        name="password"
                        type="password"
                        className="form-control"
                        autoComplete="off"
                      />
                      <span className="input-group-text"></span>
                    </div>
                  </div>
                  <div className="mb-3">
                    <label className="form-label">Confirm Password</label>
                    <div className="input-group input-group-flat">
                      <input
                        onChange={this.handleChange}
                        name="password_confirmation"
                        type="password"
                        className="form-control"
                        autoComplete="off"
                      />
                      <span className="input-group-text"></span>
                    </div>
                  </div>
                  <div className="form-footer">
                    <p>
                      By clicking the button below, you agree to our{' '}
                      <a href="https://tooljet.io/terms">Terms and Conditions</a>.
                    </p>
                    <button
                      className={`btn mt-2 btn-primary w-100 ${isLoading ? ' btn-loading' : ''}`}
                      onClick={(e) => this.acceptInvite(e, true)}
                      disabled={isLoading}
                    >
                      Finish account setup and accept invite
                    </button>
  setPasswordFromToken,
  acceptInvite,

function setPasswordFromToken({ token, password, organization, role, firstName, lastName, organizationToken }) {
  const body = {
    token,
    organizationToken,
    password,
    organization,
    role,
    first_name: firstName,
    last_name: lastName,
  };

  const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body) };
  return fetch(`${config.apiUrl}/set-password-from-token`, requestOptions).then(handleResponse);
}

function acceptInvite({ token, password }) {
  const body = {
    token,
    password,
  };

  const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body) };
  return fetch(`${config.apiUrl}/accept-invite`, requestOptions);
}
import { MultiOrganizationGuard } from 'src/modules/auth/multi-organization.guard';
import { SignupDisableGuard } from 'src/modules/auth/signup-disable.guard';
import { CreateUserDto } from '@dto/user.dto';
import { AcceptInviteDto } from '@dto/accept-organization-invite.dto';
  @UseGuards(MultiOrganizationGuard, SignupDisableGuard)
  @Post('set-password-from-token')
  async create(@Body() userCreateDto: CreateUserDto) {
    await this.authService.setupAccountFromInvitationToken(userCreateDto);
    return {};
  }

  @Post('accept-invite')
  async acceptInvite(@Body() acceptInviteDto: AcceptInviteDto) {
    await this.authService.acceptOrganizationInvite(acceptInviteDto);
    return {};
  }

  @UseGuards(MultiOrganizationGuard, SignupDisableGuard)
import { OrganizationsService } from '@services/organizations.service';
  constructor(
    private organizationUsersService: OrganizationUsersService,
    private organizationsService: OrganizationsService
  ) {}
    const result = await this.organizationsService.inviteNewUser(user, inviteNewUserDto);
import { Body, Controller, Post, Patch, UseGuards, UseInterceptors, UploadedFile } from '@nestjs/common';
import { UpdateUserDto } from '@dto/user.dto';
  async addAvatar(@User() user, @UploadedFile() file: Express.Multer.File) {
    return this.usersService.addAvatar(user.id, file.buffer, file.originalname);
import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
  @IsString()
  @IsOptional()
  @IsNotEmpty()
  organizationToken: string;

import {
  BadRequestException,
  Injectable,
  NotAcceptableException,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { OrganizationUser } from 'src/entities/organization_user.entity';
import { CreateUserDto } from '@dto/user.dto';
import { AcceptInviteDto } from '@dto/accept-organization-invite.dto';
    @InjectRepository(User)
    private usersRepository: Repository<User>,
    @InjectRepository(OrganizationUser)
    private organizationUsersRepository: Repository<OrganizationUser>,
    @InjectRepository(Organization)
    private organizationsRepository: Repository<Organization>,
  private async validateUser(email: string, password: string, organizationId?: string): Promise<User> {
    const user = await this.usersService.findByEmail(email, organizationId);
    if (existingUser?.organizationUsers?.some((ou) => ou.status === 'active')) {
    if (existingUser?.invitationToken) {
      await this.emailService.sendWelcomeEmail(
        existingUser.email,
        existingUser.firstName,
        existingUser.invitationToken
      );
      return;
    }


  async setupAccountFromInvitationToken(userCreateDto: CreateUserDto) {
    const {
      organization,
      password,
      token,
      role,
      first_name: firstName,
      last_name: lastName,
      organizationToken,
    } = userCreateDto;

    if (!token) {
      throw new BadRequestException('Invalid token');
    }

    const user: User = await this.usersRepository.findOne({ where: { invitationToken: token } });

    if (!user?.organizationUsers) {
      throw new BadRequestException('Invalid invitation link');
    }
    const organizationUser: OrganizationUser = user.organizationUsers.find(
      (ou) => ou.organizationId === user.defaultOrganizationId
    );

    if (!organizationUser) {
      throw new BadRequestException('Invalid invitation link');
    }

    await this.usersRepository.save(
      Object.assign(user, {
        firstName,
        lastName,
        password,
        role,
        invitationToken: null,
      })
    );

    await this.organizationUsersRepository.save(
      Object.assign(organizationUser, {
        invitationToken: null,
        status: 'active',
      })
    );

    if (organization) {
      await this.organizationsRepository.update(user.defaultOrganizationId, {
        name: organization,
      });
    }

    if (this.configService.get<string>('DISABLE_MULTI_WORKSPACE') !== 'true' && organizationToken) {
      const organizationUser = await this.organizationUsersRepository.findOne({
        where: { invitationToken: organizationToken },
      });

      if (organizationUser) {
        await this.organizationUsersRepository.save(
          Object.assign(organizationUser, {
            invitationToken: null,
            status: 'active',
          })
        );
      }
    }
  }

  async acceptOrganizationInvite(acceptInviteDto: AcceptInviteDto) {
    const { password, token } = acceptInviteDto;

    if (this.configService.get<string>('DISABLE_MULTI_WORKSPACE') === 'true' && !password) {
      throw new BadRequestException('Please enter password');
    }
    const organizationUser = await this.organizationUsersRepository.findOne({
      where: { invitationToken: token },
      relations: ['user'],
    });

    if (!organizationUser?.user) {
      throw new BadRequestException('Invalid invitation link');
    }
    const user: User = organizationUser.user;

    if (this.configService.get<string>('DISABLE_MULTI_WORKSPACE') !== 'true' && user.invitationToken) {
      // User sign up link send - not activated account
      this.emailService
        .sendWelcomeEmail(
          user.email,
          `${user.firstName} ${user.lastName}`,
          user.invitationToken,
          organizationUser.invitationToken
        )
        .catch((err) => console.error('Error while sending welcome mail', err));
      throw new UnauthorizedException(
        'User not exist in the workspace, Please setup your account using link shared via email'
      );
    }

    if (this.configService.get<string>('DISABLE_MULTI_WORKSPACE') === 'true') {
      // set new password
      await this.usersRepository.save(
        Object.assign(user, {
          ...(password ? { password } : {}),
          invitationToken: null,
        })
      );
    }

    await this.organizationUsersRepository.save(
      Object.assign(organizationUser, {
        invitationToken: null,
        status: 'active',
      })
    );
  }
  async sendWelcomeEmail(
    to: string,
    name: string,
    invitationtoken: string,
    organizationInvitationToken?: string,
    organizationName?: string,
    sender?: string
  ) {
    const inviteUrl = `${this.TOOLJET_HOST}/invitations/${invitationtoken}${
      organizationInvitationToken ? `/workspaces/${organizationInvitationToken}` : ''
    }`;
          ${
            organizationInvitationToken && sender && organizationName
              ? `<span>
              ${sender} has invited you to use ToolJet workspace: ${organizationName}.
            </span>`
              : ''
          }
    organizationName: string
          ${sender} has invited you to use ToolJet workspace: ${organizationName}. Use the link below to set up your account and get started.
import { EmailService } from './email.service';
import { InviteNewUserDto } from '@dto/invite-new-user.dto';
import { ConfigService } from '@nestjs/config';
    private encryptionService: EncryptionService,
    private emailService: EmailService,
    private configService: ConfigService

  async inviteNewUser(currentUser: User, inviteNewUserDto: InviteNewUserDto): Promise<OrganizationUser> {
    const userParams = <User>{
      firstName: inviteNewUserDto.first_name,
      lastName: inviteNewUserDto.last_name,
      email: inviteNewUserDto.email,
    };

    let user = await this.usersService.findByEmail(userParams.email);
    let defaultOrganisation: Organization,
      shouldSendWelcomeMail = false;

    if (user?.organizationUsers?.some((ou) => ou.organizationId === currentUser.organizationId)) {
      throw new BadRequestException('User with such email already exists.');
    }

    if (user?.invitationToken) {
      // user sign up not completed, name will be empty - updating name
      await this.usersService.update(user.id, { firstName: userParams.firstName, lastName: userParams.lastName });
    }

    if (!user && this.configService.get<string>('DISABLE_MULTI_WORKSPACE') !== 'true') {
      // User not exist
      shouldSendWelcomeMail = true;
      // Create default organization
      defaultOrganisation = await this.create('Untitled workspace');
    }
    user = await this.usersService.create(
      userParams,
      currentUser.organizationId,
      ['all_users'],
      user,
      true,
      defaultOrganisation?.id
    );

    if (defaultOrganisation) {
      // Setting up default organization
      await this.organizationUserService.create(user, defaultOrganisation, true);
      await this.usersService.attachUserGroup(['all_users', 'admin'], defaultOrganisation.id, user.id);
    }

    const currentOrganization: Organization = (
      await this.organizationUsersRepository.findOne({
        where: { userId: currentUser.id, organizationId: currentUser.organizationId },
        relations: ['organization'],
      })
    )?.organization;

    const organizationUser: OrganizationUser = await this.organizationUserService.create(
      user,
      currentOrganization,
      true
    );

    if (shouldSendWelcomeMail) {
      this.emailService
        .sendWelcomeEmail(
          user.email,
          user.firstName,
          user.invitationToken,
          organizationUser.invitationToken,
          currentOrganization.name,
          `${currentUser.firstName} ${currentUser.lastName}`
        )
        .catch((err) => console.error('Error while sending welcome mail', err));
    } else {
      this.emailService
        .sendOrganizationUserWelcomeEmail(
          user.email,
          user.firstName,
          `${currentUser.firstName} ${currentUser.lastName}`,
          organizationUser.invitationToken,
          currentOrganization.name
        )
        .catch((err) => console.error('Error while sending welcome mail', err));
    }
    return organizationUser;
  }
    isInvite?: boolean,
    defaultOrganizationId?: string
          defaultOrganizationId: defaultOrganizationId || organizationId,
    });

    await this.attachUserGroup(groups, organizationId, user.id);

    return user;
  }
  async attachUserGroup(groups, organizationId, userId) {
    await getManager().transaction(async (manager) => {
            userId: userId,
import { getManager, Repository, Not } from 'typeorm';
import { v4 as uuidv4 } from 'uuid';
        expect(response.statusCode).toBe(403);
  describe('POST /api/set-password-from-token', () => {
    beforeEach(() => {
      jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
        switch (key) {
          case 'DISABLE_MULTI_WORKSPACE':
            return 'false';
          default:
            return process.env[key];
        }
      });
    });
    it('should allow users to setup account after sign up using  Multi-Workspace', async () => {
      const invitationToken = uuidv4();
      const userData = await createUser(app, {
        email: 'signup@tooljet.io',
        invitationToken,
        status: 'invited',
      });
      const { user, organization } = userData;

      const response = await request(app.getHttpServer()).post('/api/set-password-from-token').send({
        first_name: 'signupuser',
        last_name: 'user',
        organization: 'org1',
        password: uuidv4(),
        token: invitationToken,
        role: 'developer',
      });

      expect(response.statusCode).toBe(201);

      const updatedUser = await getManager().findOneOrFail(User, { where: { email: user.email } });
      expect(updatedUser.firstName).toEqual('signupuser');
      expect(updatedUser.lastName).toEqual('user');
      expect(updatedUser.defaultOrganizationId).toEqual(organization.id);
      const organizationUser = await getManager().findOneOrFail(OrganizationUser, { where: { userId: user.id } });
      expect(organizationUser.status).toEqual('active');
    });

    it('should return error if required params are not present - Multi-Workspace', async () => {
      const invitationToken = uuidv4();
      await createUser(app, {
        email: 'signup@tooljet.io',
        invitationToken,
        status: 'invited',
      });

      const response = await request(app.getHttpServer()).post('/api/set-password-from-token');

      expect(response.statusCode).toBe(400);
      expect(response.body.message).toStrictEqual([
        'password should not be empty',
        'password must be a string',
        'token should not be empty',
        'token must be a string',
      ]);
    });

    it('should allow users to setup account for single organization only once', async () => {
      jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
        switch (key) {
          case 'DISABLE_MULTI_WORKSPACE':
            return 'true';
          default:
            return process.env[key];
        }
      });
      const invitationToken = uuidv4();
      await createUser(app, {
        email: 'signup@tooljet.io',
        invitationToken,
        status: 'invited',
      });

      let response = await request(app.getHttpServer()).post('/api/set-password-from-token').send({
        first_name: 'signupuser',
        last_name: 'user',
        organization: 'org1',
        password: uuidv4(),
        token: invitationToken,
        role: 'developer',
      });

      expect(response.statusCode).toBe(201);

      await createUser(app, {
        email: 'signup2@tooljet.io',
        invitationToken,
        status: 'invited',
      });

      response = await request(app.getHttpServer()).post('/api/set-password-from-token').send({
        first_name: 'signupuser2',
        last_name: 'user2',
        organization: 'org1',
        password: uuidv4(),
        token: invitationToken,
        role: 'developer',
      });

      expect(response.statusCode).toBe(403);
    });

    it('should not allow users to setup account for Multi-Workspace and sign up disabled', async () => {
      jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
        switch (key) {
          case 'DISABLE_SIGNUPS':
            return 'true';
          default:
            return process.env[key];
        }
      });
      const invitationToken = uuidv4();
      await createUser(app, {
        email: 'signup@tooljet.io',
        invitationToken,
        status: 'invited',
      });

      const response = await request(app.getHttpServer()).post('/api/set-password-from-token').send({
        first_name: 'signupuser',
        last_name: 'user',
        organization: 'org1',
        password: uuidv4(),
        token: invitationToken,
        role: 'developer',
      });

      expect(response.statusCode).toBe(403);
    });

    it('should allow users to sign up and setup account if already invited to an organization but not setup the account', async () => {
      const { organization: org, user: adminUser } = await createUser(app, {
        email: 'admin@tooljet.io',
      });

      await request(app.getHttpServer())
        .post(`/api/organization_users/`)
        .set('Authorization', authHeaderForUser(adminUser))
        .send({ email: 'invited@tooljet.io' })
        .expect(201);

      const signUpResponse = await request(app.getHttpServer())
        .post('/api/signup')
        .send({ email: 'invited@tooljet.io' });

      expect(signUpResponse.statusCode).toBe(201);

      const invitedUserDetails = await getManager().findOneOrFail(User, { where: { email: 'invited@tooljet.io' } });

      expect(invitedUserDetails.defaultOrganizationId).not.toBe(org.id);

      const response = await request(app.getHttpServer()).post('/api/set-password-from-token').send({
        first_name: 'signupuser',
        last_name: 'user',
        organization: 'org1',
        password: uuidv4(),
        token: invitedUserDetails.invitationToken,
        role: 'developer',
      });

      expect(response.statusCode).toBe(201);
      const updatedUser = await getManager().findOneOrFail(User, { where: { email: 'invited@tooljet.io' } });
      expect(updatedUser.firstName).toEqual('signupuser');
      expect(updatedUser.lastName).toEqual('user');
      expect(updatedUser.defaultOrganizationId).not.toBe(org.id);
      const organizationUser = await getManager().findOneOrFail(OrganizationUser, {
        where: { userId: Not(adminUser.id), organizationId: org.id },
      });
      const defaultOrganizationUser = await getManager().findOneOrFail(OrganizationUser, {
        where: { userId: Not(adminUser.id), organizationId: invitedUserDetails.defaultOrganizationId },
      });
      expect(organizationUser.status).toEqual('invited');
      expect(defaultOrganizationUser.status).toEqual('active');

      const acceptInviteResponse = await request(app.getHttpServer()).post('/api/accept-invite').send({
        token: organizationUser.invitationToken,
      });

      expect(acceptInviteResponse.statusCode).toBe(201);

      const organizationUserUpdated = await getManager().findOneOrFail(OrganizationUser, {
        where: { userId: Not(adminUser.id), organizationId: org.id },
      });
      expect(organizationUserUpdated.status).toEqual('active');
    });

    it('should allow users setup account and accept invite', async () => {
      const { organization: org, user: adminUser } = await createUser(app, {
        email: 'admin@tooljet.io',
      });

      await request(app.getHttpServer())
        .post(`/api/organization_users/`)
        .set('Authorization', authHeaderForUser(adminUser))
        .send({ email: 'invited@tooljet.io' })
        .expect(201);

      const invitedUserDetails = await getManager().findOneOrFail(User, { where: { email: 'invited@tooljet.io' } });

      expect(invitedUserDetails.defaultOrganizationId).not.toBe(org.id);

      const organizationUserBeforeUpdate = await getManager().findOneOrFail(OrganizationUser, {
        where: { userId: Not(adminUser.id), organizationId: org.id },
      });

      const response = await request(app.getHttpServer()).post('/api/set-password-from-token').send({
        first_name: 'signupuser',
        last_name: 'user',
        organization: 'org1',
        password: uuidv4(),
        token: invitedUserDetails.invitationToken,
        organizationToken: organizationUserBeforeUpdate.invitationToken,
        role: 'developer',
      });

      expect(response.statusCode).toBe(201);
      const updatedUser = await getManager().findOneOrFail(User, { where: { email: 'invited@tooljet.io' } });
      expect(updatedUser.firstName).toEqual('signupuser');
      expect(updatedUser.lastName).toEqual('user');
      expect(updatedUser.defaultOrganizationId).not.toBe(org.id);
      const organizationUser = await getManager().findOneOrFail(OrganizationUser, {
        where: { userId: Not(adminUser.id), organizationId: org.id },
      });
      const defaultOrganizationUser = await getManager().findOneOrFail(OrganizationUser, {
        where: { userId: Not(adminUser.id), organizationId: invitedUserDetails.defaultOrganizationId },
      });
      expect(organizationUser.status).toEqual('active');
      expect(defaultOrganizationUser.status).toEqual('active');

      const acceptInviteResponse = await request(app.getHttpServer()).post('/api/accept-invite').send({
        token: organizationUser.invitationToken,
      });

      expect(acceptInviteResponse.statusCode).toBe(400);
    });

    it('should not allow users to setup account if already invited to an organization and trying to accept invite before setting up account', async () => {
      const { organization: org, user: adminUser } = await createUser(app, {
        email: 'admin@tooljet.io',
      });

      await request(app.getHttpServer())
        .post(`/api/organization_users/`)
        .set('Authorization', authHeaderForUser(adminUser))
        .send({ email: 'invited@tooljet.io' })
        .expect(201);

      const signUpResponse = await request(app.getHttpServer())
        .post('/api/signup')
        .send({ email: 'invited@tooljet.io' });

      expect(signUpResponse.statusCode).toBe(201);

      const invitedUserDetails = await getManager().findOneOrFail(User, { where: { email: 'invited@tooljet.io' } });
      const orgUser = await getManager().findOneOrFail(OrganizationUser, {
        where: { userId: invitedUserDetails.id, organizationId: org.id },
      });

      expect(invitedUserDetails.defaultOrganizationId).not.toBe(org.id);

      const acceptInviteResponse = await request(app.getHttpServer()).post('/api/accept-invite').send({
        token: orgUser.invitationToken,
      });

      expect(acceptInviteResponse.statusCode).toBe(401);

      const organizationUser = await getManager().findOneOrFail(OrganizationUser, {
        where: { userId: invitedUserDetails.id, organizationId: org.id },
      });
      const defaultOrganizationUser = await getManager().findOneOrFail(OrganizationUser, {
        where: { userId: invitedUserDetails.id, organizationId: invitedUserDetails.defaultOrganizationId },
      });
      expect(organizationUser.status).toEqual('invited');
      expect(defaultOrganizationUser.status).toEqual('invited');

      const response = await request(app.getHttpServer()).post('/api/set-password-from-token').send({
        first_name: 'signupuser',
        last_name: 'user',
        organization: 'org1',
        password: uuidv4(),
        token: invitedUserDetails.invitationToken,
        role: 'developer',
      });

      expect(response.statusCode).toBe(201);
    });
  });

  describe('POST /api/accept-invite', () => {
    describe('Multi-Worlspace Enabled', () => {
      beforeEach(() => {
        jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
          switch (key) {
            case 'DISABLE_MULTI_WORKSPACE':
              return 'false';
            default:
              return process.env[key];
          }
        });
      });

      it('should allow users to accept invitation when Multi-Workspace is enabled', async () => {
        const userData = await createUser(app, {
          email: 'organizationUser@tooljet.io',
          status: 'invited',
        });

        const { user, orgUser } = userData;

        const response = await request(app.getHttpServer()).post('/api/accept-invite').send({
          token: orgUser.invitationToken,
        });

        expect(response.statusCode).toBe(201);

        const organizationUser = await getManager().findOneOrFail(OrganizationUser, { where: { userId: user.id } });
        expect(organizationUser.status).toEqual('active');
      });

      it('should not allow users to accept invitation when user sign up is not completed', async () => {
        const userData = await createUser(app, {
          email: 'organizationUser@tooljet.io',
          invitationToken: uuidv4(),
          status: 'invited',
        });
        const { user, orgUser } = userData;

        const response = await request(app.getHttpServer()).post('/api/accept-invite').send({
          token: orgUser.invitationToken,
        });

        expect(response.statusCode).toBe(401);
        expect(response.body.message).toBe(
          'User not exist in the workspace, Please setup your account using link shared via email'
        );
      });
    });

    describe('Multi-Worlspace Disabled', () => {
      beforeEach(() => {
        jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
          switch (key) {
            case 'DISABLE_MULTI_WORKSPACE':
              return 'true';
            default:
              return process.env[key];
          }
        });
      });

      it('should allow users to accept invitation when Multi-Workspace is disabled', async () => {
        const userData = await createUser(app, {
          email: 'organizationUser@tooljet.io',
          status: 'invited',
        });
        const { user, orgUser } = userData;

        const response = await request(app.getHttpServer()).post('/api/accept-invite').send({
          token: orgUser.invitationToken,
          password: uuidv4(),
        });

        expect(response.statusCode).toBe(201);

        const organizationUser = await getManager().findOneOrFail(OrganizationUser, { where: { userId: user.id } });
        expect(organizationUser.status).toEqual('active');
      });

      it('should not allow users to accept invitation when user not entered password for single workspace', async () => {
        const userData = await createUser(app, {
          email: 'organizationUser@tooljet.io',
          invitationToken: uuidv4(),
          status: 'invited',
        });
        const { orgUser } = userData;

        const response = await request(app.getHttpServer()).post('/api/accept-invite').send({
          token: orgUser.invitationToken,
        });

        expect(response.statusCode).toBe(400);
        expect(response.body.message).toBe('Please enter password');
      });
    });
  });

import { authHeaderForUser, clearDB, createUser, createNestAppInstance } from '../test.helper';
    app = await createNestAppInstance();