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:
@ -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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 {}
|
||||
|
||||
Reference in New Issue
Block a user