From 9af2628264d9ccc9dd2d92c1f2f313e1b853f971 Mon Sep 17 00:00:00 2001 From: Antoine Moreaux Date: Fri, 28 Mar 2025 07:38:58 +0100 Subject: [PATCH] feat(auth): enhance email validation when no workspace available + disable captcha on email validation (#11239) Implemented fallback logic to associate a user with a workspace when none is found. Introduced new GraphQL types and mutations for roles and permissions management. Simplified and refactored URL-building logic for email verification, improving code maintainability and flexibility. --- .../engine/core-modules/auth/auth.resolver.ts | 22 ++++----- .../services/domain-manager.service.ts | 47 ++++++++++++------- .../email-verification.resolver.ts | 3 -- .../services/email-verification.service.ts | 24 ++++++---- .../user-workspace/user-workspace.service.ts | 16 +++++++ 5 files changed, 69 insertions(+), 43 deletions(-) diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts index 1ca91b2e5..51c0e1f01 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts @@ -155,31 +155,25 @@ export class AuthResolver { return { loginToken }; } - @UseGuards(CaptchaGuard) @Mutation(() => LoginToken) async getLoginTokenFromEmailVerificationToken( @Args() getLoginTokenFromEmailVerificationTokenInput: GetLoginTokenFromEmailVerificationTokenInput, @OriginHeader() origin: string, ) { - const workspace = - await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace( - origin, - ); - - workspaceValidator.assertIsDefinedOrThrow( - workspace, - new AuthException( - 'Workspace not found', - AuthExceptionCode.WORKSPACE_NOT_FOUND, - ), - ); - const user = await this.emailVerificationTokenService.validateEmailVerificationTokenOrThrow( getLoginTokenFromEmailVerificationTokenInput.emailVerificationToken, ); + const workspace = + (await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace( + origin, + )) ?? + (await this.userWorkspaceService.findFirstRandomWorkspaceByUserId( + user.id, + )); + await this.userService.markEmailAsVerified(user.id); const loginToken = await this.loginTokenService.generateLoginToken( diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.ts b/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.ts index c15ebc013..f27598af2 100644 --- a/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.ts +++ b/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.ts @@ -42,22 +42,37 @@ export class DomainManagerService { return baseUrl; } - buildEmailVerificationURL({ - emailVerificationToken, - email, - workspace, - }: { - emailVerificationToken: string; - email: string; - workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType; - }) { - return this.buildWorkspaceURL({ - workspace, - pathname: 'verify-email', - searchParams: { emailVerificationToken, email }, + private appendSearchParams( + url: URL, + searchParams: Record, + ) { + Object.entries(searchParams).forEach(([key, value]) => { + if (isDefined(value)) { + url.searchParams.set(key, value.toString()); + } }); } + buildBaseUrl({ + pathname, + searchParams, + }: { + pathname?: string; + searchParams?: Record; + }) { + const url = this.getBaseUrl(); + + if (pathname) { + url.pathname = pathname; + } + + if (searchParams) { + this.appendSearchParams(url, searchParams); + } + + return url; + } + buildWorkspaceURL({ workspace, pathname, @@ -76,11 +91,7 @@ export class DomainManagerService { } if (searchParams) { - Object.entries(searchParams).forEach(([key, value]) => { - if (isDefined(value)) { - url.searchParams.set(key, value.toString()); - } - }); + this.appendSearchParams(url, searchParams); } return url; diff --git a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts index ddac48673..4018e1892 100644 --- a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts @@ -7,7 +7,6 @@ import { ResendEmailVerificationTokenInput } from 'src/engine/core-modules/email import { ResendEmailVerificationTokenOutput } from 'src/engine/core-modules/email-verification/dtos/resend-email-verification-token.output'; import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service'; import { I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type'; -import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator'; @Resolver() @@ -29,8 +28,6 @@ export class EmailVerificationResolver { origin, ); - workspaceValidator.assertIsDefinedOrThrow(workspace); - return await this.emailVerificationService.resendEmailVerificationToken( resendEmailVerificationTokenInput.email, workspace, diff --git a/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts b/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts index 9afcad77b..5dfa00bcc 100644 --- a/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts +++ b/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts @@ -41,7 +41,9 @@ export class EmailVerificationService { async sendVerificationEmail( userId: string, email: string, - workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType, + workspace: + | WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType + | undefined, locale: keyof typeof APP_LOCALES, ) { if (!this.environmentService.get('IS_EMAIL_VERIFICATION_REQUIRED')) { @@ -51,12 +53,16 @@ export class EmailVerificationService { const { token: emailVerificationToken } = await this.emailVerificationTokenService.generateToken(userId, email); - const verificationLink = - this.domainManagerService.buildEmailVerificationURL({ - emailVerificationToken, - email, - workspace, - }); + const linkPathnameAndSearchParams = { + pathname: 'verify-email', + searchParams: { emailVerificationToken, email }, + }; + const verificationLink = workspace + ? this.domainManagerService.buildWorkspaceURL({ + workspace, + ...linkPathnameAndSearchParams, + }) + : this.domainManagerService.buildBaseUrl(linkPathnameAndSearchParams); const emailData = { link: verificationLink.toString(), @@ -88,7 +94,9 @@ export class EmailVerificationService { async resendEmailVerificationToken( email: string, - workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType, + workspace: + | WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType + | undefined, locale: keyof typeof APP_LOCALES, ) { if (!this.environmentService.get('IS_EMAIL_VERIFICATION_REQUIRED')) { diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts index 1e3670079..8b77f0d25 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts @@ -186,6 +186,22 @@ export class UserWorkspaceService extends TypeOrmQueryService { }); } + async findFirstRandomWorkspaceByUserId(userId: string) { + const user = await this.userRepository.findOne({ + where: { + id: userId, + }, + relations: ['workspaces', 'workspaces.workspace'], + }); + + userValidator.assertIsDefinedOrThrow( + user, + new AuthException('User not found', AuthExceptionCode.USER_NOT_FOUND), + ); + + return user.workspaces[0].workspace; + } + async findAvailableWorkspacesByEmail(email: string) { const user = await this.userRepository.findOne({ where: {