24 changes: 4 additions & 20 deletions server/src/controllers/users.controller.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,16 @@
import { Body, Controller, Post, Patch, UseGuards, UseInterceptors, Req, UploadedFile } from '@nestjs/common';
import { Body, Controller, Post, Patch, UseGuards, UseInterceptors, UploadedFile } from '@nestjs/common';
import { Express } from 'express';
import { FileInterceptor } from '@nestjs/platform-express';
import { JwtAuthGuard } from 'src/modules/auth/jwt-auth.guard';
import { PasswordRevalidateGuard } from 'src/modules/auth/password-revalidate.guard';
import { UsersService } from 'src/services/users.service';
import { User } from 'src/decorators/user.decorator';
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';
import { UpdateUserDto } from '@dto/user.dto';

@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}

@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 {};
}

@UseGuards(JwtAuthGuard)
@Patch('update')
async update(@User() user, @Body() updateUserDto: UpdateUserDto) {
Expand All @@ -42,8 +26,8 @@ export class UsersController {
@Post('avatar')
@UseGuards(JwtAuthGuard)
@UseInterceptors(FileInterceptor('file'))
async addAvatar(@Req() req, @UploadedFile() file: Express.Multer.File) {
return this.usersService.addAvatar(req.user.id, file.buffer, file.originalname);
async addAvatar(@User() user, @UploadedFile() file: Express.Multer.File) {
return this.usersService.addAvatar(user.id, file.buffer, file.originalname);
}

@UseGuards(JwtAuthGuard, PasswordRevalidateGuard)
Expand Down
2 changes: 1 addition & 1 deletion server/src/dto/accept-organization-invite.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IsString, IsOptional, IsNotEmpty } from 'class-validator';
import { IsString, IsNotEmpty, IsOptional } from 'class-validator';

export class AcceptInviteDto {
@IsString()
Expand Down
5 changes: 5 additions & 0 deletions server/src/dto/user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export class CreateUserDto {
@IsNotEmpty()
password: string;

@IsString()
@IsOptional()
@IsNotEmpty()
organizationToken: string;

@IsString()
@IsNotEmpty()
token: string;
Expand Down
150 changes: 146 additions & 4 deletions server/src/services/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { Injectable, NotAcceptableException, NotFoundException, UnauthorizedException } from '@nestjs/common';
import {
BadRequestException,
Injectable,
NotAcceptableException,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { OrganizationsService } from './organizations.service';
import { JwtService } from '@nestjs/jwt';
Expand All @@ -9,12 +15,23 @@ import { decamelizeKeys } from 'humps';
import { Organization } from 'src/entities/organization.entity';
import { ConfigService } from '@nestjs/config';
import { SSOConfigs } from 'src/entities/sso_config.entity';
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';
const bcrypt = require('bcrypt');
const uuid = require('uuid');

@Injectable()
export class AuthService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
@InjectRepository(OrganizationUser)
private organizationUsersRepository: Repository<OrganizationUser>,
@InjectRepository(Organization)
private organizationsRepository: Repository<Organization>,
private usersService: UsersService,
private jwtService: JwtService,
private organizationsService: OrganizationsService,
Expand All @@ -32,8 +49,8 @@ export class AuthService {
}
}

private async validateUser(email: string, password: string, organisationId?: string): Promise<User> {
const user = await this.usersService.findByEmail(email, organisationId);
private async validateUser(email: string, password: string, organizationId?: string): Promise<User> {
const user = await this.usersService.findByEmail(email, organizationId);

if (!user) return null;

Expand Down Expand Up @@ -169,10 +186,19 @@ export class AuthService {

async signup(email: string) {
const existingUser = await this.usersService.findByEmail(email);
if (existingUser?.invitationToken || existingUser?.organizationUsers?.some((ou) => ou.status === 'active')) {
if (existingUser?.organizationUsers?.some((ou) => ou.status === 'active')) {
throw new NotAcceptableException('Email already exists');
}

if (existingUser?.invitationToken) {
await this.emailService.sendWelcomeEmail(
existingUser.email,
existingUser.firstName,
existingUser.invitationToken
);
return;
}

let organization: Organization;
// Check if the configs allows user signups
if (this.configService.get<string>('DISABLE_MULTI_WORKSPACE') === 'true') {
Expand Down Expand Up @@ -215,4 +241,120 @@ export class AuthService {
});
}
}

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',
})
);
}
}
24 changes: 20 additions & 4 deletions server/src/services/email.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,18 @@ export class EmailService {
return hostname?.endsWith('/') ? hostname.slice(0, -1) : hostname;
}

async sendWelcomeEmail(to: string, name: string, invitationtoken: string) {
async sendWelcomeEmail(
to: string,
name: string,
invitationtoken: string,
organizationInvitationToken?: string,
organizationName?: string,
sender?: string
) {
const subject = 'Welcome to ToolJet';
const inviteUrl = `${this.TOOLJET_HOST}/invitations/${invitationtoken}`;
const inviteUrl = `${this.TOOLJET_HOST}/invitations/${invitationtoken}${
organizationInvitationToken ? `/workspaces/${organizationInvitationToken}` : ''
}`;
const html = `
<!DOCTYPE html>
<html>
Expand All @@ -64,6 +73,13 @@ export class EmailService {
</head>
<body>
<p>Hi ${name || ''},</p>
${
organizationInvitationToken && sender && organizationName
? `<span>
${sender} has invited you to use ToolJet workspace: ${organizationName}.
</span>`
: ''
}
<span>
Please use the link below to set up your account and get started.
</span>
Expand All @@ -86,7 +102,7 @@ export class EmailService {
name: string,
sender: string,
invitationtoken: string,
organisationName: string
organizationName: string
) {
const subject = 'Welcome to ToolJet';
const inviteUrl = `${this.TOOLJET_HOST}/organization-invitations/${invitationtoken}`;
Expand All @@ -100,7 +116,7 @@ export class EmailService {
<p>Hi ${name || ''},</p>
<br>
<span>
${sender} has invited you to use ToolJet workspace ${organisationName}. Use the link below to set up your account and get started.
${sender} has invited you to use ToolJet workspace: ${organizationName}. Use the link below to set up your account and get started.
</span>
<br>
<a href="${inviteUrl}">${inviteUrl}</a>
Expand Down
41 changes: 0 additions & 41 deletions server/src/services/organization_users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { BadRequestException } from '@nestjs/common';
import { EmailService } from './email.service';
import { Organization } from 'src/entities/organization.entity';
import { GroupPermission } from 'src/entities/group_permission.entity';
import { InviteNewUserDto } from '@dto/invite-new-user.dto';
const uuid = require('uuid');

@Injectable()
Expand All @@ -24,46 +23,6 @@ export class OrganizationUsersService {
return await this.organizationUsersRepository.findOne({ where: { id } });
}

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;
}

async create(user: User, organization: Organization, isInvite?: boolean): Promise<OrganizationUser> {
return await this.organizationUsersRepository.save(
this.organizationUsersRepository.create({
Expand Down
86 changes: 85 additions & 1 deletion server/src/services/organizations.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import { User } from 'src/entities/user.entity';
import { cleanObject } from 'src/helpers/utils.helper';
import { createQueryBuilder, Repository } from 'typeorm';
import { OrganizationUser } from '../entities/organization_user.entity';
import { EmailService } from './email.service';
import { EncryptionService } from './encryption.service';
import { GroupPermissionsService } from './group_permissions.service';
import { OrganizationUsersService } from './organization_users.service';
import { UsersService } from './users.service';
import { InviteNewUserDto } from '@dto/invite-new-user.dto';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class OrganizationsService {
Expand All @@ -26,7 +29,9 @@ export class OrganizationsService {
private usersService: UsersService,
private organizationUserService: OrganizationUsersService,
private groupPermissionService: GroupPermissionsService,
private encryptionService: EncryptionService
private encryptionService: EncryptionService,
private emailService: EmailService,
private configService: ConfigService
) {}

async create(name: string, user?: User): Promise<Organization> {
Expand Down Expand Up @@ -308,4 +313,83 @@ export class OrganizationsService {
await this.decryptSecret(result?.configs);
return result;
}

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;
}
}
119 changes: 12 additions & 107 deletions server/src/services/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from '../entities/user.entity';
import { FilesService } from '../services/files.service';
import { Organization } from 'src/entities/organization.entity';
import { App } from 'src/entities/app.entity';
import { Connection, createQueryBuilder, EntityManager, getManager, getRepository, In, Repository } from 'typeorm';
import { OrganizationUser } from '../entities/organization_user.entity';
Expand All @@ -11,7 +10,6 @@ import { UserGroupPermission } from 'src/entities/user_group_permission.entity';
import { GroupPermission } from 'src/entities/group_permission.entity';
import { BadRequestException } from '@nestjs/common';
import { cleanObject } from 'src/helpers/utils.helper';
import { CreateUserDto } from '@dto/user.dto';
import { CreateFileDto } from '@dto/create-file.dto';
const uuid = require('uuid');
const bcrypt = require('bcrypt');
Expand All @@ -25,8 +23,6 @@ export class UsersService {
private usersRepository: Repository<User>,
@InjectRepository(OrganizationUser)
private organizationUsersRepository: Repository<OrganizationUser>,
@InjectRepository(Organization)
private organizationsRepository: Repository<Organization>,
@InjectRepository(App)
private appsRepository: Repository<App>
) {}
Expand Down Expand Up @@ -65,7 +61,8 @@ export class UsersService {
organizationId: string,
groups?: string[],
existingUser?: User,
isInvite?: boolean
isInvite?: boolean,
defaultOrganizationId?: string
): Promise<User> {
const password = uuid.v4();

Expand All @@ -80,24 +77,23 @@ export class UsersService {
lastName,
password,
invitationToken: isInvite ? uuid.v4() : null,
defaultOrganizationId: organizationId,
defaultOrganizationId: defaultOrganizationId || organizationId,
createdAt: new Date(),
updatedAt: new Date(),
});
await manager.save(user);
} else {
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,
})
);
}
user = existingUser;
}
});

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

return user;
}

async attachUserGroup(groups, organizationId, userId) {
await getManager().transaction(async (manager) => {
for (const group of groups) {
const orgGroupPermission = await manager.findOne(GroupPermission, {
where: {
Expand All @@ -109,16 +105,14 @@ export class UsersService {
if (orgGroupPermission) {
const userGroupPermission = manager.create(UserGroupPermission, {
groupPermissionId: orgGroupPermission.id,
userId: user.id,
userId: userId,
});
await manager.save(userGroupPermission);
} else {
throw new BadRequestException(`${group} group does not exist for current organization`);
}
}
});

return user;
}

async status(user: User) {
Expand All @@ -144,95 +138,6 @@ export class UsersService {
return { user, newUserCreated };
}

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',
})
);
}

async updateDefaultOrganization(user: User, organizationId: string) {
await this.usersRepository.update(user.id, { defaultOrganizationId: organizationId });
}
Expand Down
392 changes: 390 additions & 2 deletions server/test/controllers/app.e2e-spec.ts

Large diffs are not rendered by default.

272 changes: 2 additions & 270 deletions server/test/controllers/users.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import * as request from 'supertest';
import { INestApplication } from '@nestjs/common';
import { authHeaderForUser, clearDB, createUser, createNestAppInstanceWithEnvMock } from '../test.helper';
import { authHeaderForUser, clearDB, createUser, createNestAppInstance } from '../test.helper';
import { getManager } from 'typeorm';
import { User } from 'src/entities/user.entity';
import { v4 as uuidv4 } from 'uuid';
import { OrganizationUser } from 'src/entities/organization_user.entity';
const path = require('path');

describe('users controller', () => {
let app: INestApplication;
let mockConfig;

beforeEach(async () => {
await clearDB();
});

beforeAll(async () => {
({ app, mockConfig } = await createNestAppInstanceWithEnvMock());
app = await createNestAppInstance();
});

afterEach(() => {
Expand Down Expand Up @@ -82,271 +79,6 @@ describe('users controller', () => {
});
});

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');
});
});

describe('POST /api/users/avatar', () => {
it('should allow users to add a avatar', async () => {
const userData = await createUser(app, { email: 'admin@tooljet.io' });
Expand Down