lowercase user and invitation emails (#12130)

### Solution

> After discussion with charles & weiko, we chose the long term
solution.
> 
> Fix FE to request checkUserExists resolver with lowercased emails
> Add a decorator on User (and AppToken for invitation), to lowercase
email at user (appToken) creation. ⚠️ It works for TypeOrm .save method
only (there is no user email update in codebase, but in future it
could..)
> Add email lowercasing logic in external auth controller
> Fix FE to request sendInvitations resolver with lowercased emails
> Add migration command to lowercase all existing user emails and
invitation emails

> For other BE resolvers, we let them permissive. For example, if you
made a request on CheckUserExists resolver with uppercased email, you
will not found any user. We will not transform input before checking for
existence.

[link to comment
](https://github.com/twentyhq/twenty/pull/12130#discussion_r2098062093)

### Test 🚧 
- sign-in and up from main subdomain and workspace sub domain > Google
Auth (lowercased email) ✔️ | Microsoft Auth (uppercased email ✔️ &
lowercased email) | LoginPassword (uppercased email ✔️& lowercased
email✔️)
- invite flow with uppercased and lowercased ✔️
- migration command + sign-in ( former uppercased microsoft email ✔️) /
sign-up ( former uppercased invited email ✔️)

closes https://github.com/twentyhq/private-issues/issues/278, closes
https://github.com/twentyhq/private-issues/issues/275, closes
https://github.com/twentyhq/private-issues/issues/279
This commit is contained in:
Etienne
2025-05-21 11:06:29 +02:00
committed by GitHub
parent 6ff5a5bafa
commit 7461b7ac58
11 changed files with 161 additions and 11 deletions

View File

@ -1,7 +1,10 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { BeforeCreateOne, IDField } from '@ptc-org/nestjs-query-graphql';
import { isDefined } from 'twenty-shared/utils';
import {
BeforeInsert,
BeforeUpdate,
Column,
CreateDateColumn,
Entity,
@ -78,6 +81,14 @@ export class AppToken {
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
@BeforeInsert()
@BeforeUpdate()
formatEmail?() {
if (isDefined(this.context?.email)) {
this.context.email = this.context.email.toLowerCase();
}
}
@Column({ nullable: true, type: 'jsonb' })
context: { email: string } | null;
}

View File

@ -27,6 +27,7 @@ import {
import { GetAuthorizationUrlForSSOInput } from 'src/engine/core-modules/auth/dto/get-authorization-url-for-sso.input';
import { GetAuthorizationUrlForSSOOutput } from 'src/engine/core-modules/auth/dto/get-authorization-url-for-sso.output';
import { GetLoginTokenFromEmailVerificationTokenInput } from 'src/engine/core-modules/auth/dto/get-login-token-from-email-verification-token.input';
import { GetLoginTokenFromEmailVerificationTokenOutput } from 'src/engine/core-modules/auth/dto/get-login-token-from-email-verification-token.output';
import { SignUpOutput } from 'src/engine/core-modules/auth/dto/sign-up.output';
import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service';
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
@ -52,7 +53,6 @@ import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { GetLoginTokenFromEmailVerificationTokenOutput } from 'src/engine/core-modules/auth/dto/get-login-token-from-email-verification-token.output';
import { GetAuthTokensFromLoginTokenInput } from './dto/get-auth-tokens-from-login-token.input';
import { GetLoginTokenFromCredentialsInput } from './dto/get-login-token-from-credentials.input';
@ -92,7 +92,9 @@ export class AuthResolver {
async checkUserExists(
@Args() checkUserExistsInput: CheckUserExistsInput,
): Promise<typeof UserExistsOutput> {
return await this.authService.checkUserExists(checkUserExistsInput.email);
return await this.authService.checkUserExists(
checkUserExistsInput.email.toLowerCase(),
);
}
@Mutation(() => GetAuthorizationUrlForSSOOutput)

View File

@ -18,9 +18,9 @@ import { GoogleProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
import { GoogleRequest } from 'src/engine/core-modules/auth/strategies/google.auth.strategy';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
@Controller('auth/google')
@UseFilters(AuthRestApiExceptionFilter)
@ -48,7 +48,7 @@ export class GoogleAuthController {
const {
firstName,
lastName,
email,
email: rawEmail,
picture,
workspaceInviteHash,
workspaceId,
@ -56,6 +56,8 @@ export class GoogleAuthController {
locale,
} = req.user;
const email = rawEmail.toLowerCase();
const currentWorkspace = await this.authService.findWorkspaceForSignInUp({
workspaceId,
workspaceInviteHash,

View File

@ -17,9 +17,9 @@ import { MicrosoftProviderEnabledGuard } from 'src/engine/core-modules/auth/guar
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
import { MicrosoftRequest } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
@Controller('auth/microsoft')
@UseFilters(AuthRestApiExceptionFilter)
@ -49,7 +49,7 @@ export class MicrosoftAuthController {
const {
firstName,
lastName,
email,
email: rawEmail,
picture,
workspaceInviteHash,
workspaceId,
@ -57,6 +57,8 @@ export class MicrosoftAuthController {
locale,
} = req.user;
const email = rawEmail.toLowerCase();
const currentWorkspace = await this.authService.findWorkspaceForSignInUp({
workspaceId,
workspaceInviteHash,

View File

@ -2,6 +2,8 @@ import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import {
BeforeInsert,
BeforeUpdate,
Column,
CreateDateColumn,
DeleteDateColumn,
@ -45,6 +47,12 @@ export class User {
@Column({ default: '' })
lastName: string;
@BeforeInsert()
@BeforeUpdate()
formatEmail?() {
this.email = this.email.toLowerCase();
}
@Field()
@Column()
email: string;