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

@ -0,0 +1,113 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Raw, Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import {
AppToken,
AppTokenType,
} from 'src/engine/core-modules/app-token/app-token.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Command({
name: 'upgrade:0-54:lowercase-user-and-invitation-emails',
description: 'Lowercase user and invitation emails',
})
export class LowercaseUserAndInvitationEmailsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(User, 'core')
protected readonly userRepository: Repository<User>,
@InjectRepository(AppToken, 'core')
protected readonly appTokenRepository: Repository<AppToken>,
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
index,
total,
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
);
await this.lowercaseUserEmails(workspaceId, !!options.dryRun);
await this.lowercaseInvitationEmails(workspaceId, !!options.dryRun);
}
private async lowercaseUserEmails(workspaceId: string, dryRun: boolean) {
const users = await this.userRepository.find({
where: {
workspaces: {
workspaceId,
},
email: Raw((alias) => `LOWER(${alias}) != ${alias}`),
},
});
if (users.length === 0) return;
for (const user of users) {
if (!dryRun) {
await this.userRepository.update(
{
id: user.id,
},
{
email: user.email.toLowerCase(),
},
);
}
this.logger.log(
`Lowercased user email ${user.email} for workspace ${workspaceId}`,
);
}
}
private async lowercaseInvitationEmails(
workspaceId: string,
dryRun: boolean,
) {
const appTokens = await this.appTokenRepository.find({
where: {
workspaceId,
type: AppTokenType.InvitationToken,
context: Raw((_) => `LOWER(context->>'email') != context->>'email'`),
},
});
if (appTokens.length === 0) return;
for (const appToken of appTokens) {
if (!dryRun) {
await this.appTokenRepository.update(
{
id: appToken.id,
},
{
context: {
...appToken.context,
email: appToken.context?.email.toLowerCase(),
},
},
);
}
this.logger.log(
`Lowercased invitation email ${appToken.context?.email} for workspace ${workspaceId}`,
);
}
}
}

View File

@ -4,7 +4,10 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { CleanNotFoundFilesCommand } from 'src/database/commands/upgrade-version-command/0-54/0-54-clean-not-found-files.command';
import { FixCreatedByDefaultValueCommand } from 'src/database/commands/upgrade-version-command/0-54/0-54-created-by-default-value.command';
import { FixStandardSelectFieldsPositionCommand } from 'src/database/commands/upgrade-version-command/0-54/0-54-fix-standard-select-fields-position.command';
import { LowercaseUserAndInvitationEmailsCommand } from 'src/database/commands/upgrade-version-command/0-54/0-54-lowercase-user-and-invitation-emails.command';
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@ -14,7 +17,7 @@ import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/wor
@Module({
imports: [
TypeOrmModule.forFeature([Workspace], 'core'),
TypeOrmModule.forFeature([Workspace, AppToken, User], 'core'),
TypeOrmModule.forFeature(
[FieldMetadataEntity, ObjectMetadataEntity],
'metadata',
@ -28,11 +31,13 @@ import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/wor
FixStandardSelectFieldsPositionCommand,
FixCreatedByDefaultValueCommand,
CleanNotFoundFilesCommand,
LowercaseUserAndInvitationEmailsCommand,
],
exports: [
FixStandardSelectFieldsPositionCommand,
FixCreatedByDefaultValueCommand,
CleanNotFoundFilesCommand,
LowercaseUserAndInvitationEmailsCommand,
],
})
export class V0_54_UpgradeVersionCommandModule {}