From f722a2d61915811a43d694fad2be194bd9db745e Mon Sep 17 00:00:00 2001 From: Samyak Piya <76403666+samyakpiya@users.noreply.github.com> Date: Wed, 15 Jan 2025 12:43:40 -0500 Subject: [PATCH] Add Email Verification for non-Microsoft/Google Emails (#9288) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes twentyhq/twenty#8240 This PR introduces email verification for non-Microsoft/Google Emails: ## Email Verification SignInUp Flow: https://github.com/user-attachments/assets/740e9714-5413-4fd8-b02e-ace728ea47ef The email verification link is sent as part of the `SignInUpStep.EmailVerification`. The email verification token validation is handled on a separate page (`AppPath.VerifyEmail`). A verification email resend can be triggered from both pages. ## Email Verification Flow Screenshots (In Order): ![image](https://github.com/user-attachments/assets/d52237dc-fcc6-4754-a40f-b7d6294eebad) ![image](https://github.com/user-attachments/assets/263a4b6b-db49-406b-9e43-6c0f90488bb8) ![image](https://github.com/user-attachments/assets/0343ae51-32ef-48b8-8167-a96deb7db99e) ## Sent Email Details (Subject & Template): ![Screenshot 2025-01-05 at 11 56 56 PM](https://github.com/user-attachments/assets/475840d1-7d47-4792-b8c6-5c9ef5e02229) ![image](https://github.com/user-attachments/assets/a41b3b36-a36f-4a8e-b1f9-beeec7fe23e4) ### Successful Email Verification Redirect: ![image](https://github.com/user-attachments/assets/e2fad9e2-f4b1-485e-8f4a-32163c2718e7) ### Unsuccessful Email Verification (invalid token, invalid email, token expired, user does not exist, etc.): ![image](https://github.com/user-attachments/assets/92f4b65e-2971-4f26-a9fa-7aafadd2b305) ### Force Sign In When Email Not Verified: ![image](https://github.com/user-attachments/assets/86d0f188-cded-49a6-bde9-9630fd18d71e) # TODOs: ## Sign Up Process - [x] Introduce server-level environment variable IS_EMAIL_VERIFICATION_REQUIRED (defaults to false) - [x] Ensure users joining an existing workspace through an invite are not required to validate their email - [x] Generate an email verification token - [x] Store the token in appToken - [x] Send email containing the verification link - [x] Create new email template for email verification - [x] Create a frontend page to handle verification requests ## Sign In Process - [x] After verifying user credentials, check if user's email is verified and prompt to to verify - [x] Show an option to resend the verification email ## Database - [x] Rename the `emailVerified` colum on `user` to to `isEmailVerified` for consistency ## During Deployment - [x] Run a script/sql query to set `isEmailVerified` to `true` for all users with a Google/Microsoft email and all users that show an indication of a valid subscription (e.g. linked credit card) - I have created a draft migration file below that shows one possible approach to implementing this change: ```typescript import { MigrationInterface, QueryRunner } from 'typeorm'; export class UpdateEmailVerifiedForActiveUsers1733318043628 implements MigrationInterface { name = 'UpdateEmailVerifiedForActiveUsers1733318043628'; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` CREATE TABLE core."user_email_verified_backup" AS SELECT id, email, "isEmailVerified" FROM core."user" WHERE "deletedAt" IS NULL; `); await queryRunner.query(` -- Update isEmailVerified for users who have been part of workspaces with active subscriptions UPDATE core."user" u SET "isEmailVerified" = true WHERE EXISTS ( -- Check if user has been part of a workspace through userWorkspace table SELECT 1 FROM core."userWorkspace" uw JOIN core."workspace" w ON uw."workspaceId" = w.id WHERE uw."userId" = u.id -- Check for valid subscription indicators AND ( w."activationStatus" = 'ACTIVE' -- Add any other subscription-related conditions here ) ) AND u."deletedAt" IS NULL; `); } public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(` UPDATE core."user" u SET "isEmailVerified" = b."isEmailVerified" FROM core."user_email_verified_backup" b WHERE u.id = b.id; `); await queryRunner.query(`DROP TABLE core."user_email_verified_backup";`); } } ``` --------- Co-authored-by: Antoine Moreaux Co-authored-by: Félix Malfait --- .../twenty-emails/src/components/Footer.tsx | 55 ++++++ .../src/components/WhatIsTwenty.tsx | 35 +--- .../send-email-verification-link.email.tsx | 28 +++ packages/twenty-emails/src/index.ts | 1 + .../src/generated-metadata/graphql.ts | 23 ++- .../twenty-front/src/generated/graphql.tsx | 115 ++++++++++- ...sePageChangeEffectNavigateLocation.test.ts | 11 ++ .../usePageChangeEffectNavigateLocation.ts | 3 +- .../modules/app/hooks/useCreateAppRouter.tsx | 3 + .../modules/auth/components/VerifyEffect.tsx | 5 +- .../auth/components/VerifyEmailEffect.tsx | 68 +++++++ ...getLoginTokenFromEmailVerificationToken.ts | 17 ++ .../mutations/resendEmailVerificationToken.ts | 9 + .../auth/graphql/queries/checkUserExists.ts | 1 + .../auth/hooks/__tests__/useAuth.test.tsx | 7 +- .../src/modules/auth/hooks/useAuth.ts | 85 ++++++-- .../components/EmailVerificationSent.tsx | 79 ++++++++ .../useHandleResendEmailVerificationToken.ts | 47 +++++ .../modules/auth/states/signInUpStepState.ts | 1 + .../components/ClientConfigProviderEffect.tsx | 8 + .../graphql/queries/getClientConfig.ts | 1 + .../isEmailVerificationRequiredState.ts | 6 + .../twenty-front/src/modules/types/AppPath.ts | 1 + .../snack-bar-manager/components/SnackBar.tsx | 1 + .../snack-bar-manager/hooks/useSnackBar.ts | 12 +- .../hooks/__tests__/useShowAuthModal.test.tsx | 11 ++ .../ui/layout/hooks/useShowAuthModal.ts | 1 + .../twenty-front/src/pages/auth/SignInUp.tsx | 56 ++++-- .../src/testing/mock-data/config.ts | 1 + packages/twenty-server/.env.example | 2 + ...1736050161854-renameEmailVerifiedColumn.ts | 19 ++ .../app-token/app-token.entity.ts | 23 +-- .../core-modules/auth/auth.exception.ts | 1 + .../engine/core-modules/auth/auth.module.ts | 2 + .../core-modules/auth/auth.resolver.spec.ts | 12 +- .../engine/core-modules/auth/auth.resolver.ts | 85 ++++++-- ...ken-from-email-verification-token.input.ts | 16 ++ .../auth/dto/user-exists.entity.ts | 3 + .../auth-graphql-api-exception.filter.ts | 3 + .../auth/services/auth.service.spec.ts | 29 +-- .../auth/services/auth.service.ts | 58 ++---- .../auth/services/sign-in-up.service.spec.ts | 15 +- .../auth/services/sign-in-up.service.ts | 4 + .../email-verification-token.service.spec.ts | 187 ++++++++++++++++++ .../email-verification-token.service.ts | 103 ++++++++++ .../client-config/client-config.entity.ts | 3 + .../client-config/client-config.resolver.ts | 3 + .../service/domain-manager.service.ts | 16 ++ .../resend-email-verification-token.input.ts | 11 ++ .../resend-email-verification-token.output.ts | 10 + .../email-verification.exception.ts | 17 ++ .../email-verification.module.ts | 30 +++ .../email-verification.resolver.ts | 35 ++++ .../services/email-verification.service.ts | 131 ++++++++++++ .../environment/environment-variables.ts | 9 + .../graphql/utils/graphql-errors.util.ts | 9 + .../user-workspace/user-workspace.service.ts | 48 +++++ .../user/services/user.service.ts | 26 +++ .../engine/core-modules/user/user.entity.ts | 2 +- ...l-hydrate-request-from-token.middleware.ts | 2 + .../content/developers/self-hosting/setup.mdx | 26 +-- 61 files changed, 1460 insertions(+), 171 deletions(-) create mode 100644 packages/twenty-emails/src/components/Footer.tsx create mode 100644 packages/twenty-emails/src/emails/send-email-verification-link.email.tsx create mode 100644 packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx create mode 100644 packages/twenty-front/src/modules/auth/graphql/mutations/getLoginTokenFromEmailVerificationToken.ts create mode 100644 packages/twenty-front/src/modules/auth/graphql/mutations/resendEmailVerificationToken.ts create mode 100644 packages/twenty-front/src/modules/auth/sign-in-up/components/EmailVerificationSent.tsx create mode 100644 packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResendEmailVerificationToken.ts create mode 100644 packages/twenty-front/src/modules/client-config/states/isEmailVerificationRequiredState.ts create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1736050161854-renameEmailVerifiedColumn.ts create mode 100644 packages/twenty-server/src/engine/core-modules/auth/dto/get-login-token-from-email-verification-token.input.ts create mode 100644 packages/twenty-server/src/engine/core-modules/auth/token/services/email-verification-token.service.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/auth/token/services/email-verification-token.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/email-verification/dtos/resend-email-verification-token.input.ts create mode 100644 packages/twenty-server/src/engine/core-modules/email-verification/dtos/resend-email-verification-token.output.ts create mode 100644 packages/twenty-server/src/engine/core-modules/email-verification/email-verification.exception.ts create mode 100644 packages/twenty-server/src/engine/core-modules/email-verification/email-verification.module.ts create mode 100644 packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts create mode 100644 packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts diff --git a/packages/twenty-emails/src/components/Footer.tsx b/packages/twenty-emails/src/components/Footer.tsx new file mode 100644 index 000000000..919686fb8 --- /dev/null +++ b/packages/twenty-emails/src/components/Footer.tsx @@ -0,0 +1,55 @@ +import { Column, Row } from '@react-email/components'; +import { Link } from 'src/components/Link'; +import { ShadowText } from 'src/components/ShadowText'; + +export const Footer = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + Twenty.com Public Benefit Corporation +
+ 2261 Market Street #5275 +
+ San Francisco, CA 94114 +
+ + ); +}; diff --git a/packages/twenty-emails/src/components/WhatIsTwenty.tsx b/packages/twenty-emails/src/components/WhatIsTwenty.tsx index 9b3913f78..6b905a1f3 100644 --- a/packages/twenty-emails/src/components/WhatIsTwenty.tsx +++ b/packages/twenty-emails/src/components/WhatIsTwenty.tsx @@ -1,8 +1,7 @@ -import { Column, Row } from '@react-email/components'; -import { Link } from 'src/components/Link'; +import { Footer } from 'src/components/Footer'; import { MainText } from 'src/components/MainText'; -import { ShadowText } from 'src/components/ShadowText'; import { SubTitle } from 'src/components/SubTitle'; + export const WhatIsTwenty = () => { return ( <> @@ -11,35 +10,7 @@ export const WhatIsTwenty = () => { It's a CRM, a software to help businesses manage their customer data and relationships efficiently. - - - - - - - - - - - - - - - - - - - - - - - - Twenty.com Public Benefit Corporation -
- 2261 Market Street #5275 -
- San Francisco, CA 94114 -
+