diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json index 4e5a0087aa01..8ff02ed91af0 100644 --- a/packages/twenty-server/package.json +++ b/packages/twenty-server/package.json @@ -46,6 +46,7 @@ "monaco-editor": "^0.51.0", "monaco-editor-auto-typings": "^0.4.5", "openid-client": "^5.7.0", + "otplib": "^12.0.1", "passport": "^0.7.0", "psl": "^1.9.0", "redis": "^4.7.0", diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1737033794408-two-factor-method.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1737033794408-two-factor-method.ts new file mode 100644 index 000000000000..67043720995e --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1737033794408-two-factor-method.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class TwoFactorMethod1737033794408 implements MigrationInterface { + name = 'TwoFactorMethod1737033794408'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "core"."twoFactorMethod" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userWorkspaceId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_752f0250dd6824289ceddd8b054" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "core"."twoFactorMethod" ADD CONSTRAINT "FK_c1044145be65a4ee65c07e0a658" FOREIGN KEY ("userWorkspaceId") REFERENCES "core"."userWorkspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."twoFactorMethod" DROP CONSTRAINT "FK_c1044145be65a4ee65c07e0a658"`, + ); + await queryRunner.query(`DROP TABLE "core"."twoFactorMethod"`); + } +} diff --git a/packages/twenty-server/src/database/typeorm/typeorm.service.ts b/packages/twenty-server/src/database/typeorm/typeorm.service.ts index 025fbe2b0a0e..3a4ec3d0a6e7 100644 --- a/packages/twenty-server/src/database/typeorm/typeorm.service.ts +++ b/packages/twenty-server/src/database/typeorm/typeorm.service.ts @@ -15,6 +15,7 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature- import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity'; import { PostgresCredentials } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.entity'; import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; +import { TwoFactorMethod } from 'src/engine/core-modules/two-factor-method/two-factor-method.entity'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -47,6 +48,7 @@ export class TypeORMService implements OnModuleInit, OnModuleDestroy { BillingEntitlement, PostgresCredentials, WorkspaceSSOIdentityProvider, + TwoFactorMethod, ], metadataTableName: '_typeorm_generated_columns_and_materialized_views', ssl: environmentService.get('PG_SSL_ALLOW_SELF_SIGNED') diff --git a/packages/twenty-server/src/engine/core-modules/two-factor-method/two-factor-method.entity.ts b/packages/twenty-server/src/engine/core-modules/two-factor-method/two-factor-method.entity.ts new file mode 100644 index 000000000000..4536f859f9ed --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/two-factor-method/two-factor-method.entity.ts @@ -0,0 +1,49 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + Relation, + UpdateDateColumn, +} from 'typeorm'; + +import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; + +@Entity({ name: 'twoFactorMethod', schema: 'core' }) +@ObjectType('TwoFactorMethod') +export class TwoFactorMethod { + @Field() + @PrimaryGeneratedColumn('uuid') + id: string; + + @Field({ nullable: false }) + @Column({ nullable: false, type: 'uuid' }) + userWorkspaceId: string; + + @Field(() => UserWorkspace) + @ManyToOne( + () => UserWorkspace, + (userWorkspace) => userWorkspace.twoFactorMethods, + { + onDelete: 'CASCADE', + }, + ) + @JoinColumn({ name: 'userWorkspaceId' }) + userWorkspace: Relation; + + @Field() + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @Field() + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @Field({ nullable: true }) + @Column({ nullable: true, type: 'timestamptz' }) + deletedAt: Date; +} diff --git a/packages/twenty-server/src/engine/core-modules/two-factor-method/two-factor-method.module.ts b/packages/twenty-server/src/engine/core-modules/two-factor-method/two-factor-method.module.ts new file mode 100644 index 000000000000..f0066d02c595 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/two-factor-method/two-factor-method.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; + +import { TwoFactorMethod } from './two-factor-method.entity'; +import { TwoFactorMethodService } from './two-factor-method.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([TwoFactorMethod, UserWorkspace], 'core')], + providers: [TwoFactorMethodService], + exports: [TwoFactorMethodService], +}) +export class TwoFactorMethodModule {} diff --git a/packages/twenty-server/src/engine/core-modules/two-factor-method/two-factor-method.service.ts b/packages/twenty-server/src/engine/core-modules/two-factor-method/two-factor-method.service.ts new file mode 100644 index 000000000000..01a446f2902d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/two-factor-method/two-factor-method.service.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { TwoFactorMethod } from './two-factor-method.entity'; + +@Injectable() +export class TwoFactorMethodService { + constructor( + @InjectRepository(TwoFactorMethod) + private readonly twoFactorMethodRepository: Repository, + ) {} + + async createTwoFactorMethod( + userWorkspaceId: string, + ): Promise { + const twoFactorMethod = this.twoFactorMethodRepository.create({ + userWorkspace: { id: userWorkspaceId }, + }); + + return this.twoFactorMethodRepository.save(twoFactorMethod); + } + + async findAll(): Promise { + return this.twoFactorMethodRepository.find(); + } + + async findOne(id: string): Promise { + return this.twoFactorMethodRepository.findOne({ where: { id } }); + } + + async remove(id: string): Promise { + await this.twoFactorMethodRepository.delete(id); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts index 0004ad405740..0d8a4ac2bb5d 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts @@ -7,15 +7,17 @@ import { Entity, JoinColumn, ManyToOne, + OneToMany, PrimaryGeneratedColumn, Relation, Unique, UpdateDateColumn, } from 'typeorm'; +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { TwoFactorMethod } from 'src/engine/core-modules/two-factor-method/two-factor-method.entity'; import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; @Entity({ name: 'userWorkspace', schema: 'core' }) @ObjectType('UserWorkspace') @@ -58,4 +60,10 @@ export class UserWorkspace { @Field({ nullable: true }) @Column({ nullable: true, type: 'timestamptz' }) deletedAt: Date; + + @OneToMany( + () => TwoFactorMethod, + (twoFactorMethod) => twoFactorMethod.userWorkspace, + ) + twoFactorMethods: Relation; } diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts index fd47972e1011..56edb5e7fd9a 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts @@ -4,22 +4,23 @@ import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql'; import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; +import { TwoFactorMethod } from 'src/engine/core-modules/two-factor-method/two-factor-method.entity'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; +import { UserWorkspaceResolver } from 'src/engine/core-modules/user-workspace/user-workspace.resolver'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; -import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; -import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { User } from 'src/engine/core-modules/user/user.entity'; import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { UserWorkspaceResolver } from 'src/engine/core-modules/user-workspace/user-workspace.resolver'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; @Module({ imports: [ NestjsQueryGraphQLModule.forFeature({ imports: [ NestjsQueryTypeOrmModule.forFeature( - [User, UserWorkspace, Workspace], + [User, UserWorkspace, Workspace, TwoFactorMethod], 'core', ), NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'), diff --git a/yarn.lock b/yarn.lock index db22733ffedb..9fe620698750 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9847,6 +9847,54 @@ __metadata: languageName: node linkType: hard +"@otplib/core@npm:^12.0.1": + version: 12.0.1 + resolution: "@otplib/core@npm:12.0.1" + checksum: 10c0/efe703d92d50cf11b39e47fc6ccd10c01d8bfbd9423724e88aecf2470f740562b2422c10b779e0b7d1d29c09f5d3d00de69200f04c8250e2adeb13a15e5dee7f + languageName: node + linkType: hard + +"@otplib/plugin-crypto@npm:^12.0.1": + version: 12.0.1 + resolution: "@otplib/plugin-crypto@npm:12.0.1" + dependencies: + "@otplib/core": "npm:^12.0.1" + checksum: 10c0/7ad0cda9c643411a13a118bcf0d031ef61a9031f794a21b549fb3f1e38334f83c5481a9f706eb5b25066325759c1a598fbc1f9c05c4620dff5addccbe2ad23ad + languageName: node + linkType: hard + +"@otplib/plugin-thirty-two@npm:^12.0.1": + version: 12.0.1 + resolution: "@otplib/plugin-thirty-two@npm:12.0.1" + dependencies: + "@otplib/core": "npm:^12.0.1" + thirty-two: "npm:^1.0.2" + checksum: 10c0/971a6f592c0f33cb258dcb750f6f0c058cbfd45e0212ea6f29d0c75dc6c5fcb67dbc8f19c3e8ae710e93292319b8d3d15fd7281714deae9fadaab794f9d44544 + languageName: node + linkType: hard + +"@otplib/preset-default@npm:^12.0.1": + version: 12.0.1 + resolution: "@otplib/preset-default@npm:12.0.1" + dependencies: + "@otplib/core": "npm:^12.0.1" + "@otplib/plugin-crypto": "npm:^12.0.1" + "@otplib/plugin-thirty-two": "npm:^12.0.1" + checksum: 10c0/cd683861b96d04b7581534169a5a130fc049bd3188a850a1642ba4cf2a53866dedafe5201763697e38a3f0726103783baf338448153752c464fff797948dc01c + languageName: node + linkType: hard + +"@otplib/preset-v11@npm:^12.0.1": + version: 12.0.1 + resolution: "@otplib/preset-v11@npm:12.0.1" + dependencies: + "@otplib/core": "npm:^12.0.1" + "@otplib/plugin-crypto": "npm:^12.0.1" + "@otplib/plugin-thirty-two": "npm:^12.0.1" + checksum: 10c0/5a1bf8198f169182e267ab6ae3115f2441190f34a212e8232ff4b9c2c8a7253fddf3aa185ab9a6246487e9c1052b676395e80b6e0b2825fa218eb8e29bd7b14b + languageName: node + linkType: hard + "@parcel/watcher-android-arm64@npm:2.4.1": version: 2.4.1 resolution: "@parcel/watcher-android-arm64@npm:2.4.1" @@ -38084,6 +38132,17 @@ __metadata: languageName: node linkType: hard +"otplib@npm:^12.0.1": + version: 12.0.1 + resolution: "otplib@npm:12.0.1" + dependencies: + "@otplib/core": "npm:^12.0.1" + "@otplib/preset-default": "npm:^12.0.1" + "@otplib/preset-v11": "npm:^12.0.1" + checksum: 10c0/5a6cd332ed38f9fb6915407775bb9b4bf298d4d32c7c5691291add913dc1788064ab4ca353315f8add4ea704af2cb2c970d2f2d354725c6daa0c945cd5d09811 + languageName: node + linkType: hard + "outvariant@npm:1.4.0": version: 1.4.0 resolution: "outvariant@npm:1.4.0" @@ -44756,6 +44815,13 @@ __metadata: languageName: node linkType: hard +"thirty-two@npm:^1.0.2": + version: 1.0.2 + resolution: "thirty-two@npm:1.0.2" + checksum: 10c0/a6f8c69a153a1aa4d6948eabe004f5a38e45cb869d7d6c535df7460fc44c95c596a60efc56cce56b5e4f0988601db1298be5d1b6ae3fe12dcdeb461c9aeadd17 + languageName: node + linkType: hard + "throttle-debounce@npm:^3.0.1": version: 3.0.1 resolution: "throttle-debounce@npm:3.0.1" @@ -45668,6 +45734,7 @@ __metadata: monaco-editor: "npm:^0.51.0" monaco-editor-auto-typings: "npm:^0.4.5" openid-client: "npm:^5.7.0" + otplib: "npm:^12.0.1" passport: "npm:^0.7.0" psl: "npm:^1.9.0" redis: "npm:^4.7.0"