Add Email Verification for non-Microsoft/Google Emails (#9288)
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):    ## Sent Email Details (Subject & Template):   ### Successful Email Verification Redirect:  ### Unsuccessful Email Verification (invalid token, invalid email, token expired, user does not exist, etc.):  ### Force Sign In When Email Not Verified:  # 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<void> { 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<void> { 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 <moreaux.antoine@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@ -0,0 +1,79 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { SubTitle } from '@/auth/components/SubTitle';
|
||||
import { Title } from '@/auth/components/Title';
|
||||
import { useHandleResendEmailVerificationToken } from '@/auth/sign-in-up/hooks/useHandleResendEmailVerificationToken';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { AnimatedEaseIn, IconMail, Loader, MainButton, RGBA } from 'twenty-ui';
|
||||
|
||||
const StyledMailContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border: 2px solid ${(props) => props.color};
|
||||
border-radius: ${({ theme }) => theme.border.radius.rounded};
|
||||
box-shadow: ${(props) =>
|
||||
props.color && `-4px 4px 0 -2px ${RGBA(props.color, 1)}`};
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledEmail = styled.span`
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
`;
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
margin-top: ${({ theme }) => theme.spacing(8)};
|
||||
`;
|
||||
|
||||
export const EmailVerificationSent = ({
|
||||
email,
|
||||
isError = false,
|
||||
}: {
|
||||
email: string | null;
|
||||
isError?: boolean;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const color =
|
||||
theme.name === 'light' ? theme.grayScale.gray90 : theme.grayScale.gray10;
|
||||
|
||||
const { handleResendEmailVerificationToken, loading: isLoading } =
|
||||
useHandleResendEmailVerificationToken();
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatedEaseIn>
|
||||
<StyledMailContainer color={color}>
|
||||
<IconMail color={color} size={24} stroke={3} />
|
||||
</StyledMailContainer>
|
||||
</AnimatedEaseIn>
|
||||
<Title animate>
|
||||
{isError ? 'Email Verification Failed' : 'Confirm Your Email Address'}
|
||||
</Title>
|
||||
<SubTitle>
|
||||
{isError ? (
|
||||
<>
|
||||
Oops! We encountered an issue verifying{' '}
|
||||
<StyledEmail>{email}</StyledEmail>. Please request a new
|
||||
verification email and try again.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
A verification email has been sent to{' '}
|
||||
<StyledEmail>{email}</StyledEmail>. Please check your inbox and
|
||||
click the link in the email to activate your account.
|
||||
</>
|
||||
)}
|
||||
</SubTitle>
|
||||
<StyledButtonContainer>
|
||||
<MainButton
|
||||
title="Click to resend"
|
||||
onClick={handleResendEmailVerificationToken(email)}
|
||||
Icon={() => (isLoading ? <Loader /> : undefined)}
|
||||
width={200}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user