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>
81 lines
3.0 KiB
Plaintext
81 lines
3.0 KiB
Plaintext
# Use this for local setup
|
|
PG_DATABASE_URL=postgres://postgres:postgres@localhost:5432/default
|
|
REDIS_URL=redis://localhost:6379
|
|
|
|
APP_SECRET=replace_me_with_a_random_string
|
|
SIGN_IN_PREFILLED=true
|
|
|
|
ACCESS_TOKEN_SECRET=replace_me_with_a_random_string_access
|
|
|
|
FRONT_PROTOCOL=http
|
|
FRONT_DOMAIN=localhost
|
|
FRONT_PORT=3001
|
|
|
|
# ———————— Optional ————————
|
|
# PORT=3000
|
|
# DEBUG_MODE=true
|
|
# DEBUG_PORT=9000
|
|
# ACCESS_TOKEN_EXPIRES_IN=30m
|
|
# LOGIN_TOKEN_EXPIRES_IN=15m
|
|
# REFRESH_TOKEN_EXPIRES_IN=90d
|
|
# FILE_TOKEN_EXPIRES_IN=1d
|
|
# MESSAGING_PROVIDER_GMAIL_ENABLED=false
|
|
# CALENDAR_PROVIDER_GOOGLE_ENABLED=false
|
|
# IS_BILLING_ENABLED=false
|
|
# BILLING_PLAN_REQUIRED_LINK=https://twenty.com/stripe-redirection
|
|
# AUTH_PASSWORD_ENABLED=false
|
|
# IS_MULTIWORKSPACE_ENABLED=false
|
|
# AUTH_MICROSOFT_ENABLED=false
|
|
# AUTH_MICROSOFT_CLIENT_ID=replace_me_with_azure_client_id
|
|
# AUTH_MICROSOFT_TENANT_ID=replace_me_with_azure_tenant_id
|
|
# AUTH_MICROSOFT_CLIENT_SECRET=replace_me_with_azure_client_secret
|
|
# AUTH_MICROSOFT_CALLBACK_URL=http://localhost:3000/auth/microsoft/redirect
|
|
# AUTH_MICROSOFT_APIS_CALLBACK_URL=http://localhost:3000/auth/microsoft-apis/get-access-token
|
|
# AUTH_GOOGLE_ENABLED=false
|
|
# AUTH_GOOGLE_CLIENT_ID=replace_me_with_google_client_id
|
|
# AUTH_GOOGLE_CLIENT_SECRET=replace_me_with_google_client_secret
|
|
# AUTH_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/redirect
|
|
# AUTH_GOOGLE_APIS_CALLBACK_URL=http://localhost:3000/auth/google-apis/get-access-token
|
|
# SERVERLESS_TYPE=local
|
|
# STORAGE_TYPE=local
|
|
# STORAGE_LOCAL_PATH=.local-storage
|
|
# SUPPORT_DRIVER=front
|
|
# SUPPORT_FRONT_HMAC_KEY=replace_me_with_front_chat_verification_secret
|
|
# SUPPORT_FRONT_CHAT_ID=replace_me_with_front_chat_id
|
|
# LOGGER_DRIVER=console
|
|
# LOGGER_IS_BUFFER_ENABLED=true
|
|
# EXCEPTION_HANDLER_DRIVER=sentry
|
|
# SENTRY_ENVIRONMENT=main
|
|
# SENTRY_RELEASE=latest
|
|
# SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx
|
|
# SENTRY_FRONT_DSN=https://xxx@xxx.ingest.sentry.io/xxx
|
|
# LOG_LEVELS=error,warn
|
|
# MESSAGE_QUEUE_TYPE=bull-mq
|
|
# DEMO_WORKSPACE_IDS=REPLACE_ME_WITH_A_RANDOM_UUID
|
|
# SERVER_URL=http://localhost:3000
|
|
# WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION=30
|
|
# WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION=60
|
|
# Email Server Settings, see this doc for more info: https://docs.twenty.com/start/self-hosting/#email
|
|
# IS_EMAIL_VERIFICATION_REQUIRED=false
|
|
# EMAIL_VERIFICATION_TOKEN_EXPIRES_IN=1h
|
|
# EMAIL_FROM_ADDRESS=contact@yourdomain.com
|
|
# EMAIL_SYSTEM_ADDRESS=system@yourdomain.com
|
|
# EMAIL_FROM_NAME='John from YourDomain'
|
|
# EMAIL_DRIVER=logger
|
|
# EMAIL_SMTP_HOST=
|
|
# EMAIL_SMTP_PORT=
|
|
# EMAIL_SMTP_USER=
|
|
# EMAIL_SMTP_PASSWORD=
|
|
# PASSWORD_RESET_TOKEN_EXPIRES_IN=5m
|
|
# CAPTCHA_DRIVER=
|
|
# CAPTCHA_SITE_KEY=
|
|
# CAPTCHA_SECRET_KEY=
|
|
# API_RATE_LIMITING_TTL=
|
|
# API_RATE_LIMITING_LIMIT=
|
|
# MUTATION_MAXIMUM_AFFECTED_RECORDS=100
|
|
# CHROME_EXTENSION_ID=bggmipldbceihilonnbpgoeclgbkblkp
|
|
# PG_SSL_ALLOW_SELF_SIGNED=true
|
|
# SESSION_STORE_SECRET=replace_me_with_a_random_string_session
|
|
# ENTERPRISE_KEY=replace_me_with_a_valid_enterprise_key
|
|
# SSL_KEY_PATH="./certs/your-cert.key"
|
|
# SSL_CERT_PATH="./certs/your-cert.crt" |