From d1a68a927c4f9356ae115b0f544e0ae222bcc1a2 Mon Sep 17 00:00:00 2001 From: Siddharthpl Date: Mon, 22 Dec 2025 10:24:15 +0530 Subject: [PATCH 01/10] Added allowed and restricted domain for password login and signup. --- ...1765958549099-AddGranularDomainSettings.ts | 46 ++++ ...AddPasswordDomainColumnsToOrganizations.ts | 30 +++ server/src/entities/organization.entity.ts | 6 + server/src/helpers/utils.helper.ts | 200 ++++++++++++++++++ server/src/modules/auth/service.ts | 34 ++- server/src/modules/configs/service.ts | 2 + .../instance-settings/constants/index.ts | 2 + server/src/modules/login-configs/dto/index.ts | 24 +++ server/src/modules/login-configs/service.ts | 4 +- server/src/modules/onboarding/service.ts | 9 +- 10 files changed, 352 insertions(+), 5 deletions(-) create mode 100644 server/data-migrations/1765958549099-AddGranularDomainSettings.ts create mode 100644 server/migrations/1766224841342-AddPasswordDomainColumnsToOrganizations.ts diff --git a/server/data-migrations/1765958549099-AddGranularDomainSettings.ts b/server/data-migrations/1765958549099-AddGranularDomainSettings.ts new file mode 100644 index 0000000000..91b39c2112 --- /dev/null +++ b/server/data-migrations/1765958549099-AddGranularDomainSettings.ts @@ -0,0 +1,46 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; +import { InstanceSettings } from '@entities/instance_settings.entity'; +import { INSTANCE_CONFIGS_DATA_TYPES } from '@modules/instance-settings/constants'; +import { INSTANCE_SETTINGS_TYPE, INSTANCE_SYSTEM_SETTINGS } from '@modules/instance-settings/constants'; +export class AddGranularDomainSettings1765958549099 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + const entityManager = queryRunner.manager; + + // Query existing ALLOWED_DOMAINS to prefill PASSWORD_ALLOWED_DOMAINS + const existingAllowedDomains = await entityManager.findOne(InstanceSettings, { + where: { key: INSTANCE_SYSTEM_SETTINGS.ALLOWED_DOMAINS } + }); + const existingAllowedDomainsValue = existingAllowedDomains?.value || ''; + + const newSettings = [ + { + key: 'PASSWORD_ALLOWED_DOMAINS', + label: 'Password Allowed Domains', + dataType: INSTANCE_CONFIGS_DATA_TYPES.TEXT, + value: existingAllowedDomainsValue, + type: INSTANCE_SETTINGS_TYPE.SYSTEM, + createdAt: new Date(), + }, + { + key: 'PASSWORD_RESTRICTED_DOMAINS', + label: 'Password Restricted Domains', + dataType: INSTANCE_CONFIGS_DATA_TYPES.TEXT, + value: '', + type: INSTANCE_SETTINGS_TYPE.SYSTEM, + createdAt: new Date(), + } + ]; + + for (const setting of newSettings) { + // Use upsert or check existence to prevent errors if you run this twice + await entityManager.insert(InstanceSettings, setting); + } + } + + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DELETE FROM instance_settings WHERE key IN ('PASSWORD_ALLOWED_DOMAINS', 'PASSWORD_RESTRICTED_DOMAINS')`); + } + +} diff --git a/server/migrations/1766224841342-AddPasswordDomainColumnsToOrganizations.ts b/server/migrations/1766224841342-AddPasswordDomainColumnsToOrganizations.ts new file mode 100644 index 0000000000..4dc02df986 --- /dev/null +++ b/server/migrations/1766224841342-AddPasswordDomainColumnsToOrganizations.ts @@ -0,0 +1,30 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +export class AddPasswordDomainColumnsToOrganizations1766224841342 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumns('organizations', [ + new TableColumn({ + name: 'password_allowed_domains', + type: 'varchar', + isNullable: true, + }), + new TableColumn({ + name: 'password_restricted_domains', + type: 'varchar', + isNullable: true, + }), + ]); + + // Prefill password_allowed_domains with existing domain values + await queryRunner.query( + `UPDATE organizations SET password_allowed_domains = domain WHERE domain IS NOT NULL AND domain != ''` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn('organizations', 'password_allowed_domains'); + await queryRunner.dropColumn('organizations', 'password_restricted_domains'); + } +} + + diff --git a/server/src/entities/organization.entity.ts b/server/src/entities/organization.entity.ts index 5ed48f4fbc..2630836039 100644 --- a/server/src/entities/organization.entity.ts +++ b/server/src/entities/organization.entity.ts @@ -37,6 +37,12 @@ export class Organization extends BaseEntity { @Column({ name: 'domain' }) domain: string; + @Column({ name: 'password_allowed_domains', nullable: true }) + passwordAllowedDomains: string; + + @Column({ name: 'password_restricted_domains', nullable: true }) + passwordRestrictedDomains: string; + @Column({ name: 'is_default', default: false }) isDefault: boolean; diff --git a/server/src/helpers/utils.helper.ts b/server/src/helpers/utils.helper.ts index ebcabb06d0..311e5543bb 100644 --- a/server/src/helpers/utils.helper.ts +++ b/server/src/helpers/utils.helper.ts @@ -9,6 +9,7 @@ import { getEnvVars } from 'scripts/database-config-utils'; import { decamelizeKeys } from 'humps'; import * as semver from 'semver'; import { BadRequestException } from '@nestjs/common'; +import { INSTANCE_SYSTEM_SETTINGS } from '@modules/instance-settings/constants'; const PASSWORD_REGEX = /^(?=.{12,24}$)[A-Za-z0-9!@#\$%\^&\*\(\)_+\-=\{\}\[\]:;\"',\.\?\/\\\|]+$/; @@ -465,6 +466,125 @@ export const isValidDomain = (email: string, restrictedDomain: string): boolean return true; }; +/** + * Validates email domain for SSO sign-in with organization/instance hierarchy. + * Organization domain settings override instance settings. + * This feature is only available in EE edition. For CE edition, always returns true. + * @param email - User email address + * @param orgDomain - Organization domain setting (backward compatible with old 'domain' field) + * @param instanceAllowedDomains - Instance level SSO allowed domains + * @returns true if domain is valid, false otherwise + */ +export const isValidSSODomain = ( + email: string, + orgDomain: string | null | undefined, + instanceAllowedDomains: string | null | undefined +): boolean => { + // Skip validation for CE edition, validate for EE edition + if (getTooljetEdition() === 'ce') { + return true; + } + + if (!email) { + return false; + } + + // Use organization domain if present (overrides instance), otherwise use instance setting + const allowedDomains = orgDomain || instanceAllowedDomains; + + // If no domain restriction, allow all + if (!allowedDomains || !allowedDomains.trim()) { + return true; + } + + const domain = email.substring(email.lastIndexOf('@') + 1); + if (!domain) { + return false; + } + + // Check if domain is in allowed list + const allowedDomainList = allowedDomains + .split(',') + .map((e) => e && e.trim()) + .filter((e) => !!e); + + return allowedDomainList.includes(domain); +}; + +/** + * Validates email domain for password sign-in with organization/instance hierarchy. + * Organization domain settings override instance settings. + * Supports both allowed domains (whitelist) and restricted domains (blacklist). + * This feature is only available in EE edition. For CE edition, always returns true. + * @param email - User email address + * @param orgAllowedDomains - Organization level password allowed domains + * @param orgRestrictedDomains - Organization level password restricted domains + * @param instanceAllowedDomains - Instance level password allowed domains + * @param instanceRestrictedDomains - Instance level password restricted domains + * @returns true if domain is valid, false otherwise + */ +export const isValidPasswordDomain = ( + email: string, + orgAllowedDomains: string | null | undefined, + orgRestrictedDomains: string | null | undefined, + instanceAllowedDomains: string | null | undefined, + instanceRestrictedDomains: string | null | undefined +): boolean => { + // Skip validation for CE edition, validate for EE edition + if (getTooljetEdition() === 'ce') { + return true; + } + + if (!email) { + return false; + } + + const domain = email.substring(email.lastIndexOf('@') + 1); + if (!domain) { + return false; + } + + // Use organization settings if present (overrides instance), otherwise use instance settings + const allowedDomains = orgAllowedDomains || instanceAllowedDomains; + + // Check restricted domains from both org and instance (blacklist takes precedence) + // If domain is restricted in either org or instance, deny access + if (orgRestrictedDomains && orgRestrictedDomains.trim()) { + const orgRestrictedDomainList = orgRestrictedDomains + .split(',') + .map((e) => e && e.trim()) + .filter((e) => !!e); + + if (orgRestrictedDomainList.includes(domain)) { + return false; + } + } + + if (instanceRestrictedDomains && instanceRestrictedDomains.trim()) { + const instanceRestrictedDomainList = instanceRestrictedDomains + .split(',') + .map((e) => e && e.trim()) + .filter((e) => !!e); + + if (instanceRestrictedDomainList.includes(domain)) { + return false; + } + } + + // Check allowed domains (whitelist) + if (allowedDomains && allowedDomains.trim()) { + const allowedDomainList = allowedDomains + .split(',') + .map((e) => e && e.trim()) + .filter((e) => !!e); + + return allowedDomainList.includes(domain); + } + + // If no restrictions configured, allow all + return true; +}; + export const isHttpsEnabled = () => { return !!process.env.TOOLJET_HOST?.startsWith('https'); }; @@ -567,3 +687,83 @@ export function throwIfSignalAborted(signal: AbortSignal, timeout: number) { throw new QueryError(`Defined query timeout of ${timeout}ms exceeded when running query.`, 'Query timed out', {}); } } + +/** + * Validates email domain for SSO sign-in with organization/instance hierarchy. + * Fetches instance settings internally. Organization domain settings override instance settings. + * This feature is only available in EE edition. For CE edition, always returns true. + * @param email - User email address + * @param orgDomain - Organization domain setting (backward compatible with old 'domain' field) + * @param instanceSettingsUtilService - Instance settings util service to fetch settings + * @param isInstanceSSO - Whether this is instance-level SSO (domain already contains instance value) + * @returns Promise - true if domain is valid, false otherwise + */ +export async function validateSSODomain( + email: string, + orgDomain: string | null | undefined, + instanceSettingsUtilService: any, + isInstanceSSO: boolean = false +): Promise { + // Skip validation for CE edition, validate for EE edition + if (getTooljetEdition() === 'ce') { + return true; + } + + if (!email) { + return false; + } + + // For instance SSO, orgDomain already contains instance value, so use it directly + if (isInstanceSSO) { + return isValidSSODomain(email, null, orgDomain); + } + + // Fetch instance settings + const instanceSettings = await instanceSettingsUtilService.getSettings([ + INSTANCE_SYSTEM_SETTINGS.ALLOWED_DOMAINS, + ]); + const instanceAllowedDomains = instanceSettings?.ALLOWED_DOMAINS; + + return isValidSSODomain(email, orgDomain, instanceAllowedDomains); +} + +/** + * Validates email domain for password sign-in with organization/instance hierarchy. + * Fetches instance settings internally. Organization domain settings override instance settings. + * Supports both allowed domains (whitelist) and restricted domains (blacklist). + * This feature is only available in EE edition. For CE edition, always returns true. + * @param email - User email address + * @param orgAllowedDomains - Organization level password allowed domains + * @param orgRestrictedDomains - Organization level password restricted domains + * @param instanceSettingsUtilService - Instance settings util service to fetch settings + * @returns Promise - true if domain is valid, false otherwise + */ +export async function validatePasswordDomain( + email: string, + orgAllowedDomains: string | null | undefined, + orgRestrictedDomains: string | null | undefined, + instanceSettingsUtilService: any +): Promise { + // Skip validation for CE edition, validate for EE edition + if (getTooljetEdition() === 'ce') { + return true; + } + + if (!email) { + return false; + } + + // Fetch instance settings + const instanceSettings = await instanceSettingsUtilService.getSettings([ + INSTANCE_SYSTEM_SETTINGS.PASSWORD_ALLOWED_DOMAINS, + INSTANCE_SYSTEM_SETTINGS.PASSWORD_RESTRICTED_DOMAINS, + ]); + + return isValidPasswordDomain( + email, + orgAllowedDomains, + orgRestrictedDomains, + instanceSettings?.PASSWORD_ALLOWED_DOMAINS, + instanceSettings?.PASSWORD_RESTRICTED_DOMAINS + ); +} diff --git a/server/src/modules/auth/service.ts b/server/src/modules/auth/service.ts index 16b0d8879c..a07493b97a 100644 --- a/server/src/modules/auth/service.ts +++ b/server/src/modules/auth/service.ts @@ -4,7 +4,11 @@ import { Organization } from 'src/entities/organization.entity'; import { SSOConfigs } from 'src/entities/sso_config.entity'; import { EntityManager } from 'typeorm'; import { WORKSPACE_USER_STATUS } from '@modules/users/constants/lifecycle'; -import { isSuperAdmin, generateNextNameAndSlug } from 'src/helpers/utils.helper'; +import { + isSuperAdmin, + generateNextNameAndSlug, + validatePasswordDomain, +} from 'src/helpers/utils.helper'; import { dbTransactionWrap } from 'src/helpers/database.helper'; import { InstanceSettingsUtilService } from '@modules/instance-settings/util.service'; import { Response } from 'express'; @@ -96,6 +100,20 @@ export class AuthService implements IAuthService { if (organization) user.organizationId = organization.id; /* CASE: No active workspace. But one workspace with invited status. waiting for activation */ if (isInviteRedirect && !organization) user.organizationId = invitingOrganizationId ?? ''; + + // Validate password domain for global login (org overrides instance) + if (organization && !isSuperAdmin(user)) { + if ( + !(await validatePasswordDomain( + email, + organization.passwordAllowedDomains, + organization.passwordRestrictedDomains, + this.instanceSettingsUtilService + )) + ) { + throw new UnauthorizedException(`This login method is not available for your domain. Please contact admin or try another method.`); + } + } } else { // organization specific login // No need to validate user status, validateUser() already covers it @@ -109,6 +127,20 @@ export class AuthService implements IAuthService { // no configurations in organization side or Form login disabled for the organization throw new UnauthorizedException('Password login is disabled for the organization'); } + + // Validate password domain with org/instance hierarchy (org overrides instance) + if (!isSuperAdmin(user)) { + if ( + !(await validatePasswordDomain( + email, + organization.passwordAllowedDomains, + organization.passwordRestrictedDomains, + this.instanceSettingsUtilService + )) + ) { + throw new UnauthorizedException(`This login method is not available for your domain. Please contact admin or try another method.`); + } + } } const shouldUpdateDefaultOrgId = diff --git a/server/src/modules/configs/service.ts b/server/src/modules/configs/service.ts index 88fb47676a..46bafcbde6 100644 --- a/server/src/modules/configs/service.ts +++ b/server/src/modules/configs/service.ts @@ -56,6 +56,8 @@ export class ConfigService implements IConfigService { INSTANCE_USER_SETTINGS.ALLOW_PERSONAL_WORKSPACE, INSTANCE_USER_SETTINGS.ENABLE_MULTIPLAYER_EDITING, INSTANCE_USER_SETTINGS.ENABLE_COMMENTS, + INSTANCE_SYSTEM_SETTINGS.PASSWORD_ALLOWED_DOMAINS, + INSTANCE_SYSTEM_SETTINGS.PASSWORD_RESTRICTED_DOMAINS, INSTANCE_SYSTEM_SETTINGS.ALLOWED_DOMAINS, INSTANCE_SYSTEM_SETTINGS.ENABLE_SIGNUP, INSTANCE_SYSTEM_SETTINGS.ENABLE_WORKSPACE_LOGIN_CONFIGURATION, diff --git a/server/src/modules/instance-settings/constants/index.ts b/server/src/modules/instance-settings/constants/index.ts index f144d0b1ef..1547bb6051 100644 --- a/server/src/modules/instance-settings/constants/index.ts +++ b/server/src/modules/instance-settings/constants/index.ts @@ -12,6 +12,8 @@ export enum INSTANCE_SETTINGS_TYPE { export enum INSTANCE_SYSTEM_SETTINGS { ALLOWED_DOMAINS = 'ALLOWED_DOMAINS', + PASSWORD_ALLOWED_DOMAINS = 'PASSWORD_ALLOWED_DOMAINS', + PASSWORD_RESTRICTED_DOMAINS = 'PASSWORD_RESTRICTED_DOMAINS', ENABLE_SIGNUP = 'ENABLE_SIGNUP', ENABLE_WORKSPACE_LOGIN_CONFIGURATION = 'ENABLE_WORKSPACE_LOGIN_CONFIGURATION', AUTOMATIC_SSO_LOGIN = 'AUTOMATIC_SSO_LOGIN', diff --git a/server/src/modules/login-configs/dto/index.ts b/server/src/modules/login-configs/dto/index.ts index d2a8838dc3..d4044d52e3 100644 --- a/server/src/modules/login-configs/dto/index.ts +++ b/server/src/modules/login-configs/dto/index.ts @@ -9,6 +9,18 @@ export class OrganizationConfigsUpdateDto { @MaxLength(250, { message: 'Domain cannot be longer than 250 characters' }) domain?: string; + @IsOptional() + @IsString() + @Transform(({ value }) => sanitizeInput(value)) + @MaxLength(250, { message: 'Password allowed domains cannot be longer than 250 characters' }) + passwordAllowedDomains?: string; + + @IsOptional() + @IsString() + @Transform(({ value }) => sanitizeInput(value)) + @MaxLength(250, { message: 'Password restricted domains cannot be longer than 250 characters' }) + passwordRestrictedDomains?: string; + @IsOptional() @IsBoolean() enableSignUp?: boolean; @@ -28,6 +40,18 @@ export class InstanceConfigsUpdateDto { @Transform(({ value }) => sanitizeInput(value)) @MaxLength(250, { message: 'Domains cannot be longer than 250 characters' }) allowedDomains?: string; + + @IsOptional() + @IsString() + @Transform(({ value }) => sanitizeInput(value)) + @MaxLength(250, { message: 'Allowed domains cannot be longer than 250 characters' }) + passwordAllowedDomains?: string; + + @IsOptional() + @IsString() + @Transform(({ value }) => sanitizeInput(value)) + @MaxLength(250, { message: 'Restricted domains cannot be longer than 250 characters' }) + passwordRestrictedDomains?: string; @IsOptional() @IsBoolean() diff --git a/server/src/modules/login-configs/service.ts b/server/src/modules/login-configs/service.ts index c9db2b32ad..2ac03ef162 100644 --- a/server/src/modules/login-configs/service.ts +++ b/server/src/modules/login-configs/service.ts @@ -145,10 +145,12 @@ export class LoginConfigsService implements ILoginConfigsService { async updateGeneralOrganizationConfigs(user: User, params: OrganizationConfigsUpdateDto) { const organizationId = user.organizationId; - const { domain, enableSignUp, inheritSSO, automaticSsoLogin } = params; + const { domain, passwordAllowedDomains, passwordRestrictedDomains, enableSignUp, inheritSSO, automaticSsoLogin } = params; const updatableParams = { domain, + passwordAllowedDomains, + passwordRestrictedDomains, enableSignUp, inheritSSO, automaticSsoLogin, diff --git a/server/src/modules/onboarding/service.ts b/server/src/modules/onboarding/service.ts index c64afcd52f..f0a608a61a 100644 --- a/server/src/modules/onboarding/service.ts +++ b/server/src/modules/onboarding/service.ts @@ -32,6 +32,7 @@ import { isValidDomain, generateWorkspaceSlug, validatePasswordServer, + validatePasswordDomain, } from 'src/helpers/utils.helper'; import { dbTransactionWrap } from 'src/helpers/database.helper'; import { Response } from 'express'; @@ -99,12 +100,14 @@ export class OnboardingService implements IOnboardingService { throw new NotFoundException('Could not found organization details. Please verify the orgnization id'); } /* Check if the workspace allows user signup or not */ - const { enableSignUp, domain } = signingUpOrganization; + const { enableSignUp, passwordAllowedDomains, passwordRestrictedDomains } = signingUpOrganization; if (!enableSignUp) { throw new ForbiddenException('Workspace signup has been disabled. Please contact the workspace admin.'); } - if (!isValidDomain(email, domain)) { - throw new ForbiddenException('You cannot sign up using the email address - Domain verification failed.'); + if ( + !(await validatePasswordDomain(email, passwordAllowedDomains, passwordRestrictedDomains, this.instanceSettingsUtilService)) + ) { + throw new ForbiddenException('This login method is not available for your domain. Please contact admin or try another method.'); } } From aa1dc448f9938a2b26859c148c937b5dec7f2c97 Mon Sep 17 00:00:00 2001 From: Siddharthpl Date: Tue, 23 Dec 2025 16:13:50 +0530 Subject: [PATCH 02/10] Updated the migration --- ...1765958549099-AddGranularDomainSettings.ts | 65 +++++++++---------- ...AddPasswordDomainColumnsToOrganizations.ts | 5 -- 2 files changed, 30 insertions(+), 40 deletions(-) diff --git a/server/data-migrations/1765958549099-AddGranularDomainSettings.ts b/server/data-migrations/1765958549099-AddGranularDomainSettings.ts index 91b39c2112..a912c7c90e 100644 --- a/server/data-migrations/1765958549099-AddGranularDomainSettings.ts +++ b/server/data-migrations/1765958549099-AddGranularDomainSettings.ts @@ -1,43 +1,38 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; -import { InstanceSettings } from '@entities/instance_settings.entity'; -import { INSTANCE_CONFIGS_DATA_TYPES } from '@modules/instance-settings/constants'; -import { INSTANCE_SETTINGS_TYPE, INSTANCE_SYSTEM_SETTINGS } from '@modules/instance-settings/constants'; +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { + INSTANCE_CONFIGS_DATA_TYPES, + INSTANCE_SETTINGS_TYPE, +} from '@modules/instance-settings/constants'; + export class AddGranularDomainSettings1765958549099 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - const entityManager = queryRunner.manager; + const passwordAllowedKey = 'PASSWORD_ALLOWED_DOMAINS'; + const passwordRestrictedKey = 'PASSWORD_RESTRICTED_DOMAINS'; - // Query existing ALLOWED_DOMAINS to prefill PASSWORD_ALLOWED_DOMAINS - const existingAllowedDomains = await entityManager.findOne(InstanceSettings, { - where: { key: INSTANCE_SYSTEM_SETTINGS.ALLOWED_DOMAINS } - }); - const existingAllowedDomainsValue = existingAllowedDomains?.value || ''; - - const newSettings = [ - { - key: 'PASSWORD_ALLOWED_DOMAINS', - label: 'Password Allowed Domains', - dataType: INSTANCE_CONFIGS_DATA_TYPES.TEXT, - value: existingAllowedDomainsValue, - type: INSTANCE_SETTINGS_TYPE.SYSTEM, - createdAt: new Date(), - }, - { - key: 'PASSWORD_RESTRICTED_DOMAINS', - label: 'Password Restricted Domains', - dataType: INSTANCE_CONFIGS_DATA_TYPES.TEXT, - value: '', - type: INSTANCE_SETTINGS_TYPE.SYSTEM, - createdAt: new Date(), - } - ]; - - for (const setting of newSettings) { - // Use upsert or check existence to prevent errors if you run this twice - await entityManager.insert(InstanceSettings, setting); - } - } + await queryRunner.query( + ` + INSERT INTO instance_settings (key, label, data_type, value, type) + VALUES + ($1, $2, $3, $4, $5), + ($6, $7, $8, $9, $10) + ON CONFLICT (key) DO NOTHING + `, + [ + passwordAllowedKey, + 'Password Allowed Domains', + INSTANCE_CONFIGS_DATA_TYPES.TEXT, + '', + INSTANCE_SETTINGS_TYPE.SYSTEM, + passwordRestrictedKey, + 'Password Restricted Domains', + INSTANCE_CONFIGS_DATA_TYPES.TEXT, + '', + INSTANCE_SETTINGS_TYPE.SYSTEM, + ], + ); + } public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(`DELETE FROM instance_settings WHERE key IN ('PASSWORD_ALLOWED_DOMAINS', 'PASSWORD_RESTRICTED_DOMAINS')`); diff --git a/server/migrations/1766224841342-AddPasswordDomainColumnsToOrganizations.ts b/server/migrations/1766224841342-AddPasswordDomainColumnsToOrganizations.ts index 4dc02df986..3a7c090b72 100644 --- a/server/migrations/1766224841342-AddPasswordDomainColumnsToOrganizations.ts +++ b/server/migrations/1766224841342-AddPasswordDomainColumnsToOrganizations.ts @@ -14,11 +14,6 @@ export class AddPasswordDomainColumnsToOrganizations1766224841342 implements Mig isNullable: true, }), ]); - - // Prefill password_allowed_domains with existing domain values - await queryRunner.query( - `UPDATE organizations SET password_allowed_domains = domain WHERE domain IS NOT NULL AND domain != ''` - ); } public async down(queryRunner: QueryRunner): Promise { From 20eedfb4a4019ac41deed6ab1ac808537a67281d Mon Sep 17 00:00:00 2001 From: Siddharthpl Date: Sun, 28 Dec 2025 16:00:10 +0530 Subject: [PATCH 03/10] Removed email verification for ee and ce --- .../pages/SignupPage/SignupPage.jsx | 11 +- server/src/modules/onboarding/util.service.ts | 141 ++++++++++++++---- 2 files changed, 118 insertions(+), 34 deletions(-) diff --git a/frontend/src/modules/onboarding/pages/SignupPage/SignupPage.jsx b/frontend/src/modules/onboarding/pages/SignupPage/SignupPage.jsx index c0d7b33fcc..09bb4130cc 100644 --- a/frontend/src/modules/onboarding/pages/SignupPage/SignupPage.jsx +++ b/frontend/src/modules/onboarding/pages/SignupPage/SignupPage.jsx @@ -72,7 +72,16 @@ const SignupPage = ({ configs, organizationId }) => { const { organizationInviteUrl } = response; if (organizationInviteUrl) onInvitedUserSignUpSuccess(response, navigate); setSigningUserInfo({ email, name }); - setSignupSuccess(true); + + // Show email verification flow only for cloud edition. + if (edition === 'cloud') { + setSignupSuccess(true); + } else { + // For non-cloud editions, skip email verification screen and redirect to login. + const loginRedirect = redirectTo ? `?redirectTo=${redirectTo}` : ''; + navigate(`/login${loginRedirect}`, { replace: true }); + } + onSuccess(); }) .catch((e) => { diff --git a/server/src/modules/onboarding/util.service.ts b/server/src/modules/onboarding/util.service.ts index ff8cc8e54a..d89b74893b 100644 --- a/server/src/modules/onboarding/util.service.ts +++ b/server/src/modules/onboarding/util.service.ts @@ -9,7 +9,7 @@ import { LicenseCountsService } from '../licensing/services/count.service'; import { LICENSE_TRIAL_API, ORGANIZATION_INSTANCE_KEY } from '../licensing/constants'; import got from 'got/dist/source'; import { HttpException } from '@nestjs/common'; -import { fullName, generateNextNameAndSlug, generateOrgInviteURL } from 'src/helpers/utils.helper'; +import { fullName, generateNextNameAndSlug, generateOrgInviteURL, getTooljetEdition } from 'src/helpers/utils.helper'; import { NotAcceptableException } from '@nestjs/common'; import { Organization } from '../../entities/organization.entity'; import { EntityManager } from 'typeorm'; @@ -17,6 +17,7 @@ import { getUserStatusAndSource, lifecycleEvents, SOURCE, + USER_STATUS, WORKSPACE_USER_STATUS, WORKSPACE_USER_SOURCE, } from '@modules/users/constants/lifecycle'; @@ -442,6 +443,23 @@ export class OnboardingUtilService implements IOnboardingUtilService { lastName: user.lastName, role: user.role, }); + + // Auto-activate users for non-cloud editions (skip email verification) + const edition = getTooljetEdition(); + const isCloudEdition = edition === 'cloud'; + if (!isCloudEdition && user.status === USER_STATUS.INVITED) { + await this.userRepository.updateOne( + user.id, + { + status: USER_STATUS.ACTIVE, + invitationToken: null, + }, + manager + ); + user.status = USER_STATUS.ACTIVE; + user.invitationToken = null; + } + if (signingUpOrganization) { /* Attach the user and user groups to the organization */ const organizationUser = await this.organizationUserRepository.createOne( @@ -451,20 +469,30 @@ export class OnboardingUtilService implements IOnboardingUtilService { manager, WORKSPACE_USER_SOURCE.SIGNUP ); + + // Auto-activate organization user for non-cloud editions + if (!isCloudEdition && organizationUser.status === WORKSPACE_USER_STATUS.INVITED) { + await this.organizationUsersUtilService.activateOrganization(organizationUser, manager); + } + await this.licenseUserService.validateUser(manager, organizationId); - this.eventEmitter.emit('emailEvent', { - type: EMAIL_EVENTS.SEND_WELCOME_EMAIL, - payload: { - to: user.email, - name: user.firstName, - invitationtoken: user.invitationToken, - organizationInvitationToken: organizationUser.invitationToken, - organizationId: signingUpOrganization.id, - organizationName: signingUpOrganization.name, - sender: null, - redirectTo: redirectTo, - }, - }); + + // Only send verification email for cloud edition + if (isCloudEdition) { + this.eventEmitter.emit('emailEvent', { + type: EMAIL_EVENTS.SEND_WELCOME_EMAIL, + payload: { + to: user.email, + name: user.firstName, + invitationtoken: user.invitationToken, + organizationInvitationToken: organizationUser.invitationToken, + organizationId: signingUpOrganization.id, + organizationName: signingUpOrganization.name, + sender: null, + redirectTo: redirectTo, + }, + }); + } // this.eventEmitter.emit( // 'auditLogEntry', // { @@ -479,15 +507,39 @@ export class OnboardingUtilService implements IOnboardingUtilService { // ); return {}; } else { - await this.licenseUserService.validateUser(manager, organizationId); - this.eventEmitter.emit('emailEvent', { - type: EMAIL_EVENTS.SEND_WELCOME_EMAIL, - payload: { - to: user.email, - name: user.firstName, - invitationtoken: user.invitationToken, - }, - }); + // Auto-activate users for non-cloud editions (skip email verification) + const edition = getTooljetEdition(); + const isCloudEdition = edition === 'cloud'; + if (!isCloudEdition && user.status === USER_STATUS.INVITED) { + await this.userRepository.updateOne( + user.id, + { + status: USER_STATUS.ACTIVE, + invitationToken: null, + }, + manager + ); + user.status = USER_STATUS.ACTIVE; + user.invitationToken = null; + } + + // Use user's default organization ID if signingUpOrganization is null + const orgIdForValidation = user.defaultOrganizationId || organizationId; + if (orgIdForValidation) { + await this.licenseUserService.validateUser(manager, orgIdForValidation); + } + + // Only send verification email for cloud edition + if (isCloudEdition) { + this.eventEmitter.emit('emailEvent', { + type: EMAIL_EVENTS.SEND_WELCOME_EMAIL, + payload: { + to: user.email, + name: user.firstName, + invitationtoken: user.invitationToken, + }, + }); + } // this.eventEmitter.emit( // 'auditLogEntry', @@ -640,7 +692,7 @@ export class OnboardingUtilService implements IOnboardingUtilService { ); // Create organization user entry - await this.organizationUserRepository.createOne( + const organizationUser = await this.organizationUserRepository.createOne( user, defaultWorkspace, true, @@ -648,18 +700,41 @@ export class OnboardingUtilService implements IOnboardingUtilService { WORKSPACE_USER_SOURCE.SIGNUP ); + // Auto-activate users for non-cloud editions (skip email verification) + const edition = getTooljetEdition(); + const isCloudEdition = edition === 'cloud'; + if (!isCloudEdition && user.status === USER_STATUS.INVITED) { + await this.userRepository.updateOne( + user.id, + { + status: USER_STATUS.ACTIVE, + invitationToken: null, + }, + manager + ); + user.status = USER_STATUS.ACTIVE; + user.invitationToken = null; + + // Also activate the organization user for non-cloud editions + if (organizationUser.status === WORKSPACE_USER_STATUS.INVITED) { + await this.organizationUsersUtilService.activateOrganization(organizationUser, manager); + } + } + // Validate license await this.licenseUserService.validateUser(manager, user?.defaultOrganizationId); - // Send welcome email - this.eventEmitter.emit('emailEvent', { - type: EMAIL_EVENTS.SEND_WELCOME_EMAIL, - payload: { - to: user.email, - name: user.firstName, - invitationtoken: user.invitationToken, - }, - }); + // Only send verification email for cloud edition + if (isCloudEdition) { + this.eventEmitter.emit('emailEvent', { + type: EMAIL_EVENTS.SEND_WELCOME_EMAIL, + payload: { + to: user.email, + name: user.firstName, + invitationtoken: user.invitationToken, + }, + }); + } return {}; }, manager); From c006260e6a628a13eae3f0a0d6b5255289b27d5c Mon Sep 17 00:00:00 2001 From: Siddharthpl Date: Mon, 29 Dec 2025 12:54:05 +0530 Subject: [PATCH 04/10] Updated the password login for instance level --- server/src/modules/onboarding/service.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/src/modules/onboarding/service.ts b/server/src/modules/onboarding/service.ts index f0a608a61a..f62a7658c7 100644 --- a/server/src/modules/onboarding/service.ts +++ b/server/src/modules/onboarding/service.ts @@ -109,6 +109,13 @@ export class OnboardingService implements IOnboardingService { ) { throw new ForbiddenException('This login method is not available for your domain. Please contact admin or try another method.'); } + } else { + // No organization provided - validate against instance-level settings + if ( + !(await validatePasswordDomain(email, undefined, undefined, this.instanceSettingsUtilService)) + ) { + throw new ForbiddenException('This login method is not available for your domain. Please contact admin or try another method.'); + } } const names = { firstName: '', lastName: '' }; From b3890e01288c72b650e47b6b317fa0661129a980 Mon Sep 17 00:00:00 2001 From: Siddharthpl Date: Mon, 29 Dec 2025 14:30:30 +0530 Subject: [PATCH 05/10] Updated the migration to prefilled both password login and soo allowed domain --- ...1765958549099-AddGranularDomainSettings.ts | 58 +++++++++++-------- ...AddPasswordDomainColumnsToOrganizations.ts | 6 +- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/server/data-migrations/1765958549099-AddGranularDomainSettings.ts b/server/data-migrations/1765958549099-AddGranularDomainSettings.ts index a912c7c90e..1fb50fa5e9 100644 --- a/server/data-migrations/1765958549099-AddGranularDomainSettings.ts +++ b/server/data-migrations/1765958549099-AddGranularDomainSettings.ts @@ -3,39 +3,47 @@ import { INSTANCE_CONFIGS_DATA_TYPES, INSTANCE_SETTINGS_TYPE, } from '@modules/instance-settings/constants'; +import { INSTANCE_SYSTEM_SETTINGS } from '@modules/instance-settings/constants'; export class AddGranularDomainSettings1765958549099 implements MigrationInterface { - - public async up(queryRunner: QueryRunner): Promise { - const passwordAllowedKey = 'PASSWORD_ALLOWED_DOMAINS'; - const passwordRestrictedKey = 'PASSWORD_RESTRICTED_DOMAINS'; - + public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( ` INSERT INTO instance_settings (key, label, data_type, value, type) VALUES - ($1, $2, $3, $4, $5), - ($6, $7, $8, $9, $10) - ON CONFLICT (key) DO NOTHING + ( + 'PASSWORD_ALLOWED_DOMAINS', + 'Password Allowed Domains', + '${INSTANCE_CONFIGS_DATA_TYPES.TEXT}', + COALESCE( + ( + SELECT value + FROM instance_settings + WHERE key = '${INSTANCE_SYSTEM_SETTINGS.ALLOWED_DOMAINS}' + LIMIT 1 + ), + '' + ), + '${INSTANCE_SETTINGS_TYPE.SYSTEM}' + ), + ( + 'PASSWORD_RESTRICTED_DOMAINS', + 'Password Restricted Domains', + '${INSTANCE_CONFIGS_DATA_TYPES.TEXT}', + '', + '${INSTANCE_SETTINGS_TYPE.SYSTEM}' + ) + ON CONFLICT (key) DO NOTHING; `, - [ - passwordAllowedKey, - 'Password Allowed Domains', - INSTANCE_CONFIGS_DATA_TYPES.TEXT, - '', - INSTANCE_SETTINGS_TYPE.SYSTEM, - - passwordRestrictedKey, - 'Password Restricted Domains', - INSTANCE_CONFIGS_DATA_TYPES.TEXT, - '', - INSTANCE_SETTINGS_TYPE.SYSTEM, - ], ); } - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DELETE FROM instance_settings WHERE key IN ('PASSWORD_ALLOWED_DOMAINS', 'PASSWORD_RESTRICTED_DOMAINS')`); - } - + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + ` + DELETE FROM instance_settings + WHERE key IN ('PASSWORD_ALLOWED_DOMAINS', 'PASSWORD_RESTRICTED_DOMAINS'); + `, + ); + } } diff --git a/server/migrations/1766224841342-AddPasswordDomainColumnsToOrganizations.ts b/server/migrations/1766224841342-AddPasswordDomainColumnsToOrganizations.ts index 3a7c090b72..3a9fb3ef4d 100644 --- a/server/migrations/1766224841342-AddPasswordDomainColumnsToOrganizations.ts +++ b/server/migrations/1766224841342-AddPasswordDomainColumnsToOrganizations.ts @@ -14,6 +14,11 @@ export class AddPasswordDomainColumnsToOrganizations1766224841342 implements Mig isNullable: true, }), ]); + + // Prefill password_allowed_domains with existing domain values + await queryRunner.query( + `UPDATE organizations SET password_allowed_domains = domain WHERE domain IS NOT NULL AND domain != ''` + ); } public async down(queryRunner: QueryRunner): Promise { @@ -22,4 +27,3 @@ export class AddPasswordDomainColumnsToOrganizations1766224841342 implements Mig } } - From d5f1037275cde23ac1dae82f9b2255f1e91140fb Mon Sep 17 00:00:00 2001 From: Siddharthpl Date: Tue, 30 Dec 2025 10:12:11 +0530 Subject: [PATCH 06/10] Revert "Removed email verification for ee and ce" This reverts commit 20eedfb4a4019ac41deed6ab1ac808537a67281d. --- .../pages/SignupPage/SignupPage.jsx | 11 +- server/src/modules/onboarding/util.service.ts | 141 ++++-------------- 2 files changed, 34 insertions(+), 118 deletions(-) diff --git a/frontend/src/modules/onboarding/pages/SignupPage/SignupPage.jsx b/frontend/src/modules/onboarding/pages/SignupPage/SignupPage.jsx index 09bb4130cc..c0d7b33fcc 100644 --- a/frontend/src/modules/onboarding/pages/SignupPage/SignupPage.jsx +++ b/frontend/src/modules/onboarding/pages/SignupPage/SignupPage.jsx @@ -72,16 +72,7 @@ const SignupPage = ({ configs, organizationId }) => { const { organizationInviteUrl } = response; if (organizationInviteUrl) onInvitedUserSignUpSuccess(response, navigate); setSigningUserInfo({ email, name }); - - // Show email verification flow only for cloud edition. - if (edition === 'cloud') { - setSignupSuccess(true); - } else { - // For non-cloud editions, skip email verification screen and redirect to login. - const loginRedirect = redirectTo ? `?redirectTo=${redirectTo}` : ''; - navigate(`/login${loginRedirect}`, { replace: true }); - } - + setSignupSuccess(true); onSuccess(); }) .catch((e) => { diff --git a/server/src/modules/onboarding/util.service.ts b/server/src/modules/onboarding/util.service.ts index d89b74893b..ff8cc8e54a 100644 --- a/server/src/modules/onboarding/util.service.ts +++ b/server/src/modules/onboarding/util.service.ts @@ -9,7 +9,7 @@ import { LicenseCountsService } from '../licensing/services/count.service'; import { LICENSE_TRIAL_API, ORGANIZATION_INSTANCE_KEY } from '../licensing/constants'; import got from 'got/dist/source'; import { HttpException } from '@nestjs/common'; -import { fullName, generateNextNameAndSlug, generateOrgInviteURL, getTooljetEdition } from 'src/helpers/utils.helper'; +import { fullName, generateNextNameAndSlug, generateOrgInviteURL } from 'src/helpers/utils.helper'; import { NotAcceptableException } from '@nestjs/common'; import { Organization } from '../../entities/organization.entity'; import { EntityManager } from 'typeorm'; @@ -17,7 +17,6 @@ import { getUserStatusAndSource, lifecycleEvents, SOURCE, - USER_STATUS, WORKSPACE_USER_STATUS, WORKSPACE_USER_SOURCE, } from '@modules/users/constants/lifecycle'; @@ -443,23 +442,6 @@ export class OnboardingUtilService implements IOnboardingUtilService { lastName: user.lastName, role: user.role, }); - - // Auto-activate users for non-cloud editions (skip email verification) - const edition = getTooljetEdition(); - const isCloudEdition = edition === 'cloud'; - if (!isCloudEdition && user.status === USER_STATUS.INVITED) { - await this.userRepository.updateOne( - user.id, - { - status: USER_STATUS.ACTIVE, - invitationToken: null, - }, - manager - ); - user.status = USER_STATUS.ACTIVE; - user.invitationToken = null; - } - if (signingUpOrganization) { /* Attach the user and user groups to the organization */ const organizationUser = await this.organizationUserRepository.createOne( @@ -469,30 +451,20 @@ export class OnboardingUtilService implements IOnboardingUtilService { manager, WORKSPACE_USER_SOURCE.SIGNUP ); - - // Auto-activate organization user for non-cloud editions - if (!isCloudEdition && organizationUser.status === WORKSPACE_USER_STATUS.INVITED) { - await this.organizationUsersUtilService.activateOrganization(organizationUser, manager); - } - await this.licenseUserService.validateUser(manager, organizationId); - - // Only send verification email for cloud edition - if (isCloudEdition) { - this.eventEmitter.emit('emailEvent', { - type: EMAIL_EVENTS.SEND_WELCOME_EMAIL, - payload: { - to: user.email, - name: user.firstName, - invitationtoken: user.invitationToken, - organizationInvitationToken: organizationUser.invitationToken, - organizationId: signingUpOrganization.id, - organizationName: signingUpOrganization.name, - sender: null, - redirectTo: redirectTo, - }, - }); - } + this.eventEmitter.emit('emailEvent', { + type: EMAIL_EVENTS.SEND_WELCOME_EMAIL, + payload: { + to: user.email, + name: user.firstName, + invitationtoken: user.invitationToken, + organizationInvitationToken: organizationUser.invitationToken, + organizationId: signingUpOrganization.id, + organizationName: signingUpOrganization.name, + sender: null, + redirectTo: redirectTo, + }, + }); // this.eventEmitter.emit( // 'auditLogEntry', // { @@ -507,39 +479,15 @@ export class OnboardingUtilService implements IOnboardingUtilService { // ); return {}; } else { - // Auto-activate users for non-cloud editions (skip email verification) - const edition = getTooljetEdition(); - const isCloudEdition = edition === 'cloud'; - if (!isCloudEdition && user.status === USER_STATUS.INVITED) { - await this.userRepository.updateOne( - user.id, - { - status: USER_STATUS.ACTIVE, - invitationToken: null, - }, - manager - ); - user.status = USER_STATUS.ACTIVE; - user.invitationToken = null; - } - - // Use user's default organization ID if signingUpOrganization is null - const orgIdForValidation = user.defaultOrganizationId || organizationId; - if (orgIdForValidation) { - await this.licenseUserService.validateUser(manager, orgIdForValidation); - } - - // Only send verification email for cloud edition - if (isCloudEdition) { - this.eventEmitter.emit('emailEvent', { - type: EMAIL_EVENTS.SEND_WELCOME_EMAIL, - payload: { - to: user.email, - name: user.firstName, - invitationtoken: user.invitationToken, - }, - }); - } + await this.licenseUserService.validateUser(manager, organizationId); + this.eventEmitter.emit('emailEvent', { + type: EMAIL_EVENTS.SEND_WELCOME_EMAIL, + payload: { + to: user.email, + name: user.firstName, + invitationtoken: user.invitationToken, + }, + }); // this.eventEmitter.emit( // 'auditLogEntry', @@ -692,7 +640,7 @@ export class OnboardingUtilService implements IOnboardingUtilService { ); // Create organization user entry - const organizationUser = await this.organizationUserRepository.createOne( + await this.organizationUserRepository.createOne( user, defaultWorkspace, true, @@ -700,41 +648,18 @@ export class OnboardingUtilService implements IOnboardingUtilService { WORKSPACE_USER_SOURCE.SIGNUP ); - // Auto-activate users for non-cloud editions (skip email verification) - const edition = getTooljetEdition(); - const isCloudEdition = edition === 'cloud'; - if (!isCloudEdition && user.status === USER_STATUS.INVITED) { - await this.userRepository.updateOne( - user.id, - { - status: USER_STATUS.ACTIVE, - invitationToken: null, - }, - manager - ); - user.status = USER_STATUS.ACTIVE; - user.invitationToken = null; - - // Also activate the organization user for non-cloud editions - if (organizationUser.status === WORKSPACE_USER_STATUS.INVITED) { - await this.organizationUsersUtilService.activateOrganization(organizationUser, manager); - } - } - // Validate license await this.licenseUserService.validateUser(manager, user?.defaultOrganizationId); - // Only send verification email for cloud edition - if (isCloudEdition) { - this.eventEmitter.emit('emailEvent', { - type: EMAIL_EVENTS.SEND_WELCOME_EMAIL, - payload: { - to: user.email, - name: user.firstName, - invitationtoken: user.invitationToken, - }, - }); - } + // 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); From 6a24604b7a30a8c7af0331a3870f2a7977d6927f Mon Sep 17 00:00:00 2001 From: Yukti Goyal Date: Wed, 31 Dec 2025 00:29:46 +0530 Subject: [PATCH 07/10] updated allowed domain changes --- cypress-tests/cypress/commands/commands.js | 4 ++ .../cypress/constants/selectors/manageSSO.js | 33 ++++++++----- .../cypress/constants/texts/manageSSO.js | 27 ++++++---- .../cypress/support/utils/manageSSO.js | 49 ++++++++++++++----- 4 files changed, 79 insertions(+), 34 deletions(-) diff --git a/cypress-tests/cypress/commands/commands.js b/cypress-tests/cypress/commands/commands.js index fab8dc7649..d6a57f9021 100644 --- a/cypress-tests/cypress/commands/commands.js +++ b/cypress-tests/cypress/commands/commands.js @@ -266,6 +266,10 @@ Cypress.Commands.add( .and(assertion, value, ...arg); } ); +Cypress.Commands.add("scrollToElement", (selector) => { + cy.get(selector).scrollIntoView() + .should("be.visible"); +}); Cypress.Commands.add("openInCurrentTab", (selector) => { cy.get(selector).first().invoke("removeAttr", "target").click({ force: true }); diff --git a/cypress-tests/cypress/constants/selectors/manageSSO.js b/cypress-tests/cypress/constants/selectors/manageSSO.js index 45a8337f16..fd2cd671bc 100644 --- a/cypress-tests/cypress/constants/selectors/manageSSO.js +++ b/cypress-tests/cypress/constants/selectors/manageSSO.js @@ -2,16 +2,14 @@ export const ssoSelector = { pagetitle: "[data-cy=manage-sso-page-title]", instanceLoginPage: { cardTitle: "[data-cy=card-title]", - allowedDomainLabel: "[data-cy=allowed-domain-label]", - allowedDomainInput: "[data-cy=allowed-domains]", - allowedDomainHelperText: '[data-cy="allowed-allowedDomains-helper-text"]', + configurationLabel: "[data-cy=configuration-label]", + domainConstraintsLabel: "[data-cy=domain-constraints-label]", + domainConstraintsHelperText: '[data-cy="domain-constraints-helper-text"]', superAdminUrlLabel: '[data-cy="workspace-login-url-label"]', superAdminUrl: '[data-cy="workspace-login-url"]', superAdminUrlHelperText: '[data-cy="workspace-login-help-text"]', enableSignupLabel: '[data-cy="enable-sign-up-label"]', enableSignupHelperText: "[data-cy=enable-sign-up-helper-text]", - passwordLoginLabel: '[data-cy="label-password-login"]', - passwordDisableHelperText: '[data-cy="disable-password-helper-text"]', workspaceConfigurationLabel: '[data-cy="enable-workspace-configuration-label"]', workspaceConfigurationHelperText: '[data-cy="enable-workspace-configuration-helper-text"]', autoSSOLabel: '[data-cy="auto-sso-label"]', @@ -20,6 +18,9 @@ export const ssoSelector = { customLogoutUrlLabel: '[data-cy="custom-logout-url-label"]', customLogoutUrlPlaceholder: '[data-cy="custom-logout-url-input"]', customLogoutUrlHelperText: '[data-cy="custom-logout-url-helper-text"]', + loginMethodsLabel: '[data-cy="login-methods-label"]', + passwordLoginLabel: '[data-cy="label-password-login"]', + passwordDisableHelperText: '[data-cy="disable-password-helper-text"]', ssoHeader: '[data-cy="sso-header"]', googleLabel: '[data-cy="google-label"]', githubLabel: '[data-cy="github-label"]', @@ -27,18 +28,19 @@ export const ssoSelector = { }, workspaceLoginPage: { cardTitle: "[data-cy=card-title]", - allowedDomainLabel: "[data-cy=allowed-domain-label]", - allowedDomainInput: "[data-cy=allowed-domains]", - allowedDomainHelperText: '[data-cy="allowed-domain-helper-text"]', + configurationLabel: "[data-cy=configuration-label]", + domainConstraintsLabel: "[data-cy=domain-constraints-label]", + domainConstraintsHelperText: '[data-cy="domain-constraints-helper-text"]', workspaceLoginUrlLabel: '[data-cy="workspace-login-url-label"]', workspaceLoginUrl: '[data-cy="workspace-login-url"]', workspaceLoginUrlHelperText: '[data-cy="workspace-login-help-text"]', enableSignupLabel: '[data-cy="enable-sign-up-label"]', enableSignupHelperText: "[data-cy=enable-sign-up-helper-text]", - passwordLoginLabel: '[data-cy="label-password-login"]', - passwordDisableHelperText: '[data-cy="disable-password-helper-text"]', autoSSOLabel: '[data-cy="auto-sso-label"]', autoSSOHelperText: '[data-cy="auto-sso-helper-text"]', + loginMethodsLabel: '[data-cy="login-methods-label"]', + passwordLoginLabel: '[data-cy="label-password-login"]', + passwordDisableHelperText: '[data-cy="disable-password-helper-text"]', ssoHeader: '[data-cy="sso-header"]', instanceSsoCard: '[data-cy="instance-sso-card"]', instanceSsoHelperText: '[data-cy="instance-sso-helper-text"]', @@ -108,7 +110,16 @@ export const ssoSelector = { allowDefaultSSOToggle: '[style="padding-left: 0px; margin-bottom: 1px;"] > .switch > .slider', defaultSSOImage: '[data-cy="default-sso-status-image"]', - allowedDomainInput: '[data-cy="allowed-domains"]', + passwordLoginDropdown: '[data-cy="password-login-dropdown"]', + passwordLoginDropdownLabel: '[data-cy="password-login-dropdown-label"]', + passwordAllowedDomainsLabel: '[data-cy="password-allowed-domains-label"]', + passwordAllowedDomainsInput: '[data-cy="password-allowed-domains-input"]', + passwordRestrictedDomainsLabel: '[data-cy="password-restricted-domains-label"]', + passwordRestrictedDomainsInput: '[data-cy="password-restricted-domains-input"]', + ssoLoginDropdown: '[data-cy="sso-login-dropdown"]', + ssoLoginDropdownLabel: '[data-cy="sso-login-dropdown-label"]', + ssoAllowedDomainsLabel: '[data-cy="sso-allowed-domains-label"]', + ssoAllowedDomainsInput: '[data-cy="sso-allowed-domains-input"]', workspaceLoginUrl: '[data-cy="workspace-login-url"]', cancelButton: "[data-cy=cancel-button]", saveButton: "[data-cy=save-button]", diff --git a/cypress-tests/cypress/constants/texts/manageSSO.js b/cypress-tests/cypress/constants/texts/manageSSO.js index 27c5b2fcfe..f6282eaddc 100644 --- a/cypress-tests/cypress/constants/texts/manageSSO.js +++ b/cypress-tests/cypress/constants/texts/manageSSO.js @@ -3,16 +3,14 @@ export const ssoText = { instanceLoginPage: { cardTitle: "Instance login", - allowedDomainLabel: "Allowed domains", - allowedDomainInput: "", - allowedDomainHelperText: "Support multiple domains. Enter allowed domains names separated by comma. example: tooljet.com,tooljet.io,yourorganization.com", + configurationLabel: "CONFIGURATION", + domainConstraintsLabel: "Domain constraints", + domainConstraintsHelperText: "This is applicable only for users that sign up without invite. Enter multiple domains by comma separated values. Example: tooljet.com, tooljet.io", superAdminUrlLabel: "Super admin login URL", superAdminUrl: `${Cypress.config("baseUrl")}/login/super-admin`, superAdminUrlHelperText: "Use this URL for super admin to login via password", enableSignupLabel: "Enable Signup", enableSignupHelperText: "Users will be able to sign up without being invited", - passwordLoginLabel: "Password login", - passwordDisableHelperText: 'Disable password login only if your SSO is configured otherwise you will get locked out', workspaceConfigurationLabel: "Enable workspace configuration", workspaceConfigurationHelperText: "Allow workspace admin to configure their workspace’s login differently", autoSSOLabel: "Automatic SSO login", @@ -21,6 +19,9 @@ export const ssoText = { customLogoutUrlLabel: "Custom logout URL", customLogoutUrlPlaceholder: "", customLogoutUrlHelperText: "Set a personalized logout URL for users logging out of this instance.", + loginMethodsLabel: "LOGIN METHODS", + passwordLoginLabel: "Password login", + passwordDisableHelperText: 'Disable password login only if your SSO is configured otherwise you will get locked out', ssoHeader: "SSO", googleLabel: "Google", githubLabel: "GitHub", @@ -28,18 +29,19 @@ export const ssoText = { }, workspaceLoginPage: { cardTitle: "Workspace login", - allowedDomainLabel: "Allowed domains", - allowedDomainInput: "", - allowedDomainHelperText: "Support multiple domains. Enter domain names separated by comma. example: tooljet.com,tooljet.io,yourorganization.com", + configurationLabel: "CONFIGURATION", + domainConstraintsLabel: "Domain constraints", + domainConstraintsHelperText: "This is applicable only for users that sign up without invite. Enter multiple domains by comma separated values. Example: tooljet.com, tooljet.io", workspaceLoginUrlLabel: "Login URL", workspaceLoginUrl: `${Cypress.config("baseUrl")}/login/my-workspace`, workspaceLoginUrlHelperText: "Use this URL to login directly to this workspace", enableSignupLabel: "Enable signup", enableSignupHelperText: "Users will be able to sign up without being invited", - passwordLoginLabel: "Password login", - passwordDisableHelperText: 'Disable password login only if your SSO is configured otherwise you will get locked out', autoSSOLabel: "Automatic SSO login", autoSSOHelperText: "This will simulate the configured SSO login, bypassing the login screen in ToolJet", + loginMethodsLabel: "LOGIN METHODS", + passwordLoginLabel: "Password login", + passwordDisableHelperText: 'Disable password login only if your SSO is configured otherwise you will get locked out', ssoHeader: "SSO", instanceSsoCard: "Instance SSO (3)", instanceSsoHelperText: "Display instance SSO for workspace URL login", @@ -101,6 +103,11 @@ export const ssoText = { cancelButton: "Cancel", saveButton: "Save changes", allowedDomain: "tooljet.io,gmail.com", + passwordLoginDropdownLabel: "Password login", + passwordAllowedDomainsLabel: "Allowed domains", + passwordRestrictedDomainsLabel: "Restricted domains", + ssoLoginDropdownLabel: "SSO login", + ssoAllowedDomainsLabel: "Allowed domains", passwordDisableWarning: "Please ensure SSO is configured successfully before disabling password login or else you will get locked out. Are you sure you want to continue?", superAdminInfoText: `Super admin can still access their account via ${Cypress.config("baseUrl")}/login/super-admin`, ssoToast: "Organization settings have been updated", diff --git a/cypress-tests/cypress/support/utils/manageSSO.js b/cypress-tests/cypress/support/utils/manageSSO.js index 21578eef95..a0c2872cf7 100644 --- a/cypress-tests/cypress/support/utils/manageSSO.js +++ b/cypress-tests/cypress/support/utils/manageSSO.js @@ -11,10 +11,41 @@ import { commonText } from "Texts/common"; import { ssoText } from "Texts/manageSSO"; export const verifyLoginSettings = (pageName) => { - cy.get(ssoSelector.enableSignUpToggle).should("be.visible"); - cy.get(ssoSelector.allowedDomainInput).should("be.visible"); - cy.get(ssoSelector.workspaceLoginUrl).should("be.visible"); - cy.get(commonSelectors.copyIcon).should("be.visible"); + //Verify Password and SSO Domain section + cy.get(ssoSelector.passwordLoginDropdown).verifyVisibleElement("have.text", ssoText.passwordLoginDropdownLabel).click(); + cy.get(ssoSelector.passwordAllowedDomainsLabel).should("be.visible"); + cy.scrollToElement(ssoSelector.passwordAllowedDomainsInput).should("be.enabled"); + cy.clearAndType(ssoSelector.passwordAllowedDomainsInput, "allow.com"); + cy.scrollToElement(ssoSelector.passwordRestrictedDomainsLabel); + cy.scrollToElement(ssoSelector.passwordRestrictedDomainsInput).should("be.enabled"); + cy.clearAndType(ssoSelector.passwordRestrictedDomainsInput, "restrict.com"); + cy.get(ssoSelector.ssoLoginDropdown).verifyVisibleElement("have.text", ssoText.ssoLoginDropdownLabel).click(); + cy.scrollToElement(ssoSelector.ssoAllowedDomainsLabel); + cy.scrollToElement(ssoSelector.ssoAllowedDomainsInput).should("be.enabled"); + cy.clearAndType(ssoSelector.ssoAllowedDomainsInput, "allow.com"); + cy.get(ssoSelector.saveButton).verifyVisibleElement("have.text", ssoText.saveButton).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + ssoText[`${pageName}SsoToast`] + ); + + [ + ssoSelector.passwordAllowedDomainsInput, + ssoSelector.passwordRestrictedDomainsInput, + ssoSelector.ssoAllowedDomainsInput + ].forEach(selector => { + cy.clearAndType(selector, `{selectall}{backspace}`); + }); + cy.scrollToElement(ssoSelector.saveButton).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + ssoText[`${pageName}SsoToast`] + ); + + cy.scrollToElement(ssoSelector.ssoLoginDropdown).click(); + cy.scrollToElement(ssoSelector.passwordLoginDropdown).click(); + cy.scrollToElement(ssoSelector.workspaceLoginUrl); + cy.scrollToElement(commonSelectors.copyIcon); cy.get(ssoSelector.cancelButton).verifyVisibleElement( "have.text", @@ -24,8 +55,7 @@ export const verifyLoginSettings = (pageName) => { "have.text", ssoText.saveButton ); - - cy.get(ssoSelector.passwordEnableToggle).should("be.visible"); + cy.scrollToElement(ssoSelector.passwordEnableToggle); //Configure sign up toggle cy.get(ssoSelector.enableSignUpToggle).check(); @@ -39,13 +69,6 @@ export const verifyLoginSettings = (pageName) => { cy.get(ssoSelector.saveButton).click(); cy.get(ssoSelector.enableSignUpToggle).should("not.be.checked"); - cy.clearAndType(ssoSelector.allowedDomainInput, ssoText.allowedDomain); - cy.get(ssoSelector.saveButton).click(); - cy.verifyToastMessage( - commonSelectors.toastMessage, - ssoText[`${pageName}SsoToast`] - ); - cy.get(ssoSelector.passwordEnableToggle).uncheck(); cy.get(commonSelectors.modalComponent).should("be.visible"); cy.get(ssoSelector.disablePasswordLoginTitle).verifyVisibleElement( From afcb9d758ba69499d18924fb9646609504412c5b Mon Sep 17 00:00:00 2001 From: Yukti Goyal Date: Wed, 31 Dec 2025 15:56:49 +0530 Subject: [PATCH 08/10] added auto sso custom commad --- .../commands/platform/platformApiCommands.js | 15 ++++++++++++++- .../ceTestcases/workspace/manageSSO.cy.js | 1 + 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/cypress-tests/cypress/commands/platform/platformApiCommands.js b/cypress-tests/cypress/commands/platform/platformApiCommands.js index c9542b241c..0bec38c6b1 100644 --- a/cypress-tests/cypress/commands/platform/platformApiCommands.js +++ b/cypress-tests/cypress/commands/platform/platformApiCommands.js @@ -749,6 +749,19 @@ Cypress.Commands.add( }); } ); +Cypress.Commands.add( + "apiUpdateAutoSSO", + (state, scope = "instance", returnCached = false) => { + cy.getAuthHeaders(returnCached).then((headers) => { + cy.request({ + method: "PATCH", + url: `${Cypress.env("server_host")}/api/login-configs/${scope}-general`, + headers: headers, + body: { automaticSsoLogin: state }, + }); + }); + } +); Cypress.Commands.add( "apiFullUserOnboarding", @@ -782,7 +795,7 @@ Cypress.Commands.add( performOnboarding(userEmail, userPassword, organizationToken); } - function performOnboarding (email, password, orgToken) { + function performOnboarding(email, password, orgToken) { cy.task("dbConnection", { dbconfig: Cypress.env("app_db"), sql: ` diff --git a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/workspace/manageSSO.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/workspace/manageSSO.cy.js index 98fb22c4a6..d5be5afa39 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/workspace/manageSSO.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/workspace/manageSSO.cy.js @@ -40,6 +40,7 @@ describe("Manage SSO for multi workspace", () => { }); it("Should verify workspace settings page elements and their functionality", () => { + cy.apiUpdateAutoSSO(false, "organization"); SSO.setSignupStatus(false); SSO.defaultSSO(true); common.navigateToManageSSO(); From 672e7d6832ac4cc97ea79b0f32421f19a7bb9fcc Mon Sep 17 00:00:00 2001 From: Siddharthpl Date: Fri, 2 Jan 2026 11:46:41 +0530 Subject: [PATCH 09/10] Fixed the flickering of container --- .../BaseWorkspaceSettingsPage/BaseWorkspaceSettingsPage.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/modules/WorkspaceSettings/components/BaseWorkspaceSettingsPage/BaseWorkspaceSettingsPage.jsx b/frontend/src/modules/WorkspaceSettings/components/BaseWorkspaceSettingsPage/BaseWorkspaceSettingsPage.jsx index 6f7e1ffaca..2f4f4be1aa 100644 --- a/frontend/src/modules/WorkspaceSettings/components/BaseWorkspaceSettingsPage/BaseWorkspaceSettingsPage.jsx +++ b/frontend/src/modules/WorkspaceSettings/components/BaseWorkspaceSettingsPage/BaseWorkspaceSettingsPage.jsx @@ -111,7 +111,7 @@ export default function WorkspaceSettingsPage({ extraLinks, ...props }) { -
+
From 06d690fe5f7ccdf45768719908531b096392f8d4 Mon Sep 17 00:00:00 2001 From: Yukti Goyal Date: Mon, 5 Jan 2026 12:11:18 +0530 Subject: [PATCH 10/10] Updated the helper text --- cypress-tests/cypress/constants/texts/manageSSO.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cypress-tests/cypress/constants/texts/manageSSO.js b/cypress-tests/cypress/constants/texts/manageSSO.js index f6282eaddc..5a12741f1d 100644 --- a/cypress-tests/cypress/constants/texts/manageSSO.js +++ b/cypress-tests/cypress/constants/texts/manageSSO.js @@ -5,7 +5,7 @@ export const ssoText = { cardTitle: "Instance login", configurationLabel: "CONFIGURATION", domainConstraintsLabel: "Domain constraints", - domainConstraintsHelperText: "This is applicable only for users that sign up without invite. Enter multiple domains by comma separated values. Example: tooljet.com, tooljet.io", + domainConstraintsHelperText: "Support multiple domains. Enter domain names separated by comma. Example: tooljet.com, tooljet.io", superAdminUrlLabel: "Super admin login URL", superAdminUrl: `${Cypress.config("baseUrl")}/login/super-admin`, superAdminUrlHelperText: "Use this URL for super admin to login via password", @@ -31,7 +31,7 @@ export const ssoText = { cardTitle: "Workspace login", configurationLabel: "CONFIGURATION", domainConstraintsLabel: "Domain constraints", - domainConstraintsHelperText: "This is applicable only for users that sign up without invite. Enter multiple domains by comma separated values. Example: tooljet.com, tooljet.io", + domainConstraintsHelperText: "Support multiple domains. Enter domain names separated by comma. Example: tooljet.com, tooljet.io", workspaceLoginUrlLabel: "Login URL", workspaceLoginUrl: `${Cypress.config("baseUrl")}/login/my-workspace`, workspaceLoginUrlHelperText: "Use this URL to login directly to this workspace",