mirror of
https://github.com/ToolJet/ToolJet
synced 2026-04-21 13:37:28 +00:00
[improvement] Default workspace (#12834)
* Added set-default API * Setting default workspace for super-admin onboarding * Seperated the migrations * Added nestjs init * removed nestjs init * Added: default workspace case to signup * Fixed: instance signup * Fixed: existed non-active user instance signup * Added: SSO default workspace support * Added: Default workspace chooser * Moved some scss changes to ee folder * Added: disable workspace default organization check * updated the migration * Fixing .env issue * Removed the logs * Remove personal workspace check from enable signup * Fixing sign-in cases * Fixing workspace invited user's instance signup cases * Fixing sso workspace invited user's instance signup cases * fixing the workspace signup issue * Adding ee server and frontend file * Adding ee server and frontend file * Adding active check * Added query fix for the migration * Added migration logic fix * Removed/Commented the ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS env support from EE and CE * Adding server and frontend files * Added frontend file * Bump version
This commit is contained in:
parent
c0009b2c3a
commit
958cbd1d02
36 changed files with 425 additions and 137 deletions
2
.version
2
.version
|
|
@ -1 +1 @@
|
|||
3.12.0
|
||||
3.12.1
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
3.12.0
|
||||
3.12.1
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 518f3334b12a83785fd37dd53b0245d72848211a
|
||||
Subproject commit dd5796edccc8d5eda524d804a222845791d733fb
|
||||
|
|
@ -13,6 +13,7 @@ export const organizationService = {
|
|||
getWorkspacesLimit,
|
||||
checkWorkspaceUniqueness,
|
||||
updateOrganization,
|
||||
setDefaultWorkspace,
|
||||
};
|
||||
|
||||
function getUsersByValue(searchInput) {
|
||||
|
|
@ -100,3 +101,8 @@ function checkWorkspaceUniqueness(name, slug) {
|
|||
const query = queryString.stringify({ name, slug });
|
||||
return fetch(`${config.apiUrl}/organizations/is-unique?${query}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function setDefaultWorkspace(workspaceId) {
|
||||
const requestOptions = { method: 'PATCH', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/organizations/${workspaceId}/default`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10029,92 +10029,6 @@ tbody {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.manage-ws-table-body {
|
||||
width: 100%;
|
||||
|
||||
.workspace-table-row {
|
||||
border-bottom: 1px solid var(--slate5);
|
||||
height: 64px;
|
||||
width: 100%;
|
||||
|
||||
.ws-name {
|
||||
padding-left: 8px;
|
||||
|
||||
|
||||
.current-workspace-tag {
|
||||
font-weight: 500;
|
||||
color: var(--indigo9);
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
height: 21px;
|
||||
width: 130px;
|
||||
align-items: center;
|
||||
margin-left: 20px;
|
||||
padding: 4px 8px 5px 8px;
|
||||
border: 1px solid var(--indigo7);
|
||||
background-color: var(--indigo3);
|
||||
border-radius: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.open-button-cont {
|
||||
width: 44px;
|
||||
padding: 0px 8px 0px 8px;
|
||||
|
||||
.workspace-open-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background-color: var(--slate1);
|
||||
border: 1px solid var(--slate7);
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--slate4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.archive-btn-cont {
|
||||
width: 103px;
|
||||
padding-right: 8px;
|
||||
|
||||
.workspace-archive-btn {
|
||||
width: 95px;
|
||||
height: 28px;
|
||||
background-color: var(--slate1);
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--tomato7);
|
||||
color: var(--tomato9);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--tomato3);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
border: 1px solid var(--slate7);
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-active-btn {
|
||||
width: 95px;
|
||||
height: 28px;
|
||||
|
||||
background-color: var(--slate1);
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--slate7);
|
||||
color: var(--slate12);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--slate7);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.manage-workspace-table-wrap.dark-mode {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
3.12.0
|
||||
3.12.1
|
||||
|
|
|
|||
66
server/data-migrations/1740401100000-SetDefaultWorkspace.ts
Normal file
66
server/data-migrations/1740401100000-SetDefaultWorkspace.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import { TOOLJET_EDITIONS } from '@modules/app/constants';
|
||||
import { getCustomEnvVars, getTooljetEdition } from '@helpers/utils.helper';
|
||||
import { Organization } from '@entities/organization.entity';
|
||||
import { WORKSPACE_STATUS } from '@modules/users/constants/lifecycle';
|
||||
|
||||
export class SetDefaultWorkspace1740401100000 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
if (getTooljetEdition() !== TOOLJET_EDITIONS.EE) {
|
||||
console.log('Skipping migration as it is not EE edition');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if default workspace URL is configured
|
||||
const defaultWorkspaceUrl = getCustomEnvVars('TOOLJET_DEFAULT_WORKSPACE_URL');
|
||||
if (defaultWorkspaceUrl) {
|
||||
try {
|
||||
const url = new URL(defaultWorkspaceUrl);
|
||||
const pathParts = url.pathname.split('/');
|
||||
const workspaceSlug = pathParts[pathParts.length - 1];
|
||||
if (workspaceSlug) {
|
||||
const organization = await queryRunner.manager.findOne(Organization, {
|
||||
where: { slug: workspaceSlug, status: WORKSPACE_STATUS.ACTIVE },
|
||||
select: ['id'],
|
||||
});
|
||||
if (organization){
|
||||
await queryRunner.query(`
|
||||
UPDATE organizations
|
||||
SET is_default = true
|
||||
WHERE slug = $1
|
||||
`, [workspaceSlug]);
|
||||
return;
|
||||
}
|
||||
console.log(`No active organization found with slug: ${workspaceSlug}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Invalid TOOLJET_DEFAULT_WORKSPACE_URL format');
|
||||
}
|
||||
}
|
||||
|
||||
// Set the first created organization as default
|
||||
await queryRunner.query(`
|
||||
UPDATE organizations
|
||||
SET is_default = true
|
||||
WHERE id = (
|
||||
SELECT id
|
||||
FROM organizations
|
||||
WHERE status = '${WORKSPACE_STATUS.ACTIVE}'
|
||||
ORDER BY created_at ASC
|
||||
LIMIT 1
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
if (getTooljetEdition() !== TOOLJET_EDITIONS.EE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unset all default workspaces
|
||||
await queryRunner.query(`
|
||||
UPDATE organizations
|
||||
SET is_default = false;
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit b9e73f87b9062e06c49c2c73add6b82ba21dcacf
|
||||
Subproject commit 7848948f90077fa3fa02e43fc577a62d00f9a4da
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
|
||||
|
||||
export class AddIsDefaultToOrganizations1740401000000 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// Add is_default column
|
||||
await queryRunner.addColumn(
|
||||
'organizations',
|
||||
new TableColumn({
|
||||
name: 'is_default',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
isNullable: false,
|
||||
})
|
||||
);
|
||||
|
||||
// Create a partial unique index to ensure only one default workspace
|
||||
await queryRunner.query(`
|
||||
CREATE UNIQUE INDEX idx_organizations_single_default
|
||||
ON organizations (is_default)
|
||||
WHERE is_default = true;
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// Drop the unique index first
|
||||
await queryRunner.query(`DROP INDEX IF EXISTS idx_organizations_single_default;`);
|
||||
// Then drop the column
|
||||
await queryRunner.dropColumn('organizations', 'is_default');
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +35,9 @@ export class Organization extends BaseEntity {
|
|||
@Column({ name: 'domain' })
|
||||
domain: string;
|
||||
|
||||
@Column({ name: 'is_default', default: false })
|
||||
isDefault: boolean;
|
||||
|
||||
@Column({ name: 'enable_sign_up' })
|
||||
enableSignUp: boolean;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import { isEmpty } from 'lodash';
|
|||
import { USER_TYPE } from '@modules/users/constants/lifecycle';
|
||||
import { ConflictException } from '@nestjs/common';
|
||||
import { DataBaseConstraints } from './db_constraints.constants';
|
||||
import { getEnvVars } from 'scripts/database-config-utils';
|
||||
|
||||
|
||||
const semver = require('semver');
|
||||
|
||||
|
|
@ -449,5 +451,11 @@ export const getSubpath = () => {
|
|||
};
|
||||
|
||||
export function getTooljetEdition(): string {
|
||||
return process.env.TOOLJET_EDITION?.toLowerCase() || 'ce';
|
||||
const envVars = getEnvVars();
|
||||
return envVars['TOOLJET_EDITION']?.toLowerCase() || 'ce';
|
||||
}
|
||||
|
||||
export function getCustomEnvVars(name: string) {
|
||||
const envVars = getEnvVars();
|
||||
return envVars[name] || '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ export class OauthService implements IOAuthService {
|
|||
|
||||
// Not logging in to specific organization, creating new
|
||||
const { name, slug } = generateNextNameAndSlug('My workspace');
|
||||
defaultOrganization = await this.setupOrganizationsUtilService.create(name, slug, null, manager);
|
||||
defaultOrganization = await this.setupOrganizationsUtilService.create({ name, slug }, null, manager);
|
||||
|
||||
userDetails = await this.userRepository.createOrUpdate(
|
||||
{
|
||||
|
|
@ -221,7 +221,7 @@ export class OauthService implements IOAuthService {
|
|||
if (!isInviteRedirect) {
|
||||
// no SSO login enabled organization available for user - creating new one
|
||||
const { name, slug } = generateNextNameAndSlug('My workspace');
|
||||
organizationDetails = await this.setupOrganizationsUtilService.create(name, slug, userDetails, manager);
|
||||
organizationDetails = await this.setupOrganizationsUtilService.create({ name, slug }, userDetails, manager);
|
||||
await this.userRepository.updateOne(
|
||||
userDetails.id,
|
||||
{ defaultOrganizationId: organizationDetails.id },
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export class AuthService implements IAuthService {
|
|||
} else if (allowPersonalWorkspace && !isInviteRedirect) {
|
||||
// no form login enabled organization available for user - creating new one
|
||||
const { name, slug } = generateNextNameAndSlug('My workspace');
|
||||
organization = await this.setupOrganizationsUtilService.create(name, slug, user, manager);
|
||||
organization = await this.setupOrganizationsUtilService.create({ name, slug }, user, manager);
|
||||
} else {
|
||||
if (!isInviteRedirect) throw new UnauthorizedException('User is not assigned to any workspaces');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ export class AuthUtilService implements IAuthUtilService {
|
|||
|
||||
if (!user && allowPersonalWorkspace) {
|
||||
const { name, slug } = generateNextNameAndSlug('My workspace');
|
||||
defaultOrganization = await this.setupOrganizationsUtilService.create(name, slug, null, manager);
|
||||
defaultOrganization = await this.setupOrganizationsUtilService.create({ name, slug }, null, manager);
|
||||
}
|
||||
|
||||
const { source, status } = getUserStatusAndSource(lifecycleEvents.USER_SSO_ACTIVATE, sso);
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export class LoginConfigsService implements ILoginConfigsService {
|
|||
throw new NotFoundException();
|
||||
}
|
||||
if (!organizationId) {
|
||||
const result = this.loginConfigsUtilService.constructSSOConfigs();
|
||||
const result = await this.loginConfigsUtilService.constructSSOConfigs();
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ export class OnboardingController implements IOnboardingController {
|
|||
@InitFeature(FEATURE_KEY.SIGNUP)
|
||||
@UseGuards(
|
||||
SignupDisableGuard,
|
||||
AllowPersonalWorkspaceGuard,
|
||||
UserCountGuard,
|
||||
EditorUserCountGuard,
|
||||
FirstUserSignupDisableGuard,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export interface IOnboardingUtilService {
|
|||
signingUpOrganization: Organization,
|
||||
userParams: { firstName: string; lastName: string; password: string },
|
||||
redirectTo?: string,
|
||||
defaultWorkspace?: Organization,
|
||||
manager?: EntityManager
|
||||
): Promise<void>;
|
||||
processOrganizationSignup(
|
||||
|
|
@ -40,4 +41,10 @@ export interface IOnboardingUtilService {
|
|||
organizationInviteUrl: string;
|
||||
}>;
|
||||
splitName(name: string): { firstName: string; lastName: string };
|
||||
updateExistingUserDefaultWorkspace(
|
||||
userParams: { password: string; firstName: string; lastName: string },
|
||||
existingUser: User,
|
||||
defaultWorkspace: Organization,
|
||||
manager?: EntityManager
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,6 +119,9 @@ export class OnboardingService implements IOnboardingService {
|
|||
const { firstName, lastName } = names;
|
||||
const userParams = { email, password, firstName, lastName };
|
||||
|
||||
// Find the default workspace
|
||||
const defaultWorkspace = await this.organizationRepository. getDefaultWorkspaceOfInstance();
|
||||
|
||||
if (existingUser) {
|
||||
// Handling instance and workspace level signup for existing user
|
||||
return await this.onboardingUtilService.whatIfTheSignUpIsAtTheWorkspaceLevel(
|
||||
|
|
@ -126,9 +129,18 @@ export class OnboardingService implements IOnboardingService {
|
|||
signingUpOrganization,
|
||||
userParams,
|
||||
redirectTo,
|
||||
defaultWorkspace,
|
||||
manager
|
||||
);
|
||||
} else {
|
||||
if(defaultWorkspace && !signingUpOrganization) {
|
||||
return await this.onboardingUtilService.createUserInDefaultWorkspace(
|
||||
userParams,
|
||||
defaultWorkspace,
|
||||
redirectTo,
|
||||
manager
|
||||
);
|
||||
}
|
||||
return await this.onboardingUtilService.createUserOrPersonalWorkspace(
|
||||
userParams,
|
||||
existingUser,
|
||||
|
|
@ -149,8 +161,7 @@ export class OnboardingService implements IOnboardingService {
|
|||
const result = await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
// Create first organization
|
||||
const organization = await this.organizationRepository.createOne(
|
||||
workspace || 'My workspace',
|
||||
'my-workspace',
|
||||
{ name: workspace || 'My workspace', slug: 'my-workspace' },
|
||||
manager
|
||||
);
|
||||
|
||||
|
|
@ -226,7 +237,8 @@ export class OnboardingService implements IOnboardingService {
|
|||
(await this.instanceSettingsUtilService.getSettings(INSTANCE_USER_SETTINGS.ALLOW_PERSONAL_WORKSPACE)) ===
|
||||
'true';
|
||||
|
||||
if (!(allowPersonalWorkspace || organizationToken)) {
|
||||
const defaultWorkspace = await this.organizationRepository.getDefaultWorkspaceOfInstance();
|
||||
if (!(defaultWorkspace || allowPersonalWorkspace || organizationToken)) {
|
||||
throw new BadRequestException('Invalid invitation link');
|
||||
}
|
||||
if (organizationToken) {
|
||||
|
|
@ -251,7 +263,8 @@ export class OnboardingService implements IOnboardingService {
|
|||
throw new BadRequestException('Please enter password');
|
||||
}
|
||||
|
||||
if (allowPersonalWorkspace) {
|
||||
const activateDefaultWorkspace = (defaultWorkspace && defaultWorkspace.id === user.defaultOrganizationId) || allowPersonalWorkspace;
|
||||
if (activateDefaultWorkspace) {
|
||||
// Getting default workspace
|
||||
const defaultOrganizationUser: OrganizationUser = user.organizationUsers.find(
|
||||
(ou) => ou.organizationId === user.defaultOrganizationId
|
||||
|
|
@ -264,6 +277,14 @@ export class OnboardingService implements IOnboardingService {
|
|||
// Activate default workspace
|
||||
await this.organizationUsersUtilService.activateOrganization(defaultOrganizationUser, manager);
|
||||
|
||||
if(defaultWorkspace && defaultWorkspace.id === user.defaultOrganizationId){
|
||||
const personalWorkspaces = await this.organizationUsersUtilService.personalWorkspaces(user.id);
|
||||
for(const personalWorkspace of personalWorkspaces){
|
||||
// if any personal workspace left. activate those
|
||||
await this.organizationUsersUtilService.activateOrganization(personalWorkspace, manager);
|
||||
}
|
||||
}
|
||||
|
||||
if (workspaceName) {
|
||||
const { slug } = generateNextNameAndSlug('My workspace');
|
||||
await this.organizationRepository.updateOne(
|
||||
|
|
@ -449,10 +470,10 @@ export class OnboardingService implements IOnboardingService {
|
|||
onboarding_details: {
|
||||
status: user.onboardingStatus,
|
||||
password: isPasswordMandatory(user.source), // Should accept password if user is setting up first time
|
||||
questions:
|
||||
(this.configService.get<string>('ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS') === 'true' &&
|
||||
!organizationUser) || // Should ask onboarding questions if first user of the instance. If ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS=true, then will ask questions to all signup users
|
||||
(await this.userRepository.count({ where: { status: USER_STATUS.ACTIVE } })) === 0,
|
||||
// questions:
|
||||
// (this.configService.get<string>('ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS') === 'true' &&
|
||||
// !organizationUser) || // Should ask onboarding questions if first user of the instance. If ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS=true, then will ask questions to all signup users
|
||||
// (await this.userRepository.count({ where: { status: USER_STATUS.ACTIVE } })) === 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -686,8 +707,7 @@ export class OnboardingService implements IOnboardingService {
|
|||
// Create first organization
|
||||
const workspaceSlug = generateWorkspaceSlug(workspaceName || 'My workspace');
|
||||
const organization = await this.setupOrganizationsUtilService.create(
|
||||
workspaceName || 'My workspace',
|
||||
workspaceSlug,
|
||||
{ name: workspaceName || 'My workspace', slug: workspaceSlug },
|
||||
null,
|
||||
manager
|
||||
);
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ export class OnboardingUtilService implements IOnboardingUtilService {
|
|||
signingUpOrganization: Organization,
|
||||
userParams: { firstName: string; lastName: string; password: string },
|
||||
redirectTo?: string,
|
||||
defaultWorkspace?: Organization,
|
||||
manager?: EntityManager
|
||||
) => {
|
||||
return dbTransactionWrap(async (manager: EntityManager) => {
|
||||
|
|
@ -251,19 +252,28 @@ export class OnboardingUtilService implements IOnboardingUtilService {
|
|||
case hasWorkspaceInviteButUserWantsInstanceSignup: {
|
||||
const firstTimeSignup = ![SOURCE.SIGNUP, SOURCE.WORKSPACE_SIGNUP].includes(existingUser.source as SOURCE);
|
||||
if (firstTimeSignup) {
|
||||
if(defaultWorkspace) {
|
||||
return this.updateExistingUserDefaultWorkspace({
|
||||
password,
|
||||
firstName,
|
||||
lastName
|
||||
},existingUser, defaultWorkspace, manager);
|
||||
}
|
||||
|
||||
/* Invite user doing instance signup. So reset name fields and set password */
|
||||
let defaultOrganizationId = existingUser.defaultOrganizationId;
|
||||
const isPersonalWorkspaceAllowed =
|
||||
(await this.instanceSettingsUtilService.getSettings(INSTANCE_USER_SETTINGS.ALLOW_PERSONAL_WORKSPACE)) ===
|
||||
'true';
|
||||
if (!existingUser.defaultOrganizationId && isPersonalWorkspaceAllowed) {
|
||||
|
||||
if (!existingUser.defaultOrganizationId && isPersonalWorkspaceAllowed) {
|
||||
const personalWorkspaces = await this.organizationUsersUtilService.personalWorkspaces(existingUser.id);
|
||||
if (personalWorkspaces.length) {
|
||||
defaultOrganizationId = personalWorkspaces[0].organizationId;
|
||||
} else {
|
||||
/* Create a personal workspace for the user */
|
||||
const { name, slug } = generateNextNameAndSlug('My workspace');
|
||||
const defaultOrganization = await this.organizationRepository.createOne(name, slug, manager);
|
||||
const defaultOrganization = await this.organizationRepository.createOne({ name, slug }, manager);
|
||||
defaultOrganizationId = defaultOrganization.id;
|
||||
await this.organizationUserRepository.createOne(existingUser, defaultOrganization, true, manager);
|
||||
}
|
||||
|
|
@ -272,7 +282,6 @@ export class OnboardingUtilService implements IOnboardingUtilService {
|
|||
userId: existingUser.id,
|
||||
});
|
||||
}
|
||||
|
||||
await this.userRepository.updateOne(
|
||||
existingUser.id,
|
||||
{
|
||||
|
|
@ -398,7 +407,7 @@ export class OnboardingUtilService implements IOnboardingUtilService {
|
|||
let personalWorkspace: Organization;
|
||||
if (isPersonalWorkspaceEnabled) {
|
||||
const { name, slug } = generateNextNameAndSlug('My workspace');
|
||||
personalWorkspace = await this.setupOrganizationsUtilService.create(name, slug, null, manager);
|
||||
personalWorkspace = await this.setupOrganizationsUtilService.create({ name, slug }, null, manager);
|
||||
}
|
||||
const organizationRole = personalWorkspace ? USER_ROLE.ADMIN : USER_ROLE.END_USER;
|
||||
|
||||
|
|
@ -604,4 +613,130 @@ export class OnboardingUtilService implements IOnboardingUtilService {
|
|||
manager
|
||||
);
|
||||
}
|
||||
|
||||
createUserInDefaultWorkspace = async (
|
||||
userParams: { email: string; password: string; firstName: string; lastName: string },
|
||||
defaultWorkspace: Organization,
|
||||
redirectTo?: string,
|
||||
manager?: EntityManager
|
||||
) => {
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const { email, password, firstName, lastName } = userParams;
|
||||
|
||||
if (!defaultWorkspace) {
|
||||
throw new Error('No default workspace found in the instance');
|
||||
}
|
||||
|
||||
// Create user with end-user role in default workspace
|
||||
const lifeCycleParms = getUserStatusAndSource(lifecycleEvents.USER_SIGN_UP);
|
||||
|
||||
const user = await this.create(
|
||||
{
|
||||
email,
|
||||
password,
|
||||
...(firstName && { firstName }),
|
||||
...(lastName && { lastName }),
|
||||
...lifeCycleParms,
|
||||
},
|
||||
defaultWorkspace.id,
|
||||
USER_ROLE.END_USER,
|
||||
null,
|
||||
true,
|
||||
null,
|
||||
manager,
|
||||
false
|
||||
);
|
||||
|
||||
// Create organization user entry
|
||||
await this.organizationUserRepository.createOne(
|
||||
user,
|
||||
defaultWorkspace,
|
||||
true,
|
||||
manager,
|
||||
WORKSPACE_USER_SOURCE.SIGNUP
|
||||
);
|
||||
|
||||
// Validate license
|
||||
await this.licenseUserService.validateUser(manager);
|
||||
|
||||
// Send welcome email
|
||||
this.eventEmitter.emit('emailEvent', {
|
||||
type: EMAIL_EVENTS.SEND_WELCOME_EMAIL,
|
||||
payload: {
|
||||
to: user.email,
|
||||
name: user.firstName,
|
||||
invitationtoken: user.invitationToken,
|
||||
},
|
||||
});
|
||||
|
||||
return {};
|
||||
}, manager);
|
||||
};
|
||||
|
||||
updateExistingUserDefaultWorkspace = async (
|
||||
userParams: { password: string; firstName: string; lastName: string },
|
||||
existingUser: User,
|
||||
defaultWorkspace: Organization,
|
||||
manager?: EntityManager
|
||||
) => {
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const { password, firstName, lastName } = userParams;
|
||||
// Create organization user entry if not exists
|
||||
const existingOrgUser = await this.organizationUserRepository.findOne({
|
||||
where: {
|
||||
userId: existingUser.id,
|
||||
organizationId: defaultWorkspace.id,
|
||||
}
|
||||
});
|
||||
|
||||
if(existingOrgUser){
|
||||
throw new NotAcceptableException(
|
||||
'The user is already registered. Please check your inbox for the activation link'
|
||||
);
|
||||
}
|
||||
|
||||
// Update user's default organization ID
|
||||
await this.userRepository.updateOne(
|
||||
existingUser.id,
|
||||
{
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
source: SOURCE.SIGNUP,
|
||||
defaultOrganizationId: defaultWorkspace.id,
|
||||
},
|
||||
manager
|
||||
);
|
||||
|
||||
await this.organizationUserRepository.createOne(
|
||||
existingUser,
|
||||
defaultWorkspace,
|
||||
true,
|
||||
manager,
|
||||
WORKSPACE_USER_SOURCE.SIGNUP
|
||||
);
|
||||
|
||||
// Add end-user role in default workspace if not already present
|
||||
await this.rolesUtilService.addUserRole(
|
||||
defaultWorkspace.id,
|
||||
{ role: USER_ROLE.END_USER, userId: existingUser.id },
|
||||
manager
|
||||
);
|
||||
|
||||
// Validate license
|
||||
await this.licenseUserService.validateUser(manager);
|
||||
|
||||
// send welcome email
|
||||
this.eventEmitter.emit('emailEvent', {
|
||||
type: EMAIL_EVENTS.SEND_WELCOME_EMAIL,
|
||||
payload: {
|
||||
to: existingUser.email,
|
||||
name: existingUser.firstName,
|
||||
invitationtoken: existingUser.invitationToken,
|
||||
},
|
||||
});
|
||||
|
||||
return {};
|
||||
}, manager);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
lifecycleEvents,
|
||||
USER_STATUS,
|
||||
USER_TYPE,
|
||||
WORKSPACE_USER_SOURCE,
|
||||
WORKSPACE_USER_STATUS,
|
||||
} from '@modules/users/constants/lifecycle';
|
||||
import { BadRequestException, ConflictException, Injectable } from '@nestjs/common';
|
||||
|
|
@ -212,7 +213,7 @@ export class OrganizationUsersUtilService implements IOrganizationUsersUtilServi
|
|||
|
||||
async createDefaultOrganization(manager: EntityManager) {
|
||||
const { name, slug } = generateNextNameAndSlug('My workspace');
|
||||
return await this.setupOrganizationsUtilService.create(name, slug, null, manager);
|
||||
return await this.setupOrganizationsUtilService.create({ name, slug }, null, manager);
|
||||
}
|
||||
|
||||
addUserAsAdmin(userId: string, organizationId: string, manager: EntityManager) {
|
||||
|
|
@ -343,7 +344,7 @@ export class OrganizationUsersUtilService implements IOrganizationUsersUtilServi
|
|||
|
||||
async personalWorkspaces(userId: string): Promise<OrganizationUser[]> {
|
||||
const personalWorkspaces: Partial<OrganizationUser[]> = await this.organizationUsersRepository.find({
|
||||
select: ['organizationId', 'invitationToken'],
|
||||
select: ['organizationId', 'invitationToken', 'id'],
|
||||
where: { userId },
|
||||
});
|
||||
const personalWorkspaceArray: OrganizationUser[] = [];
|
||||
|
|
@ -578,4 +579,41 @@ export class OrganizationUsersUtilService implements IOrganizationUsersUtilServi
|
|||
user.organizationUserSource = organizationUser.source;
|
||||
return user;
|
||||
}
|
||||
|
||||
addUserToWorkspace = async (
|
||||
user: User,
|
||||
workspace: Organization,
|
||||
manager?: EntityManager
|
||||
) => {
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
// Create organization user entry if not exists
|
||||
let existingOrgUser = await this.organizationUsersRepository.findOne({
|
||||
where: {
|
||||
userId: user.id,
|
||||
organizationId: workspace.id,
|
||||
}
|
||||
});
|
||||
|
||||
if(existingOrgUser){
|
||||
return existingOrgUser;
|
||||
}
|
||||
|
||||
const organizationUser = await this.organizationUsersRepository.createOne(
|
||||
user,
|
||||
workspace,
|
||||
true,
|
||||
manager,
|
||||
WORKSPACE_USER_SOURCE.SIGNUP
|
||||
);
|
||||
|
||||
// Add end-user role in default workspace if not already present
|
||||
await this.rolesUtilService.addUserRole(
|
||||
workspace.id,
|
||||
{ role: USER_ROLE.END_USER, userId: user.id },
|
||||
manager
|
||||
);
|
||||
|
||||
return organizationUser;
|
||||
}, manager);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
|
|||
can([FEATURE_KEY.UPDATE, FEATURE_KEY.GET, FEATURE_KEY.CHECK_UNIQUE], Organization);
|
||||
}
|
||||
if (superAdmin) {
|
||||
can([FEATURE_KEY.WORKSPACE_STATUS_UPDATE], Organization);
|
||||
can([FEATURE_KEY.WORKSPACE_STATUS_UPDATE, FEATURE_KEY.SET_DEFAULT], Organization);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,5 +14,6 @@ export const FEATURES: FeaturesConfig = {
|
|||
[FEATURE_KEY.CHECK_UNIQUE_ONBOARDING]: {
|
||||
isPublic: true,
|
||||
},
|
||||
[FEATURE_KEY.SET_DEFAULT]: {},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,4 +26,5 @@ export enum FEATURE_KEY {
|
|||
CHECK_UNIQUE = 'check_unique',
|
||||
CREATE = 'create',
|
||||
CHECK_UNIQUE_ONBOARDING = 'check_unique_onboarding',
|
||||
SET_DEFAULT = 'set_default',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Body, Controller, Get, Patch, UseGuards, Query, Param } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Patch, UseGuards, Query, Param, NotImplementedException } from '@nestjs/common';
|
||||
import { OrganizationsService } from '@modules/organizations/service';
|
||||
import { decamelizeKeys } from 'humps';
|
||||
import { User } from '@modules/app/decorators/user.decorator';
|
||||
|
|
@ -17,7 +17,7 @@ import { OrganizationAuthGuard } from '@modules/session/guards/organization-auth
|
|||
@Controller('organizations')
|
||||
@InitModule(MODULES.ORGANIZATIONS)
|
||||
export class OrganizationsController implements IOrganizationsController {
|
||||
constructor(private organizationsService: OrganizationsService) {}
|
||||
constructor(protected organizationsService: OrganizationsService) {}
|
||||
|
||||
@InitFeature(FEATURE_KEY.GET)
|
||||
// TODO: Change to jwt auth guard - check why we need OrganizationAuthGuard here
|
||||
|
|
@ -41,6 +41,15 @@ export class OrganizationsController implements IOrganizationsController {
|
|||
await this.organizationsService.updateOrganizationNameAndSlug(user.organizationId, organizationUpdateDto);
|
||||
return;
|
||||
}
|
||||
|
||||
@InitFeature(FEATURE_KEY.SET_DEFAULT)
|
||||
@UseGuards(JwtAuthGuard, FeatureAbilityGuard)
|
||||
@Patch(':id/set-default')
|
||||
async setDefaultWorkspace(@Param('id') id: string) {
|
||||
await this.organizationsService.setDefaultWorkspace(id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Note : This endpoint is used for archive/unarchive workspaces.
|
||||
@InitFeature(FEATURE_KEY.WORKSPACE_STATUS_UPDATE)
|
||||
@UseGuards(JwtAuthGuard)
|
||||
|
|
|
|||
|
|
@ -11,4 +11,6 @@ export interface IOrganizationsController {
|
|||
checkWorkspaceUnique(name: string, slug: string): Promise<void>;
|
||||
|
||||
checkUniqueWorkspaceName(name: string): Promise<void>;
|
||||
|
||||
setDefaultWorkspace(id: string): Promise<void>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Organization } from 'src/entities/organization.entity';
|
||||
import { OrganizationUpdateDto, OrganizationStatusUpdateDto } from '@modules/organizations/dto';
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
export interface IOrganizationsService {
|
||||
fetchOrganizations(
|
||||
|
|
@ -15,4 +16,8 @@ export interface IOrganizationsService {
|
|||
updateOrganizationStatus(organizationId: string, updatableData: OrganizationStatusUpdateDto): Promise<Organization>;
|
||||
|
||||
checkWorkspaceUniqueness(name: string, slug: string): Promise<void>;
|
||||
|
||||
checkWorkspaceNameUniqueness(name: string): Promise<void>;
|
||||
|
||||
setDefaultWorkspace(organizationId: string, manager?: EntityManager): Promise<void>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { catchDbException, isSuperAdmin } from '@helpers/utils.helper';
|
|||
import { ConfigScope, SSOType } from '@entities/sso_config.entity';
|
||||
import { WORKSPACE_STATUS, WORKSPACE_USER_STATUS } from '@modules/users/constants/lifecycle';
|
||||
import { CONSTRAINTS } from './constants';
|
||||
import { OrganizationInputs } from '@modules/setup-organization/types/organization-inputs';
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationRepository extends Repository<Organization> {
|
||||
|
|
@ -106,7 +107,8 @@ export class OrganizationRepository extends Repository<Organization> {
|
|||
}, manager);
|
||||
}
|
||||
|
||||
createOne(name: string, slug: string, manager?: EntityManager): Promise<any> {
|
||||
createOne(organizationInputs: OrganizationInputs, manager?: EntityManager): Promise<any> {
|
||||
const { name, slug, isDefault } = organizationInputs;
|
||||
return dbTransactionWrap((manager: EntityManager) => {
|
||||
return catchDbException(() => {
|
||||
return manager.save(
|
||||
|
|
@ -120,6 +122,7 @@ export class OrganizationRepository extends Repository<Organization> {
|
|||
],
|
||||
name,
|
||||
slug,
|
||||
isDefault,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
|
|
@ -201,4 +204,27 @@ export class OrganizationRepository extends Repository<Organization> {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getDefaultWorkspaceOfInstance(): Promise<Organization>{
|
||||
return dbTransactionWrap(async (manager: EntityManager) => {
|
||||
try {
|
||||
return await manager.findOneOrFail(Organization, {
|
||||
where: { isDefault: true },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('No default workspace in this instance');
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async changeDefaultWorkspace(organizationId: string, manager?: EntityManager): Promise<void> {
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
// First, unset any existing default workspace
|
||||
await manager.update(Organization, { isDefault: true }, { isDefault: false });
|
||||
|
||||
// Then set the new default workspace
|
||||
await manager.update(Organization, { id: organizationId }, { isDefault: true });
|
||||
}, manager || this.manager);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ConflictException, Injectable, NotAcceptableException } from '@nestjs/common';
|
||||
import { ConflictException, Injectable, NotAcceptableException, NotImplementedException } from '@nestjs/common';
|
||||
import { Organization } from 'src/entities/organization.entity';
|
||||
import { isSuperAdmin } from 'src/helpers/utils.helper';
|
||||
import { dbTransactionWrap } from 'src/helpers/database.helper';
|
||||
|
|
@ -51,6 +51,11 @@ export class OrganizationsService implements IOrganizationsService {
|
|||
updatableData: OrganizationStatusUpdateDto
|
||||
): Promise<Organization> {
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const organization = await this.organizationRepository.findOne({ where: { id: organizationId } });
|
||||
if (organization.isDefault) {
|
||||
throw new NotAcceptableException('Default workspace cannot be archived');
|
||||
}
|
||||
|
||||
await this.organizationRepository.updateOne(organizationId, updatableData, manager);
|
||||
if (updatableData.status === WORKSPACE_STATUS.ACTIVE) {
|
||||
await this.licenseOrganizationService.validateOrganization(manager); //Check for only unarchiving
|
||||
|
|
@ -85,4 +90,8 @@ export class OrganizationsService implements IOrganizationsService {
|
|||
if (result) throw new ConflictException('Workspace name must be unique');
|
||||
return;
|
||||
}
|
||||
|
||||
async setDefaultWorkspace(organizationId: string, manager?: EntityManager): Promise<void> {
|
||||
throw new NotImplementedException('This feature is only available in Enterprise Edition');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ interface Features {
|
|||
[FEATURE_KEY.CREATE]: FeatureConfig;
|
||||
[FEATURE_KEY.CHECK_UNIQUE_ONBOARDING]: FeatureConfig;
|
||||
[FEATURE_KEY.WORKSPACE_STATUS_UPDATE]: FeatureConfig;
|
||||
[FEATURE_KEY.SET_DEFAULT]: FeatureConfig;
|
||||
}
|
||||
|
||||
export interface FeaturesConfig {
|
||||
|
|
|
|||
|
|
@ -368,8 +368,8 @@ export class SessionUtilService {
|
|||
async #onboardingFlags(user: User) {
|
||||
let isFirstUserOnboardingCompleted = true;
|
||||
let isOnboardingCompleted = true;
|
||||
const isOnboardingQuestionsEnabled =
|
||||
this.configService.get<string>('ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS') === 'true';
|
||||
// const isOnboardingQuestionsEnabled =
|
||||
// this.configService.get<string>('ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS') === 'true';
|
||||
|
||||
const instanceUsersCount = await this.userRepository.count({
|
||||
where: { status: USER_STATUS.ACTIVE },
|
||||
|
|
@ -383,14 +383,14 @@ export class SessionUtilService {
|
|||
}
|
||||
|
||||
/* Signed up user check */
|
||||
if (
|
||||
instanceUsersCount > 1 &&
|
||||
isOnboardingQuestionsEnabled &&
|
||||
user.onboardingStatus !== OnboardingStatus.ONBOARDING_COMPLETED
|
||||
) {
|
||||
/* Signed up user went through onboarding flow, didn't complete */
|
||||
isOnboardingCompleted = false;
|
||||
}
|
||||
// if (
|
||||
// instanceUsersCount > 1 &&
|
||||
// isOnboardingQuestionsEnabled &&
|
||||
// user.onboardingStatus !== OnboardingStatus.ONBOARDING_COMPLETED
|
||||
// ) {
|
||||
// /* Signed up user went through onboarding flow, didn't complete */
|
||||
// isOnboardingCompleted = false;
|
||||
// }
|
||||
|
||||
return { isFirstUserOnboardingCompleted, isOnboardingCompleted };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,8 +29,7 @@ export class SetupOrganizationsController implements ISetupOrganizationsControll
|
|||
@Res({ passthrough: true }) response: Response
|
||||
) {
|
||||
const result = await this.setupOrganizationsService.create(
|
||||
organizationCreateDto.name,
|
||||
organizationCreateDto.slug,
|
||||
{ name: organizationCreateDto.name, slug: organizationCreateDto.slug },
|
||||
user
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { User } from 'src/entities/user.entity';
|
||||
import { Organization } from 'src/entities/organization.entity';
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { OrganizationInputs } from '../types/organization-inputs';
|
||||
|
||||
export interface ISetupOrganizationsService {
|
||||
create(name: string, slug: string, user?: User, manager?: EntityManager): Promise<Organization>;
|
||||
create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise<Organization>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { User } from 'src/entities/user.entity';
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { Organization } from '@entities/organization.entity';
|
||||
import { OrganizationInputs } from '../types/organization-inputs';
|
||||
|
||||
export interface ISetupOrganizationsUtilService {
|
||||
create(name: string, slug: string, user?: User, manager?: EntityManager): Promise<Organization>;
|
||||
create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise<Organization>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ import { User } from 'src/entities/user.entity';
|
|||
import { EntityManager } from 'typeorm';
|
||||
import { SetupOrganizationsUtilService } from './util.service';
|
||||
import { ISetupOrganizationsService } from './interfaces/IService';
|
||||
import { OrganizationInputs } from './types/organization-inputs';
|
||||
|
||||
@Injectable()
|
||||
export class SetupOrganizationsService implements ISetupOrganizationsService {
|
||||
constructor(protected readonly setupOrganizationsUtilService: SetupOrganizationsUtilService) {}
|
||||
|
||||
async create(name: string, slug: string, user?: User, manager?: EntityManager): Promise<Organization> {
|
||||
return this.setupOrganizationsUtilService.create(name, slug, user, manager);
|
||||
async create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise<Organization> {
|
||||
return this.setupOrganizationsUtilService.create(organizationInputs, user, manager);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
export interface OrganizationInputs {
|
||||
name: string;
|
||||
slug: string;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import { OrganizationUsersRepository } from '@modules/organization-users/reposit
|
|||
import { SampleDataSourceService } from '@modules/data-sources/services/sample-ds.service';
|
||||
import { ISetupOrganizationsUtilService } from './interfaces/IUtilService';
|
||||
import { TooljetDbTableOperationsService } from '@modules/tooljet-db/services/tooljet-db-table-operations.service';
|
||||
import { OrganizationInputs } from './types/organization-inputs';
|
||||
|
||||
@Injectable()
|
||||
export class SetupOrganizationsUtilService implements ISetupOrganizationsUtilService {
|
||||
|
|
@ -31,9 +32,9 @@ export class SetupOrganizationsUtilService implements ISetupOrganizationsUtilSer
|
|||
protected readonly organizationUserRepository: OrganizationUsersRepository
|
||||
) {}
|
||||
|
||||
async create(name: string, slug: string, user?: User, manager?: EntityManager): Promise<Organization> {
|
||||
async create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise<Organization> {
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const organization = await this.organizationRepository.createOne(name, slug, manager);
|
||||
const organization = await this.organizationRepository.createOne(organizationInputs, manager);
|
||||
await this.appEnvironmentUtilService.createDefaultEnvironments(organization.id, manager);
|
||||
await this.groupPermissionUtilService.createDefaultGroups(organization.id, manager);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue