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:
@ -82,7 +82,7 @@ export const SignInUpGlobalScopeForm = () => {
|
|||||||
const token = await readCaptchaToken();
|
const token = await readCaptchaToken();
|
||||||
await checkUserExists.checkUserExistsQuery({
|
await checkUserExists.checkUserExistsQuery({
|
||||||
variables: {
|
variables: {
|
||||||
email: form.getValues('email'),
|
email: form.getValues('email').toLowerCase().trim(),
|
||||||
captchaToken: token,
|
captchaToken: token,
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
|||||||
@ -9,10 +9,10 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
|||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
import { sanitizeEmailList } from '@/workspace/utils/sanitizeEmailList';
|
import { sanitizeEmailList } from '@/workspace/utils/sanitizeEmailList';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { useCreateWorkspaceInvitation } from '../../workspace-invitation/hooks/useCreateWorkspaceInvitation';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { Button } from 'twenty-ui/input';
|
|
||||||
import { IconSend } from 'twenty-ui/display';
|
import { IconSend } from 'twenty-ui/display';
|
||||||
|
import { Button } from 'twenty-ui/input';
|
||||||
|
import { useCreateWorkspaceInvitation } from '../../workspace-invitation/hooks/useCreateWorkspaceInvitation';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -21,4 +21,11 @@ describe('sanitizeEmailList', () => {
|
|||||||
'toto@toto.com',
|
'toto@toto.com',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should lowercase emails', () => {
|
||||||
|
expect(sanitizeEmailList(['TOTO@toto.com', 'TOTO2@toto.com'])).toEqual([
|
||||||
|
'toto@toto.com',
|
||||||
|
'toto2@toto.com',
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,7 +2,7 @@ export const sanitizeEmailList = (emailList: string[]): string[] => {
|
|||||||
return Array.from(
|
return Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
emailList
|
emailList
|
||||||
.map((email) => email.trim())
|
.map((email) => email.trim().toLowerCase())
|
||||||
.filter((email) => email.length > 0),
|
.filter((email) => email.length > 0),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 { 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 { 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 { 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 { 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 { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.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';
|
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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Workspace], 'core'),
|
TypeOrmModule.forFeature([Workspace, AppToken, User], 'core'),
|
||||||
TypeOrmModule.forFeature(
|
TypeOrmModule.forFeature(
|
||||||
[FieldMetadataEntity, ObjectMetadataEntity],
|
[FieldMetadataEntity, ObjectMetadataEntity],
|
||||||
'metadata',
|
'metadata',
|
||||||
@ -28,11 +31,13 @@ import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/wor
|
|||||||
FixStandardSelectFieldsPositionCommand,
|
FixStandardSelectFieldsPositionCommand,
|
||||||
FixCreatedByDefaultValueCommand,
|
FixCreatedByDefaultValueCommand,
|
||||||
CleanNotFoundFilesCommand,
|
CleanNotFoundFilesCommand,
|
||||||
|
LowercaseUserAndInvitationEmailsCommand,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
FixStandardSelectFieldsPositionCommand,
|
FixStandardSelectFieldsPositionCommand,
|
||||||
FixCreatedByDefaultValueCommand,
|
FixCreatedByDefaultValueCommand,
|
||||||
CleanNotFoundFilesCommand,
|
CleanNotFoundFilesCommand,
|
||||||
|
LowercaseUserAndInvitationEmailsCommand,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class V0_54_UpgradeVersionCommandModule {}
|
export class V0_54_UpgradeVersionCommandModule {}
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import { Field, ObjectType } from '@nestjs/graphql';
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
|
||||||
import { BeforeCreateOne, IDField } from '@ptc-org/nestjs-query-graphql';
|
import { BeforeCreateOne, IDField } from '@ptc-org/nestjs-query-graphql';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import {
|
import {
|
||||||
|
BeforeInsert,
|
||||||
|
BeforeUpdate,
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
@ -78,6 +81,14 @@ export class AppToken {
|
|||||||
@UpdateDateColumn({ type: 'timestamptz' })
|
@UpdateDateColumn({ type: 'timestamptz' })
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
||||||
|
@BeforeInsert()
|
||||||
|
@BeforeUpdate()
|
||||||
|
formatEmail?() {
|
||||||
|
if (isDefined(this.context?.email)) {
|
||||||
|
this.context.email = this.context.email.toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Column({ nullable: true, type: 'jsonb' })
|
@Column({ nullable: true, type: 'jsonb' })
|
||||||
context: { email: string } | null;
|
context: { email: string } | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import {
|
|||||||
import { GetAuthorizationUrlForSSOInput } from 'src/engine/core-modules/auth/dto/get-authorization-url-for-sso.input';
|
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 { 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 { 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 { SignUpOutput } from 'src/engine/core-modules/auth/dto/sign-up.output';
|
||||||
import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service';
|
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';
|
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 { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||||
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
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 { 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 { GetAuthTokensFromLoginTokenInput } from './dto/get-auth-tokens-from-login-token.input';
|
||||||
import { GetLoginTokenFromCredentialsInput } from './dto/get-login-token-from-credentials.input';
|
import { GetLoginTokenFromCredentialsInput } from './dto/get-login-token-from-credentials.input';
|
||||||
@ -92,7 +92,9 @@ export class AuthResolver {
|
|||||||
async checkUserExists(
|
async checkUserExists(
|
||||||
@Args() checkUserExistsInput: CheckUserExistsInput,
|
@Args() checkUserExistsInput: CheckUserExistsInput,
|
||||||
): Promise<typeof UserExistsOutput> {
|
): Promise<typeof UserExistsOutput> {
|
||||||
return await this.authService.checkUserExists(checkUserExistsInput.email);
|
return await this.authService.checkUserExists(
|
||||||
|
checkUserExistsInput.email.toLowerCase(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => GetAuthorizationUrlForSSOOutput)
|
@Mutation(() => GetAuthorizationUrlForSSOOutput)
|
||||||
|
|||||||
@ -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 { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
|
||||||
import { GoogleRequest } from 'src/engine/core-modules/auth/strategies/google.auth.strategy';
|
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 { 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 { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
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')
|
@Controller('auth/google')
|
||||||
@UseFilters(AuthRestApiExceptionFilter)
|
@UseFilters(AuthRestApiExceptionFilter)
|
||||||
@ -48,7 +48,7 @@ export class GoogleAuthController {
|
|||||||
const {
|
const {
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
email,
|
email: rawEmail,
|
||||||
picture,
|
picture,
|
||||||
workspaceInviteHash,
|
workspaceInviteHash,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@ -56,6 +56,8 @@ export class GoogleAuthController {
|
|||||||
locale,
|
locale,
|
||||||
} = req.user;
|
} = req.user;
|
||||||
|
|
||||||
|
const email = rawEmail.toLowerCase();
|
||||||
|
|
||||||
const currentWorkspace = await this.authService.findWorkspaceForSignInUp({
|
const currentWorkspace = await this.authService.findWorkspaceForSignInUp({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
workspaceInviteHash,
|
workspaceInviteHash,
|
||||||
|
|||||||
@ -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 { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
|
||||||
import { MicrosoftRequest } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy';
|
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 { 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 { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
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')
|
@Controller('auth/microsoft')
|
||||||
@UseFilters(AuthRestApiExceptionFilter)
|
@UseFilters(AuthRestApiExceptionFilter)
|
||||||
@ -49,7 +49,7 @@ export class MicrosoftAuthController {
|
|||||||
const {
|
const {
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
email,
|
email: rawEmail,
|
||||||
picture,
|
picture,
|
||||||
workspaceInviteHash,
|
workspaceInviteHash,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@ -57,6 +57,8 @@ export class MicrosoftAuthController {
|
|||||||
locale,
|
locale,
|
||||||
} = req.user;
|
} = req.user;
|
||||||
|
|
||||||
|
const email = rawEmail.toLowerCase();
|
||||||
|
|
||||||
const currentWorkspace = await this.authService.findWorkspaceForSignInUp({
|
const currentWorkspace = await this.authService.findWorkspaceForSignInUp({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
workspaceInviteHash,
|
workspaceInviteHash,
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
|||||||
|
|
||||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||||
import {
|
import {
|
||||||
|
BeforeInsert,
|
||||||
|
BeforeUpdate,
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
DeleteDateColumn,
|
DeleteDateColumn,
|
||||||
@ -45,6 +47,12 @@ export class User {
|
|||||||
@Column({ default: '' })
|
@Column({ default: '' })
|
||||||
lastName: string;
|
lastName: string;
|
||||||
|
|
||||||
|
@BeforeInsert()
|
||||||
|
@BeforeUpdate()
|
||||||
|
formatEmail?() {
|
||||||
|
this.email = this.email.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
@Field()
|
@Field()
|
||||||
@Column()
|
@Column()
|
||||||
email: string;
|
email: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user