diff --git a/.version b/.version index 92536a9e48..171a6a93d6 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -3.12.0 +3.12.1 diff --git a/frontend/.version b/frontend/.version index 92536a9e48..171a6a93d6 100644 --- a/frontend/.version +++ b/frontend/.version @@ -1 +1 @@ -3.12.0 +3.12.1 diff --git a/frontend/ee b/frontend/ee index 518f3334b1..dd5796edcc 160000 --- a/frontend/ee +++ b/frontend/ee @@ -1 +1 @@ -Subproject commit 518f3334b12a83785fd37dd53b0245d72848211a +Subproject commit dd5796edccc8d5eda524d804a222845791d733fb diff --git a/frontend/src/_services/organization.service.js b/frontend/src/_services/organization.service.js index 2075e5d9de..481e8576c4 100644 --- a/frontend/src/_services/organization.service.js +++ b/frontend/src/_services/organization.service.js @@ -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); +} diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss index 1162f50687..b77c83ba30 100644 --- a/frontend/src/_styles/theme.scss +++ b/frontend/src/_styles/theme.scss @@ -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 { diff --git a/server/.version b/server/.version index 92536a9e48..171a6a93d6 100644 --- a/server/.version +++ b/server/.version @@ -1 +1 @@ -3.12.0 +3.12.1 diff --git a/server/data-migrations/1740401100000-SetDefaultWorkspace.ts b/server/data-migrations/1740401100000-SetDefaultWorkspace.ts new file mode 100644 index 0000000000..c337155fd8 --- /dev/null +++ b/server/data-migrations/1740401100000-SetDefaultWorkspace.ts @@ -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 { + 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 { + if (getTooljetEdition() !== TOOLJET_EDITIONS.EE) { + return; + } + + // Unset all default workspaces + await queryRunner.query(` + UPDATE organizations + SET is_default = false; + `); + } +} diff --git a/server/ee b/server/ee index b9e73f87b9..7848948f90 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit b9e73f87b9062e06c49c2c73add6b82ba21dcacf +Subproject commit 7848948f90077fa3fa02e43fc577a62d00f9a4da diff --git a/server/migrations/1740401000000-AddIsDefaultToOrganizations.ts b/server/migrations/1740401000000-AddIsDefaultToOrganizations.ts new file mode 100644 index 0000000000..6df32a7117 --- /dev/null +++ b/server/migrations/1740401000000-AddIsDefaultToOrganizations.ts @@ -0,0 +1,30 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +export class AddIsDefaultToOrganizations1740401000000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // 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 { + // 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'); + } +} diff --git a/server/src/entities/organization.entity.ts b/server/src/entities/organization.entity.ts index c1f8cb211a..865b124dcb 100644 --- a/server/src/entities/organization.entity.ts +++ b/server/src/entities/organization.entity.ts @@ -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; diff --git a/server/src/helpers/utils.helper.ts b/server/src/helpers/utils.helper.ts index 0eb0498812..0054a4a719 100644 --- a/server/src/helpers/utils.helper.ts +++ b/server/src/helpers/utils.helper.ts @@ -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] || ''; } diff --git a/server/src/modules/auth/oauth/service.ts b/server/src/modules/auth/oauth/service.ts index 79b2c41fa2..438673c29e 100644 --- a/server/src/modules/auth/oauth/service.ts +++ b/server/src/modules/auth/oauth/service.ts @@ -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 }, diff --git a/server/src/modules/auth/service.ts b/server/src/modules/auth/service.ts index 901dd99ad2..7c9fb3ad89 100644 --- a/server/src/modules/auth/service.ts +++ b/server/src/modules/auth/service.ts @@ -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'); } diff --git a/server/src/modules/auth/util.service.ts b/server/src/modules/auth/util.service.ts index 41d11c6bf9..e98402776e 100644 --- a/server/src/modules/auth/util.service.ts +++ b/server/src/modules/auth/util.service.ts @@ -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); diff --git a/server/src/modules/login-configs/service.ts b/server/src/modules/login-configs/service.ts index af7ed67143..34143b5fdd 100644 --- a/server/src/modules/login-configs/service.ts +++ b/server/src/modules/login-configs/service.ts @@ -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; } diff --git a/server/src/modules/onboarding/controller.ts b/server/src/modules/onboarding/controller.ts index b7d56938a3..a523e0e029 100644 --- a/server/src/modules/onboarding/controller.ts +++ b/server/src/modules/onboarding/controller.ts @@ -49,7 +49,6 @@ export class OnboardingController implements IOnboardingController { @InitFeature(FEATURE_KEY.SIGNUP) @UseGuards( SignupDisableGuard, - AllowPersonalWorkspaceGuard, UserCountGuard, EditorUserCountGuard, FirstUserSignupDisableGuard, diff --git a/server/src/modules/onboarding/interfaces/IUtilService.ts b/server/src/modules/onboarding/interfaces/IUtilService.ts index b0001e5b46..4a083de348 100644 --- a/server/src/modules/onboarding/interfaces/IUtilService.ts +++ b/server/src/modules/onboarding/interfaces/IUtilService.ts @@ -26,6 +26,7 @@ export interface IOnboardingUtilService { signingUpOrganization: Organization, userParams: { firstName: string; lastName: string; password: string }, redirectTo?: string, + defaultWorkspace?: Organization, manager?: EntityManager ): Promise; 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 + ) } diff --git a/server/src/modules/onboarding/service.ts b/server/src/modules/onboarding/service.ts index 3facd8357b..c05c9502d2 100644 --- a/server/src/modules/onboarding/service.ts +++ b/server/src/modules/onboarding/service.ts @@ -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('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('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 ); diff --git a/server/src/modules/onboarding/util.service.ts b/server/src/modules/onboarding/util.service.ts index 48c638b0b1..5c32ef89e8 100644 --- a/server/src/modules/onboarding/util.service.ts +++ b/server/src/modules/onboarding/util.service.ts @@ -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); + }; } diff --git a/server/src/modules/organization-users/util.service.ts b/server/src/modules/organization-users/util.service.ts index 618291b732..c040e2cff7 100644 --- a/server/src/modules/organization-users/util.service.ts +++ b/server/src/modules/organization-users/util.service.ts @@ -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 { const personalWorkspaces: Partial = 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); + }; } diff --git a/server/src/modules/organizations/ability/index.ts b/server/src/modules/organizations/ability/index.ts index acfd4a6078..49eaa256dd 100644 --- a/server/src/modules/organizations/ability/index.ts +++ b/server/src/modules/organizations/ability/index.ts @@ -44,7 +44,7 @@ export class FeatureAbilityFactory extends AbilityFactory 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); } } } diff --git a/server/src/modules/organizations/constants/feature.ts b/server/src/modules/organizations/constants/feature.ts index af1c847553..7276f855fb 100644 --- a/server/src/modules/organizations/constants/feature.ts +++ b/server/src/modules/organizations/constants/feature.ts @@ -14,5 +14,6 @@ export const FEATURES: FeaturesConfig = { [FEATURE_KEY.CHECK_UNIQUE_ONBOARDING]: { isPublic: true, }, + [FEATURE_KEY.SET_DEFAULT]: {}, }, }; diff --git a/server/src/modules/organizations/constants/index.ts b/server/src/modules/organizations/constants/index.ts index b829014d86..45b2f85044 100644 --- a/server/src/modules/organizations/constants/index.ts +++ b/server/src/modules/organizations/constants/index.ts @@ -26,4 +26,5 @@ export enum FEATURE_KEY { CHECK_UNIQUE = 'check_unique', CREATE = 'create', CHECK_UNIQUE_ONBOARDING = 'check_unique_onboarding', + SET_DEFAULT = 'set_default', } diff --git a/server/src/modules/organizations/controller.ts b/server/src/modules/organizations/controller.ts index 0dcc325048..2de2ed9936 100644 --- a/server/src/modules/organizations/controller.ts +++ b/server/src/modules/organizations/controller.ts @@ -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) diff --git a/server/src/modules/organizations/interfaces/IController.ts b/server/src/modules/organizations/interfaces/IController.ts index ad5feb0638..2654fe53db 100644 --- a/server/src/modules/organizations/interfaces/IController.ts +++ b/server/src/modules/organizations/interfaces/IController.ts @@ -11,4 +11,6 @@ export interface IOrganizationsController { checkWorkspaceUnique(name: string, slug: string): Promise; checkUniqueWorkspaceName(name: string): Promise; + + setDefaultWorkspace(id: string): Promise; } diff --git a/server/src/modules/organizations/interfaces/IService.ts b/server/src/modules/organizations/interfaces/IService.ts index aad08934d0..658418e4f5 100644 --- a/server/src/modules/organizations/interfaces/IService.ts +++ b/server/src/modules/organizations/interfaces/IService.ts @@ -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; checkWorkspaceUniqueness(name: string, slug: string): Promise; + + checkWorkspaceNameUniqueness(name: string): Promise; + + setDefaultWorkspace(organizationId: string, manager?: EntityManager): Promise; } diff --git a/server/src/modules/organizations/repository.ts b/server/src/modules/organizations/repository.ts index 240639dbe3..25ef5e7929 100644 --- a/server/src/modules/organizations/repository.ts +++ b/server/src/modules/organizations/repository.ts @@ -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 { @@ -106,7 +107,8 @@ export class OrganizationRepository extends Repository { }, manager); } - createOne(name: string, slug: string, manager?: EntityManager): Promise { + createOne(organizationInputs: OrganizationInputs, manager?: EntityManager): Promise { + const { name, slug, isDefault } = organizationInputs; return dbTransactionWrap((manager: EntityManager) => { return catchDbException(() => { return manager.save( @@ -120,6 +122,7 @@ export class OrganizationRepository extends Repository { ], name, slug, + isDefault, createdAt: new Date(), updatedAt: new Date(), }) @@ -201,4 +204,27 @@ export class OrganizationRepository extends Repository { }); }); } + + async getDefaultWorkspaceOfInstance(): Promise{ + 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 { + 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); + } } diff --git a/server/src/modules/organizations/service.ts b/server/src/modules/organizations/service.ts index b2a25570c9..8e8f715399 100644 --- a/server/src/modules/organizations/service.ts +++ b/server/src/modules/organizations/service.ts @@ -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 { 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 { + throw new NotImplementedException('This feature is only available in Enterprise Edition'); + } } diff --git a/server/src/modules/organizations/types/index.ts b/server/src/modules/organizations/types/index.ts index 1b3321eae6..ea5fc8c08f 100644 --- a/server/src/modules/organizations/types/index.ts +++ b/server/src/modules/organizations/types/index.ts @@ -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 { diff --git a/server/src/modules/session/util.service.ts b/server/src/modules/session/util.service.ts index 92a12e1d88..282a6179e5 100644 --- a/server/src/modules/session/util.service.ts +++ b/server/src/modules/session/util.service.ts @@ -368,8 +368,8 @@ export class SessionUtilService { async #onboardingFlags(user: User) { let isFirstUserOnboardingCompleted = true; let isOnboardingCompleted = true; - const isOnboardingQuestionsEnabled = - this.configService.get('ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS') === 'true'; + // const isOnboardingQuestionsEnabled = + // this.configService.get('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 }; } diff --git a/server/src/modules/setup-organization/controller.ts b/server/src/modules/setup-organization/controller.ts index 7b377738fd..0b16e05b65 100644 --- a/server/src/modules/setup-organization/controller.ts +++ b/server/src/modules/setup-organization/controller.ts @@ -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 ); diff --git a/server/src/modules/setup-organization/interfaces/IService.ts b/server/src/modules/setup-organization/interfaces/IService.ts index 557b438223..4e463784c3 100644 --- a/server/src/modules/setup-organization/interfaces/IService.ts +++ b/server/src/modules/setup-organization/interfaces/IService.ts @@ -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; + create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise; } diff --git a/server/src/modules/setup-organization/interfaces/IUtilService.ts b/server/src/modules/setup-organization/interfaces/IUtilService.ts index 0aaa5a9350..874ac52c35 100644 --- a/server/src/modules/setup-organization/interfaces/IUtilService.ts +++ b/server/src/modules/setup-organization/interfaces/IUtilService.ts @@ -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; + create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise; } diff --git a/server/src/modules/setup-organization/service.ts b/server/src/modules/setup-organization/service.ts index 78a066e0bb..925e8a75eb 100644 --- a/server/src/modules/setup-organization/service.ts +++ b/server/src/modules/setup-organization/service.ts @@ -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 { - return this.setupOrganizationsUtilService.create(name, slug, user, manager); + async create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise { + return this.setupOrganizationsUtilService.create(organizationInputs, user, manager); } } diff --git a/server/src/modules/setup-organization/types/organization-inputs.ts b/server/src/modules/setup-organization/types/organization-inputs.ts new file mode 100644 index 0000000000..af427cf785 --- /dev/null +++ b/server/src/modules/setup-organization/types/organization-inputs.ts @@ -0,0 +1,5 @@ +export interface OrganizationInputs { + name: string; + slug: string; + isDefault?: boolean; +} diff --git a/server/src/modules/setup-organization/util.service.ts b/server/src/modules/setup-organization/util.service.ts index e053e60b9e..8eac88ec33 100644 --- a/server/src/modules/setup-organization/util.service.ts +++ b/server/src/modules/setup-organization/util.service.ts @@ -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 { + async create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise { 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);