diff --git a/packages/server/modules/auth/index.ts b/packages/server/modules/auth/index.ts index 3aaefe2d8..9ed05608d 100644 --- a/packages/server/modules/auth/index.ts +++ b/packages/server/modules/auth/index.ts @@ -54,38 +54,24 @@ import { } from '@/modules/core/repositories/userEmails' import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails' import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request' -import { requestNewEmailVerificationFactory as requestNewEmailVerificationFactoryOld } from '@/modules/emails/services/verification/request.old' import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories' import { renderEmail } from '@/modules/emails/services/emailRendering' import { sendEmail } from '@/modules/emails/services/sending' import { getServerInfoFactory } from '@/modules/core/repositories/server' import { initializeEventListenerFactory } from '@/modules/auth/services/postAuth' import { getEventBus } from '@/modules/shared/services/eventBus' -import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' -const { FF_FORCE_EMAIL_VERIFICATION } = getFeatureFlags() const findEmail = findEmailFactory({ db }) -const requestNewEmailVerification = FF_FORCE_EMAIL_VERIFICATION - ? requestNewEmailVerificationFactory({ - findEmail, - getUser: getUserFactory({ db }), - getServerInfo: getServerInfoFactory({ db }), - deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ - db - }), - renderEmail, - sendEmail - }) - : requestNewEmailVerificationFactoryOld({ - findEmail, - getUser: getUserFactory({ db }), - getServerInfo: getServerInfoFactory({ db }), - deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ - db - }), - renderEmail, - sendEmail - }) +const requestNewEmailVerification = requestNewEmailVerificationFactory({ + findEmail, + getUser: getUserFactory({ db }), + getServerInfo: getServerInfoFactory({ db }), + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail +}) const createUser = createUserFactory({ getServerInfo: getServerInfoFactory({ db }), diff --git a/packages/server/modules/core/graph/resolvers/userEmails.ts b/packages/server/modules/core/graph/resolvers/userEmails.ts index 83e0ef441..74439855c 100644 --- a/packages/server/modules/core/graph/resolvers/userEmails.ts +++ b/packages/server/modules/core/graph/resolvers/userEmails.ts @@ -10,7 +10,6 @@ import { } from '@/modules/core/repositories/userEmails' import { db } from '@/db/knex' import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request' -import { requestNewEmailVerificationFactory as requestNewEmailVerificationFactoryOld } from '@/modules/emails/services/verification/request.old' import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing' import { deleteServerOnlyInvitesFactory, @@ -31,32 +30,18 @@ import { verifyUserEmailFactory } from '@/modules/core/services/users/emailVerification' import { commandFactory } from '@/modules/shared/command' -import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' - -const { FF_FORCE_EMAIL_VERIFICATION } = getFeatureFlags() const getUser = getUserFactory({ db }) -const requestNewEmailVerification = FF_FORCE_EMAIL_VERIFICATION - ? requestNewEmailVerificationFactory({ - findEmail: findEmailFactory({ db }), - getUser, - getServerInfo: getServerInfoFactory({ db }), - deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ - db - }), - renderEmail, - sendEmail - }) - : requestNewEmailVerificationFactoryOld({ - findEmail: findEmailFactory({ db }), - getUser, - getServerInfo: getServerInfoFactory({ db }), - deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ - db - }), - renderEmail, - sendEmail - }) +const requestNewEmailVerification = requestNewEmailVerificationFactory({ + findEmail: findEmailFactory({ db }), + getUser, + getServerInfo: getServerInfoFactory({ db }), + deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ + db + }), + renderEmail, + sendEmail +}) export = { ActiveUserMutations: { diff --git a/packages/server/modules/core/tests/integration/userEmails.graph.spec.ts b/packages/server/modules/core/tests/integration/userEmails.graph.spec.ts index ce2d5a574..110f2c375 100644 --- a/packages/server/modules/core/tests/integration/userEmails.graph.spec.ts +++ b/packages/server/modules/core/tests/integration/userEmails.graph.spec.ts @@ -41,9 +41,7 @@ import { getEventBus } from '@/modules/shared/services/eventBus' import { createTestUser, login } from '@/test/authHelper' import { EmailVerificationFinalizationError } from '@/modules/emails/errors' import { Roles } from '@speckle/shared' -import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' -const { FF_FORCE_EMAIL_VERIFICATION } = getFeatureFlags() const getServerInfo = getServerInfoFactory({ db }) const getUser = legacyGetUserFactory({ db }) const requestNewEmailVerification = requestNewEmailVerificationFactory({ @@ -178,91 +176,89 @@ describe('User emails graphql @core', () => { ).to.eq(email.toLowerCase()) }) }) - ;(FF_FORCE_EMAIL_VERIFICATION ? describe : describe.skip)( - 'verify user email mutation', - () => { - it('should throw an error if there is no pending verification for the email', async () => { - const email = createRandomEmail() - const user = await createTestUser({ - email, - role: Roles.Server.User - }) - const session = await login(user) - // Delete email verification - await db(EmailVerifications.name).where({ email }).delete() - - const res = await session.execute(VerifyUserEmailDocument, { - input: { email, code: '123456' } - }) - - expect(res).to.haveGraphQLErrors({ - code: EmailVerificationFinalizationError.code - }) + describe('verify user email mutation', () => { + it('should throw an error if there is no pending verification for the email', async () => { + const email = createRandomEmail() + const user = await createTestUser({ + email, + role: Roles.Server.User }) - it('should throw an error if verification is expired', async () => { - const email = createRandomEmail() - const user = await createTestUser({ - email, - role: Roles.Server.User - }) - const session = await login(user) + const session = await login(user) - // Manually reset email verification code - const verificationCode = await deleteOldAndInsertNewVerificationFactory({ db })( - email - ) - // Manually expire email verification - await db(EmailVerifications.name) - .where({ email }) - .update({ createdAt: new Date('2020-01-01') }) + // Delete email verification + await db(EmailVerifications.name).where({ email }).delete() - const res = await session.execute(VerifyUserEmailDocument, { - input: { email, code: verificationCode } - }) - - expect(res).to.haveGraphQLErrors({ - code: EmailVerificationFinalizationError.code - }) + const res = await session.execute(VerifyUserEmailDocument, { + input: { email, code: '123456' } }) - it('should throw an error if code is not correct', async () => { - const email = createRandomEmail() - const user = await createTestUser({ - email, - role: Roles.Server.User - }) - const session = await login(user) - const res = await session.execute(VerifyUserEmailDocument, { - input: { email, code: '123456' } - }) - - expect(res).to.haveGraphQLErrors({ - code: EmailVerificationFinalizationError.code - }) + expect(res).to.haveGraphQLErrors({ + code: EmailVerificationFinalizationError.code }) - it('should mark user email as verified', async () => { - const email = createRandomEmail() - const user = await createTestUser({ - email, - role: Roles.Server.User - }) - const session = await login(user) - - // Manually reset email verification code - const verificationCode = await deleteOldAndInsertNewVerificationFactory({ db })( - email - ) - - const res = await session.execute(VerifyUserEmailDocument, { - input: { email, code: verificationCode } - }) - - expect(res).to.not.haveGraphQLErrors() - - const userEmail = await findEmailFactory({ db })({ email, userId: user.id }) - expect(userEmail?.verified).to.be.true + }) + it('should throw an error if verification is expired', async () => { + const email = createRandomEmail() + const user = await createTestUser({ + email, + role: Roles.Server.User }) - } - ) + const session = await login(user) + + // Manually reset email verification code + const verificationCode = await deleteOldAndInsertNewVerificationFactory({ db })( + email + ) + // Manually expire email verification + await db(EmailVerifications.name) + .where({ email }) + .update({ createdAt: new Date('2020-01-01') }) + + const res = await session.execute(VerifyUserEmailDocument, { + input: { email, code: verificationCode } + }) + + expect(res).to.haveGraphQLErrors({ + code: EmailVerificationFinalizationError.code + }) + }) + it('should throw an error if code is not correct', async () => { + const email = createRandomEmail() + const user = await createTestUser({ + email, + role: Roles.Server.User + }) + const session = await login(user) + + const res = await session.execute(VerifyUserEmailDocument, { + input: { email, code: '123456' } + }) + + expect(res).to.haveGraphQLErrors({ + code: EmailVerificationFinalizationError.code + }) + }) + it('should mark user email as verified', async () => { + const email = createRandomEmail() + const user = await createTestUser({ + email, + role: Roles.Server.User + }) + const session = await login(user) + + // Manually reset email verification code + const verificationCode = await deleteOldAndInsertNewVerificationFactory({ db })( + email + ) + + const res = await session.execute(VerifyUserEmailDocument, { + input: { email, code: verificationCode } + }) + + expect(res).to.not.haveGraphQLErrors() + + const userEmail = await findEmailFactory({ db })({ email, userId: user.id }) + expect(userEmail?.verified).to.be.true + }) + }) }) diff --git a/packages/server/modules/emails/repositories/index.ts b/packages/server/modules/emails/repositories/index.ts index 67b98cfff..95a96abc0 100644 --- a/packages/server/modules/emails/repositories/index.ts +++ b/packages/server/modules/emails/repositories/index.ts @@ -11,7 +11,6 @@ import dayjs from 'dayjs' import { Knex } from 'knex' import { hash } from 'bcrypt' import { EmailVerification } from '@/modules/emails/domain/types' -import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' const tables = { emailVerifications: (db: Knex) => db(EmailVerifications.name) @@ -64,7 +63,6 @@ function generateEmailVerificationCode() { return cryptoRandomString({ length: 6, type: 'numeric' }) } -const { FF_FORCE_EMAIL_VERIFICATION } = getFeatureFlags() /** * Delete all previous verification entries and create a new one */ @@ -81,15 +79,14 @@ export const deleteOldAndInsertNewVerificationFactory = withoutTablePrefix: true }).col - const newId = cryptoRandomString({ length: 20 }) const code = generateEmailVerificationCode() await tables.emailVerifications(deps.db).insert({ - [EmailVerificationCols.id]: newId, + [EmailVerificationCols.id]: cryptoRandomString({ length: 20 }), [EmailVerificationCols.email]: email, [EmailVerificationCols.code]: await hashEmailVerificationCode(code) }) - return FF_FORCE_EMAIL_VERIFICATION ? code : newId + return code } export const getPendingVerificationByEmailFactory = diff --git a/packages/server/modules/emails/services/verification/request.old.ts b/packages/server/modules/emails/services/verification/request.old.ts deleted file mode 100644 index 3c6f3a9d8..000000000 --- a/packages/server/modules/emails/services/verification/request.old.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { - FindEmail, - FindPrimaryEmailForUser -} from '@/modules/core/domain/userEmails/operations' -import { UserEmail } from '@/modules/core/domain/userEmails/types' -import { getEmailVerificationFinalizationRoute } from '@/modules/core/helpers/routeHelper' -import { ServerInfo, UserRecord } from '@/modules/core/helpers/types' -import { EmailVerificationRequestError } from '@/modules/emails/errors' -import { getServerOrigin } from '@/modules/shared/helpers/envHelper' -import { - DeleteOldAndInsertNewVerification, - EmailTemplateParams, - RenderEmail, - RequestEmailVerification, - RequestNewEmailVerification, - SendEmail -} from '@/modules/emails/domain/operations' -import { GetUser } from '@/modules/core/domain/users/operations' -import { GetServerInfo } from '@/modules/core/domain/server/operations' - -const EMAIL_SUBJECT = 'Speckle Account E-mail Verification' - -type CreateNewVerificationDeps = { - getUser: GetUser - findPrimaryEmailForUser: FindPrimaryEmailForUser - getServerInfo: GetServerInfo - deleteOldAndInsertNewVerification: DeleteOldAndInsertNewVerification -} - -const createNewVerificationFactory = - (deps: CreateNewVerificationDeps) => - async (userId: string): Promise => { - if (!userId) - throw new EmailVerificationRequestError('User for verification not specified') - - const [user, email, serverInfo] = await Promise.all([ - deps.getUser(userId), - deps.findPrimaryEmailForUser({ userId }), - deps.getServerInfo() - ]) - - if (!user || !email) - throw new EmailVerificationRequestError( - 'Unable to resolve verification target user' - ) - - if (user.verified) - throw new EmailVerificationRequestError("User's email is already verified") - - const verificationId = await deps.deleteOldAndInsertNewVerification(user.email) - - return { - user, - email, - verificationId, - serverInfo - } - } - -type VerificationRequestContext = { - user: UserRecord - verificationId: string - serverInfo: ServerInfo - email: UserEmail -} - -type CreateNewEmailVerificationFactoryDeps = { - findEmail: FindEmail - getUser: GetUser - getServerInfo: GetServerInfo - deleteOldAndInsertNewVerification: DeleteOldAndInsertNewVerification -} - -const createNewEmailVerificationFactory = - (deps: CreateNewEmailVerificationFactoryDeps) => - async (emailId: string): Promise => { - const emailRecord = await deps.findEmail({ id: emailId }) - - if (!emailRecord) throw new EmailVerificationRequestError('Email not found') - - if (emailRecord.verified) - throw new EmailVerificationRequestError('Email is already verified') - - const [user, serverInfo] = await Promise.all([ - deps.getUser(emailRecord.userId), - deps.getServerInfo() - ]) - - if (!user) - throw new EmailVerificationRequestError( - 'Unable to resolve verification target user' - ) - - const verificationId = await deps.deleteOldAndInsertNewVerification( - emailRecord.email - ) - return { - user, - email: emailRecord, - verificationId, - serverInfo - } - } - -function buildMjmlBody() { - const bodyStart = `Hello,

You have just registered to the Speckle server, or initiated the email verification process manually. To finalize the verification process, click the button below:
` - const bodyEnd = `This link expires in 1 week.
- If the link does not work, please proceed by

- - Logging in with your e-mail address and password - Clicking on the Notification icon - Selecting "Send Verification" - Verifying your e-mail address by clicking on the link in the e-mail you will receive -
- - See you soon,
- Speckle -
- ` - - return { bodyStart, bodyEnd } -} - -function buildTextBody() { - const bodyStart = `Hello,\n\nYou have just registered to the Speckle server, or initiated the email verification process manually. To finalize the verification process, open the link below:` - const bodyEnd = `This link expires in 1 week. If the link does not work, please proceed by logging in to your Speckle account with your e-mail address and password, clicking the Notification icon, selecting "Send Verification" and verifying your e-mail address by clicking on the link in the e-mail you will receive.\n\nSee you soon,\nSpeckle - ` - - return { bodyStart, bodyEnd } -} - -function buildEmailLink(verificationId: string): string { - return new URL( - getEmailVerificationFinalizationRoute(verificationId), - getServerOrigin() - ).toString() -} - -function buildEmailTemplateParams(verificationId: string): EmailTemplateParams { - return { - mjml: buildMjmlBody(), - text: buildTextBody(), - cta: { - title: 'Verify your E-mail', - url: buildEmailLink(verificationId) - } - } -} - -type SendVerificationEmailDeps = { - sendEmail: SendEmail - renderEmail: RenderEmail -} - -const sendVerificationEmailFactory = - (deps: SendVerificationEmailDeps) => async (state: VerificationRequestContext) => { - const emailTemplateParams = buildEmailTemplateParams(state.verificationId) - const { html, text } = await deps.renderEmail( - emailTemplateParams, - state.serverInfo, - // im deliberately setting this to null, so that the email will not show the unsubscribe bit - null - ) - await deps.sendEmail({ - to: state.email.email, - subject: EMAIL_SUBJECT, - text, - html - }) - } - -/** - * Request email verification (send out verification message) for user with specified ID - */ -export const requestEmailVerificationFactory = - ( - deps: CreateNewVerificationDeps & SendVerificationEmailDeps - ): RequestEmailVerification => - async (userId) => { - const newVerificationState = await createNewVerificationFactory(deps)(userId) - await sendVerificationEmailFactory(deps)(newVerificationState) - } - -type RequestNewEmailVerificationDeps = CreateNewEmailVerificationFactoryDeps - -/** - * Request email verification for email with specified ID - */ -export const requestNewEmailVerificationFactory = - ( - deps: RequestNewEmailVerificationDeps & SendVerificationEmailDeps - ): RequestNewEmailVerification => - async (emailId) => { - const createNewEmailVerification = createNewEmailVerificationFactory(deps) - const newVerificationState = await createNewEmailVerification(emailId) - - await sendVerificationEmailFactory(deps)(newVerificationState) - } diff --git a/packages/shared/src/environment/index.ts b/packages/shared/src/environment/index.ts index cbe7619f2..75cd9cd04 100644 --- a/packages/shared/src/environment/index.ts +++ b/packages/shared/src/environment/index.ts @@ -51,11 +51,6 @@ const parseFeatureFlags = () => { schema: z.boolean(), defaults: { production: false, _: false } }, - // Forces email verification for all users - FF_FORCE_EMAIL_VERIFICATION: { - schema: z.boolean(), - defaults: { production: false, _: false } - }, // Forces onboarding for all users FF_FORCE_ONBOARDING: { schema: z.boolean(), @@ -94,7 +89,6 @@ export function getFeatureFlags(): { FF_BILLING_INTEGRATION_ENABLED: boolean FF_WORKSPACES_MULTI_REGION_ENABLED: boolean FF_FILEIMPORT_IFC_DOTNET_ENABLED: boolean - FF_FORCE_EMAIL_VERIFICATION: boolean FF_FORCE_ONBOARDING: boolean FF_OBJECTS_STREAMING_FIX: boolean FF_MOVE_PROJECT_REGION_ENABLED: boolean diff --git a/utils/helm/speckle-server/templates/_helpers.tpl b/utils/helm/speckle-server/templates/_helpers.tpl index 0e01eb278..e38a5cfba 100644 --- a/utils/helm/speckle-server/templates/_helpers.tpl +++ b/utils/helm/speckle-server/templates/_helpers.tpl @@ -589,9 +589,6 @@ Generate the environment variables for Speckle server and Speckle objects deploy - name: FF_WORKSPACES_MULTI_REGION_ENABLED value: {{ .Values.featureFlags.workspacesMultiRegionEnabled | quote }} -- name: FF_FORCE_EMAIL_VERIFICATION - value: {{ .Values.featureFlags.forceEmailVerification | quote }} - - name: FF_FORCE_ONBOARDING value: {{ .Values.featureFlags.forceOnboarding | quote }} diff --git a/utils/helm/speckle-server/templates/frontend_2/deployment.yml b/utils/helm/speckle-server/templates/frontend_2/deployment.yml index 0440c0dc2..dfbb7ca33 100644 --- a/utils/helm/speckle-server/templates/frontend_2/deployment.yml +++ b/utils/helm/speckle-server/templates/frontend_2/deployment.yml @@ -137,8 +137,6 @@ spec: value: {{ .Values.featureFlags.workspacesMultiRegionEnabled | quote }} - name: NUXT_PUBLIC_FF_GENDOAI_MODULE_ENABLED value: {{ .Values.featureFlags.gendoAIModuleEnabled | quote }} - - name: NUXT_PUBLIC_FF_FORCE_EMAIL_VERIFICATION - value: {{ .Values.featureFlags.forceEmailVerification | quote }} - name: NUXT_PUBLIC_FF_FORCE_ONBOARDING value: {{ .Values.featureFlags.forceOnboarding | quote }} {{- if .Values.analytics.survicate_workspace_key }}