Feat: prevent non work emails (#3993)

This commit is contained in:
Mike
2025-02-26 10:55:02 +01:00
committed by GitHub
parent 6daccd921f
commit 2ecb98146a
33 changed files with 135 additions and 56 deletions
@@ -6,7 +6,7 @@
v-model="email"
type="email"
name="email"
label="Email"
label="Work email"
placeholder="Email"
size="lg"
color="foundation"
@@ -69,17 +69,14 @@ import { ToastNotificationType, useGlobalToast } from '~~/lib/common/composables
import { ensureError } from '@speckle/shared'
import { useAuthManager } from '~~/lib/auth/composables/auth'
import { loginRoute } from '~~/lib/common/helpers/route'
import { passwordRules } from '~~/lib/auth/helpers/validation'
import {
passwordRules,
doesNotContainBlockedDomain
} from '~~/lib/auth/helpers/validation'
import { graphql } from '~~/lib/common/generated/gql'
import type { ServerTermsOfServicePrivacyPolicyFragmentFragment } from '~~/lib/common/generated/gql/graphql'
import { useMounted } from '@vueuse/core'
/**
* TODO:
* - (BE) Password strength check? Do we want to use it anymore?
* - Dim's answer: no, `passwordRules` are legit enough for now.
*/
graphql(`
fragment ServerTermsOfServicePrivacyPolicyFragment on ServerInfo {
termsOfService
@@ -99,13 +96,18 @@ const router = useRouter()
const { signUpWithEmail, inviteToken } = useAuthManager()
const { triggerNotification } = useGlobalToast()
const isMounted = useMounted()
const isNoPersonalEmailsEnabled = useIsNoPersonalEmailsEnabled()
const newsletterConsent = defineModel<boolean>('newsletterConsent', { required: true })
const loading = ref(false)
const password = ref('')
const email = ref('')
const emailRules = [isEmail]
const emailRules = computed(() =>
inviteToken.value || !isNoPersonalEmailsEnabled.value
? [isEmail]
: [isEmail, doesNotContainBlockedDomain]
)
const nameRules = [isRequired]
const isEmailDisabled = computed(() => !!props.inviteEmail?.length || loading.value)
@@ -71,4 +71,12 @@ export const useIsBillingIntegrationEnabled = () => {
return ref(FF_BILLING_INTEGRATION_ENABLED)
}
export const useIsNoPersonalEmailsEnabled = () => {
const {
public: { FF_NO_PERSONAL_EMAILS_ENABLED }
} = useRuntimeConfig()
return ref(FF_NO_PERSONAL_EMAILS_ENABLED)
}
export { useGlobalToast, useActiveUser, usePageQueryStandardFetchPolicy }
@@ -1,4 +1,5 @@
import { isStringOfLength, stringContains } from '~~/lib/common/helpers/validation'
import { blockedDomains } from '@speckle/shared'
export const passwordLongEnough = isStringOfLength({ minLength: 8 })
export const passwordHasAtLeastOneNumber = stringContains({
@@ -20,3 +21,10 @@ export const passwordRules = [
passwordHasAtLeastOneLowercaseLetter,
passwordHasAtLeastOneUppercaseLetter
]
export const doesNotContainBlockedDomain = (val: string) => {
const domain = val.split('@')[1]?.toLowerCase()
return domain && blockedDomains.includes(domain)
? 'Please use your work email instead of a personal email address'
: true
}
@@ -5,8 +5,11 @@ import {
} from '@/modules/core/services/ratelimiter'
import { getIpFromRequest } from '@/modules/shared/utils/ip'
import { InviteNotFoundError } from '@/modules/serverinvites/errors'
import { UserInputError, PasswordTooShortError } from '@/modules/core/errors/userinput'
import {
UserInputError,
PasswordTooShortError,
BlockedEmailDomainError
} from '@/modules/core/errors/userinput'
import { ServerInviteResourceType } from '@/modules/serverinvites/domain/constants'
import { getResourceTypeRole } from '@/modules/serverinvites/helpers/core'
import { AuthStrategyMetadata, AuthStrategyBuilder } from '@/modules/auth/helpers/types'
@@ -117,7 +120,7 @@ const localStrategyBuilderFactory =
invite = await deps.validateServerInvite(user.email, req.session.token)
}
// 3. at this point we know, that we have one of these cases:
// 3.. at this point we know, that we have one of these cases:
// * the server is invite only and the user has a valid invite
// * the server public and the user has a valid invite
// * the server public and the user doesn't have an invite
@@ -155,6 +158,7 @@ const localStrategyBuilderFactory =
case PasswordTooShortError:
case UserInputError:
case InviteNotFoundError:
case BlockedEmailDomainError:
req.log.info({ err }, 'Error while registering.')
return res.status(400).send({ err: e.message })
default:
@@ -130,7 +130,7 @@ describe('GraphQL @apps-api', () => {
;({ sendRequest } = await initializeTestServer(ctx))
testUser = {
name: 'Dimitrie Stefanescu',
email: 'didimitrie@gmail.com',
email: 'didimitrie@example.org',
password: 'wtfwtfwtf'
}
@@ -158,7 +158,7 @@ const validateToken = validateTokenFactory({
describe('Services @apps-services', () => {
const actor = {
name: 'Dimitrie Stefanescu',
email: 'didimitrie@gmail.com',
email: 'didimitrie@example.org',
password: 'wtfwtfwtf'
}
@@ -495,7 +495,7 @@ describe('Services @apps-services', () => {
})
const secondUser = {
name: 'Dimitrie Stefanescu',
email: 'didimitrie.wow@gmail.com',
email: 'didimitrie.wow@example.org',
password: 'wtfwtfwtf'
}
@@ -233,7 +233,7 @@ export type LocalAuthRestApiHelpers = ReturnType<typeof localAuthRestApi>
export const generateRegistrationParams = (): RegisterParams => ({
challenge: faker.string.uuid(),
user: {
email: (random(0, 1000) + faker.internet.email()).toLowerCase(),
email: `${random(0, 1000)}@example.org`.toLowerCase(),
password: faker.internet.password(),
name: faker.person.fullName()
}
@@ -8,6 +8,7 @@ import {
import { AllScopes } from '@/modules/core/helpers/mainConstants'
import { updateServerInfoFactory } from '@/modules/core/repositories/server'
import { findInviteFactory } from '@/modules/serverinvites/repositories/serverInvites'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import { expectToThrow, itEach } from '@/test/assertionHelper'
import { BasicTestUser, createTestUsers } from '@/test/authHelper'
import {
@@ -33,6 +34,8 @@ import {
import { Roles } from '@speckle/shared'
import { expect } from 'chai'
const { FF_NO_PERSONAL_EMAILS_ENABLED } = getFeatureFlags()
const updateServerInfo = updateServerInfoFactory({ db })
describe('Server registration', () => {
@@ -96,6 +99,18 @@ describe('Server registration', () => {
expect(user.emails.every((e) => !e.verified)).to.be.true
})
FF_NO_PERSONAL_EMAILS_ENABLED
? it('rejects registration with blocked email domain', async () => {
const params = generateRegistrationParams()
params.user.email = 'test@gmail.com'
const error = await expectToThrow(() => restApi.register(params))
expect(error.message).to.contain(
'Please use your work email instead of a personal email address'
)
})
: null
it('fails without challenge', async () => {
const params = generateRegistrationParams()
params.challenge = ''
@@ -25,3 +25,9 @@ export class UnverifiedEmailSSOLoginError extends UserInputError<UnverifiedEmail
'Email already in use by a user with unverified email. Verify the email on the existing user to be able to log in with this method.'
static code = 'UNVERIFIED_EMAIL_SSO_LOGIN_ERROR'
}
export class BlockedEmailDomainError extends UserInputError {
static defaultMessage =
'Please use your work email instead of a personal email address'
static code = 'BLOCKED_EMAIL_DOMAIN_ERROR'
}
@@ -962,7 +962,7 @@ export type DiscoverableStreamsSortingInput = {
export type DiscoverableWorkspaceCollaborator = {
__typename?: 'DiscoverableWorkspaceCollaborator';
avatar: Scalars['String']['output'];
avatar?: Maybe<Scalars['String']['output']>;
};
export type DiscoverableWorkspaceCollaboratorCollection = {
@@ -5908,7 +5908,7 @@ export interface DateTimeScalarConfig extends GraphQLScalarTypeConfig<ResolversT
}
export type DiscoverableWorkspaceCollaboratorResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['DiscoverableWorkspaceCollaborator'] = ResolversParentTypes['DiscoverableWorkspaceCollaborator']> = {
avatar?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
avatar?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
@@ -21,11 +21,16 @@ import {
UserUpdateError,
UserValidationError
} from '@/modules/core/errors/user'
import { PasswordTooShortError, UserInputError } from '@/modules/core/errors/userinput'
import {
BlockedEmailDomainError,
PasswordTooShortError,
UserInputError
} from '@/modules/core/errors/userinput'
import { UserUpdateInput } from '@/modules/core/graph/generated/graphql'
import type { UserRecord } from '@/modules/core/helpers/userHelper'
import { sanitizeImageUrl } from '@/modules/shared/helpers/sanitization'
import {
blockedDomains,
isNullOrUndefined,
NullableKeysToOptional,
Roles,
@@ -48,6 +53,9 @@ import { DeleteAllUserInvites } from '@/modules/serverinvites/domain/operations'
import { GetServerInfo } from '@/modules/core/domain/server/operations'
import { EventBusEmit } from '@/modules/shared/services/eventBus'
import { UserEvents } from '@/modules/core/domain/users/events'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
const { FF_NO_PERSONAL_EMAILS_ENABLED } = getFeatureFlags()
export const MINIMUM_PASSWORD_LENGTH = 8
@@ -163,6 +171,15 @@ export const createUserFactory =
if (!finalUser.email?.length) throw new UserInputError('E-mail address is required')
// Temporary experiment: require work emails for all new users
const isBlockedDomain = blockedDomains.includes(
finalUser.email.split('@')[1]?.toLowerCase()
)
const requireWorkDomain =
!user?.signUpContext?.isInvite && FF_NO_PERSONAL_EMAILS_ENABLED
if (requireWorkDomain && isBlockedDomain) throw new BlockedEmailDomainError()
let expectedRole = null
if (finalUser.role) {
const isValidRole = Object.values(Roles.Server).includes(finalUser.role)
@@ -49,7 +49,7 @@ const createAppToken = createAppTokenFactory({
describe('API Tokens', () => {
const user1: BasicTestUser = {
name: 'Dimitrie Stefanescu',
email: 'didimitrie@gmail.com',
email: 'didimitrie@example.org',
password: 'sn3aky-1337-b1m',
id: ''
}
@@ -321,7 +321,7 @@ describe('API Tokens', () => {
describe('with limited resource access', () => {
const user2: BasicTestUser = {
name: 'Some other guy',
email: 'bababooey@gmail.com',
email: 'bababooey@example.org',
password: 'sn3aky-1337-b1m',
id: ''
}
@@ -53,13 +53,13 @@ describe('Batch commits', () => {
const me: BasicTestUser = {
name: 'batch commit dude',
email: 'batchcommitguy@gmail.com',
email: 'batchcommitguy@example.org',
id: ''
}
const otherGuy: BasicTestUser = {
name: 'other batch commit guy',
email: 'otherbatchcommitguy@gmail.com',
email: 'otherbatchcommitguy@example.org',
id: ''
}
@@ -191,7 +191,7 @@ const createObject = createObjectFactory({
describe('Branches @core-branches', () => {
const user = {
name: 'Dimitrie Stefanescu',
email: 'didimitrie4342@gmail.com',
email: 'didimitrie4342@example.org',
password: 'sn3aky-1337-b1m',
id: ''
}
@@ -211,7 +211,7 @@ const createObject = createObjectFactory({
describe('Commits @core-commits', () => {
const user = {
name: 'Dimitrie Stefanescu',
email: 'didimitrie4342@gmail.com',
email: 'didimitrie4342@example.org',
password: 'sn3aky-1337-b1m',
id: ''
}
@@ -221,12 +221,12 @@ describe('Favorite streams', () => {
}
const me = {
name: 'Itsa Me',
email: 'me@gmail.com',
email: 'me@example.org',
password: 'sn3aky-1337-b1m'
}
const otherGuy = {
name: 'Some Other DUde',
email: 'otherguy@gmail.com',
email: 'otherguy@example.org',
password: 'sn3aky-1337-b1m'
}
@@ -240,12 +240,12 @@ describe('Generic AuthN & AuthZ controller tests', () => {
}
const serverOwner = {
name: 'Itsa Me',
email: 'me@gmail.com',
email: 'me@example.org',
password: 'sn3aky-1337-b1m'
}
const otherGuy = {
name: 'Some Other DUde',
email: 'otherguy@gmail.com',
email: 'otherguy@example.org',
password: 'sn3aky-1337-b1m'
}
@@ -179,7 +179,7 @@ const getObjects = getStreamObjectsFactory({ db })
describe('Objects @core-objects', () => {
const userOne = {
name: 'Dimitrie Stefanescu',
email: 'didimitrie43@gmail.com',
email: 'didimitrie43@example.org',
password: 'sn3aky-1337-b1m'
}
@@ -193,14 +193,14 @@ const createObject = createObjectFactory({
describe('Streams @core-streams', () => {
const userOne: BasicTestUser = {
name: 'Dimitrie Stefanescu',
email: 'didimitrie@gmail.com',
email: 'didimitrie@example.org',
password: 'sn3aky-1337-b1m',
id: ''
}
const userTwo: BasicTestUser = {
name: 'Dimitrie Stefanescu 2',
email: 'didimitrie2@gmail.com',
email: 'didimitrie2@example.org',
password: 'sn3aky-1337-b1m',
id: ''
}
@@ -266,7 +266,7 @@ const createObject = createObjectFactory({
describe('Actors & Tokens @user-services', () => {
const myTestActor = {
name: 'Dimitrie Stefanescu',
email: 'didimitrie@gmail.com',
email: 'didimitrie@example.org',
password: 'sn3aky-1337-b1m',
id: ''
}
@@ -139,7 +139,7 @@ async function getOrderedUserIds() {
describe('[Admin users list]', () => {
const me = {
name: 'Mr Server Admin Dude',
email: 'adminuserguy@gmail.com',
email: 'adminuserguy@example.org',
password: 'sn3aky-1337-b1m',
id: undefined as Optional<string>,
verified: false
@@ -197,7 +197,7 @@ describe('[Admin users list]', () => {
name: `User #${i} - ${
remainingSearchQueryUserCount-- >= 1 ? SEARCH_QUERY : ''
}`,
email: `speckleuser${i}@gmail.com`,
email: `speckleuser${i}@example.org`,
password: 'sn3aky-1337-b1m',
verified: false
})
@@ -225,7 +225,7 @@ describe('[Admin users list]', () => {
{
email: `randominvitee${i}.${
remainingSearchQueryInviteCount-- >= 1 ? SEARCH_QUERY : ''
}@gmail.com`
}@example.org`
},
randomEl(userIds)
)
@@ -237,7 +237,7 @@ describe('[Admin users list]', () => {
const { id: streamId, ownerId } = randomEl(streamData)
const email = `streamrandominvitee${i}.${
remainingSearchQueryInviteCount-- >= 1 ? SEARCH_QUERY : ''
}@gmail.com`
}@example.org`
await createInviteDirectly(
{
@@ -943,7 +943,7 @@ export type DiscoverableStreamsSortingInput = {
export type DiscoverableWorkspaceCollaborator = {
__typename?: 'DiscoverableWorkspaceCollaborator';
avatar: Scalars['String']['output'];
avatar?: Maybe<Scalars['String']['output']>;
};
export type DiscoverableWorkspaceCollaboratorCollection = {
@@ -130,7 +130,7 @@ describe('FileUploads @fileuploads', () => {
const userOne = {
name: 'User',
email: 'user@gmail.com',
email: 'user@example.org',
password: 'jdsadjsadasfdsa'
}
@@ -57,14 +57,14 @@ const mailerMock = EmailSendingServiceMock
describe('[Stream & Server Invites]', () => {
const me: BasicTestUser = {
name: 'Authenticated server invites guy',
email: 'serverinvitesguy@gmail.com',
email: 'serverinvitesguy@example.org',
password: 'sn3aky-1337-b1m',
id: ''
}
const otherGuy: BasicTestUser = {
name: 'Some Other DUde',
email: 'otherguy111@gmail.com',
email: 'otherguy111@example.org',
password: 'sn3aky-1337-b1m',
id: ''
}
@@ -883,7 +883,7 @@ describe('[Stream & Server Invites]', () => {
const ownInvitesGuy: BasicTestUser = {
name: "Some guy who's invited a lot",
email: 'mrinvitedguy111@gmail.com',
email: 'mrinvitedguy111@example.org',
password: 'sn3aky-1337-b1m',
id: ''
}
@@ -165,7 +165,7 @@ describe('Webhooks @webhooks', () => {
const userOne = {
name: 'User',
email: 'user@gmail.com',
email: 'user@example.org',
password: 'jdsadjsadasfdsa'
}
@@ -327,7 +327,7 @@ describe('Webhooks @webhooks', () => {
describe('GraphQL API Webhooks @webhooks-api', () => {
const userTwo = {
name: 'User2',
email: 'user2@gmail.com',
email: 'user2@example.org',
password: 'jdsadjsadasfdsa'
}
@@ -84,13 +84,13 @@ describe('Workspaces Invites GQL', () => {
const me: BasicTestUser = {
name: 'Authenticated server invites guy',
email: 'serverinvitesguy@gmail.com',
email: 'serverinvitesguy@example.org',
id: ''
}
const otherGuy: BasicTestUser = {
name: 'Some Other DUde',
email: 'otherguy111@gmail.com',
email: 'otherguy111@example.org',
id: ''
}
@@ -249,7 +249,7 @@ describe('Workspaces Invites GQL', () => {
const res = await gqlHelpers.batchCreateInvites({
workspaceId: myFirstWorkspace.id,
input: times(11, () => ({
email: `asdasasd${Math.random()}@gmail.com`,
email: `asdasasd${Math.random()}@example.org`,
role: WorkspaceRole.Member
}))
})
@@ -264,7 +264,7 @@ describe('Workspaces Invites GQL', () => {
const res = await gqlHelpers.batchCreateInvites({
workspaceId: otherGuysWorkspace.id,
input: times(10, () => ({
email: `asdasasd${Math.random()}@gmail.com`,
email: `asdasasd${Math.random()}@example.org`,
role: WorkspaceRole.Member
}))
})
@@ -312,7 +312,7 @@ describe('Workspaces Invites GQL', () => {
const res = await gqlHelpers.batchCreateInvites({
workspaceId: myFirstWorkspace.id,
input: times(count, () => ({
email: `asdasasd${Math.random()}@gmail.com`,
email: `asdasasd${Math.random()}@example.org`,
role: WorkspaceRole.Member
}))
})
@@ -483,7 +483,7 @@ describe('Workspaces Invites GQL', () => {
const workspaceMemberWithNoProjectAccess: BasicTestUser = {
name: 'Workspace Member With No Project Access #1',
email: 'workspaceMemberWithNoProjectAccess1@gmail.com',
email: 'workspaceMemberWithNoProjectAccess1@example.org',
id: ''
}
@@ -678,7 +678,7 @@ describe('Workspaces Invites GQL', () => {
{
workspaceId: myAdministrationWorkspace.id,
input: times(10, () => ({
email: `aszzzdasasd${Math.random()}@gmail.com`,
email: `aszzzdasasd${Math.random()}@example.org`,
role: WorkspaceRole.Member
}))
},
@@ -1456,7 +1456,7 @@ describe('Workspaces Invites GQL', () => {
const otherWorkspaceOwner: BasicTestUser = {
name: 'Other Workspace Owner',
email: 'otherworkspaceowner@gmail.com',
email: 'otherworkspaceowner@example.org',
id: ''
}
+3 -2
View File
@@ -35,6 +35,7 @@ import { getEventBus } from '@/modules/shared/services/eventBus'
import { createTestContext, testApolloServer } from '@/test/graphqlHelper'
import { faker } from '@faker-js/faker'
import { ServerScope, wait } from '@speckle/shared'
import cryptoRandomString from 'crypto-random-string'
import { isArray, isNumber, kebabCase, omit, times } from 'lodash'
const getServerInfo = getServerInfoFactory({ db })
@@ -88,7 +89,7 @@ export type BasicTestUser = {
const initTestUser = (user: Partial<BasicTestUser>): BasicTestUser => ({
name: faker.person.fullName(),
email: faker.internet.email(),
email: `${cryptoRandomString({ length: 15 })}@example.org`,
id: '',
...user
})
@@ -115,7 +116,7 @@ export async function createTestUser(userObj?: Partial<BasicTestUser>) {
}
if (!baseUser.email) {
setVal('email', `${kebabCase(baseUser.name)}@someemail.com`)
setVal('email', `${kebabCase(baseUser.name)}@example.org`)
}
const id = await createUser(omit(baseUser, ['id']), { skipPropertyValidation: true })
@@ -944,7 +944,7 @@ export type DiscoverableStreamsSortingInput = {
export type DiscoverableWorkspaceCollaborator = {
__typename?: 'DiscoverableWorkspaceCollaborator';
avatar: Scalars['String']['output'];
avatar?: Maybe<Scalars['String']['output']>;
};
export type DiscoverableWorkspaceCollaboratorCollection = {
@@ -5663,7 +5663,7 @@ export type GetWorkspaceBySlugQuery = { __typename?: 'Query', workspaceBySlug: {
export type GetActiveUserDiscoverableWorkspacesQueryVariables = Exact<{ [key: string]: never; }>;
export type GetActiveUserDiscoverableWorkspacesQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', discoverableWorkspaces: Array<{ __typename?: 'LimitedWorkspace', id: string, name: string, description?: string | null, team?: { __typename?: 'DiscoverableWorkspaceCollaboratorCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'DiscoverableWorkspaceCollaborator', avatar: string }> } | null }> } | null };
export type GetActiveUserDiscoverableWorkspacesQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', discoverableWorkspaces: Array<{ __typename?: 'LimitedWorkspace', id: string, name: string, description?: string | null, team?: { __typename?: 'DiscoverableWorkspaceCollaboratorCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'DiscoverableWorkspaceCollaborator', avatar?: string | null }> } | null }> } | null };
export type UpdateWorkspaceMutationVariables = Exact<{
input: WorkspaceUpdateInput;
+6
View File
@@ -66,6 +66,11 @@ const parseFeatureFlags = () => {
schema: z.boolean(),
defaults: { production: false, _: false }
},
// Enable to not allow personal emails
FF_NO_PERSONAL_EMAILS_ENABLED: {
schema: z.boolean(),
defaults: { production: false, _: false }
},
// Fixes the streaming of objects by ensuring that the database stream is closed properly
FF_OBJECTS_STREAMING_FIX: {
schema: z.boolean(),
@@ -104,6 +109,7 @@ export function getFeatureFlags(): {
FF_FORCE_ONBOARDING: boolean
FF_OBJECTS_STREAMING_FIX: boolean
FF_MOVE_PROJECT_REGION_ENABLED: boolean
FF_NO_PERSONAL_EMAILS_ENABLED: boolean
} {
if (!parsedFlags) parsedFlags = parseFeatureFlags()
return parsedFlags
@@ -563,6 +563,9 @@ Generate the environment variables for Speckle server and Speckle objects deploy
- name: FF_WORKSPACES_MODULE_ENABLED
value: {{ .Values.featureFlags.workspacesModuleEnabled | quote }}
- name: FF_NO_PERSONAL_EMAILS_ENABLED
value: {{ .Values.featureFlags.noPersonalEmailsEnabled | quote }}
- name: FF_WORKSPACES_SSO_ENABLED
value: {{ .Values.featureFlags.workspacesSSOEnabled | quote }}
@@ -138,6 +138,8 @@ spec:
value: {{ .Values.featureFlags.gendoAIModuleEnabled | quote }}
- name: NUXT_PUBLIC_FF_FORCE_ONBOARDING
value: {{ .Values.featureFlags.forceOnboarding | quote }}
- name: NUXT_PUBLIC_FF_NO_PERSONAL_EMAILS_ENABLED
value: {{ .Values.featureFlags.noPersonalEmailsEnabled | quote }}
{{- if .Values.analytics.survicate_workspace_key }}
- name: NUXT_PUBLIC_SURVICATE_WORKSPACE_KEY
value: {{ .Values.analytics.survicate_workspace_key | quote }}
@@ -89,6 +89,11 @@
"type": "boolean",
"description": "Forces onboarding for all users",
"default": false
},
"noPersonalEmailsEnabled": {
"type": "boolean",
"description": "Disables the ability sign up with personal email addresses",
"default": false
}
}
},
+2
View File
@@ -57,6 +57,8 @@ featureFlags:
forceEmailVerification: false
## @param featureFlags.forceOnboarding Forces onboarding for all users
forceOnboarding: false
## @param featureFlags.noPersonalEmailsEnabled Disables the ability sign up with personal email addresses
noPersonalEmailsEnabled: false
analytics:
## @param analytics.enabled Enable or disable analytics