From 13a3dc4dfb079ca83487781f54153353c715cb36 Mon Sep 17 00:00:00 2001 From: aditya pimpalkar Date: Mon, 26 Feb 2024 22:07:47 +0000 Subject: [PATCH 01/20] Seed UserWorkspace for existing demo/dev users --- .../database/typeorm-seeds/core/demo/index.ts | 6 +++ .../typeorm-seeds/core/demo/userWorkspaces.ts | 51 ++++++++++++++++++ .../src/database/typeorm-seeds/core/index.ts | 6 +++ .../typeorm-seeds/core/userWorkspaces.ts | 53 +++++++++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 packages/twenty-server/src/database/typeorm-seeds/core/demo/userWorkspaces.ts create mode 100644 packages/twenty-server/src/database/typeorm-seeds/core/userWorkspaces.ts diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/demo/index.ts b/packages/twenty-server/src/database/typeorm-seeds/core/demo/index.ts index 9494324e017a..1a3a12a06aa6 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/core/demo/index.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/core/demo/index.ts @@ -12,6 +12,10 @@ import { seedFeatureFlags, deleteFeatureFlags, } from 'src/database/typeorm-seeds/core/demo/feature-flags'; +import { + deleteUserWorkspaces, + seedUserWorkspaces, +} from 'src/database/typeorm-seeds/core/demo/userWorkspaces'; export const seedCoreSchema = async ( workspaceDataSource: DataSource, @@ -21,6 +25,7 @@ export const seedCoreSchema = async ( await seedWorkspaces(workspaceDataSource, schemaName, workspaceId); await seedUsers(workspaceDataSource, schemaName, workspaceId); + await seedUserWorkspaces(workspaceDataSource, schemaName, workspaceId); await seedFeatureFlags(workspaceDataSource, schemaName, workspaceId); }; @@ -30,6 +35,7 @@ export const deleteCoreSchema = async ( ) => { const schemaName = 'core'; + await deleteUserWorkspaces(workspaceDataSource, schemaName, workspaceId); await deleteUsersByWorkspace(workspaceDataSource, schemaName, workspaceId); await deleteFeatureFlags(workspaceDataSource, schemaName, workspaceId); // deleteWorkspaces should be last diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/demo/userWorkspaces.ts b/packages/twenty-server/src/database/typeorm-seeds/core/demo/userWorkspaces.ts new file mode 100644 index 000000000000..f6e52e17525a --- /dev/null +++ b/packages/twenty-server/src/database/typeorm-seeds/core/demo/userWorkspaces.ts @@ -0,0 +1,51 @@ +import { DataSource } from 'typeorm'; + +const tableName = 'userWorkspace'; + +export enum DemoSeedUserIds { + Noah = '20202020-9e3b-46d4-a556-88b9ddc2b035', + Hugo = '20202020-3957-4908-9c36-2929a23f8358', + Julia = '20202020-7169-42cf-bc47-1cfef15264b9', +} + +export const seedUserWorkspaces = async ( + workspaceDataSource: DataSource, + schemaName: string, + workspaceId: string, +) => { + await workspaceDataSource + .createQueryBuilder() + .insert() + .into(`${schemaName}.${tableName}`, ['userId', 'workspaceId']) + .orIgnore() + .values([ + { + userId: DemoSeedUserIds.Noah, + workspaceId: workspaceId, + }, + { + userId: DemoSeedUserIds.Hugo, + workspaceId: workspaceId, + }, + { + userId: DemoSeedUserIds.Julia, + workspaceId: workspaceId, + }, + ]) + .execute(); +}; + +export const deleteUserWorkspaces = async ( + workspaceDataSource: DataSource, + schemaName: string, + workspaceId: string, +) => { + await workspaceDataSource + .createQueryBuilder() + .delete() + .from(`${schemaName}.${tableName}`) + .where(`"${tableName}"."workspaceId" = :workspaceId`, { + workspaceId, + }) + .execute(); +}; diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/index.ts b/packages/twenty-server/src/database/typeorm-seeds/core/index.ts index b33dce403da6..6e5c9921e68d 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/core/index.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/core/index.ts @@ -12,6 +12,10 @@ import { seedFeatureFlags, deleteFeatureFlags, } from 'src/database/typeorm-seeds/core/feature-flags'; +import { + deleteUserWorkspaces, + seedUserWorkspaces, +} from 'src/database/typeorm-seeds/core/userWorkspaces'; export const seedCoreSchema = async ( workspaceDataSource: DataSource, @@ -21,6 +25,7 @@ export const seedCoreSchema = async ( await seedWorkspaces(workspaceDataSource, schemaName, workspaceId); await seedUsers(workspaceDataSource, schemaName, workspaceId); + await seedUserWorkspaces(workspaceDataSource, schemaName, workspaceId); await seedFeatureFlags(workspaceDataSource, schemaName, workspaceId); }; @@ -30,6 +35,7 @@ export const deleteCoreSchema = async ( ) => { const schemaName = 'core'; + await deleteUserWorkspaces(workspaceDataSource, schemaName, workspaceId); await deleteUsersByWorkspace(workspaceDataSource, schemaName, workspaceId); await deleteFeatureFlags(workspaceDataSource, schemaName, workspaceId); // deleteWorkspaces should be last diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/userWorkspaces.ts b/packages/twenty-server/src/database/typeorm-seeds/core/userWorkspaces.ts new file mode 100644 index 000000000000..39b2771825d9 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm-seeds/core/userWorkspaces.ts @@ -0,0 +1,53 @@ +import { DataSource } from 'typeorm'; + +// import { SeedWorkspaceId } from 'src/database/typeorm-seeds/core/workspaces'; + +const tableName = 'userWorkspace'; + +export enum SeedUserIds { + Tim = '20202020-9e3b-46d4-a556-88b9ddc2b034', + Jony = '20202020-3957-4908-9c36-2929a23f8357', + Phil = '20202020-7169-42cf-bc47-1cfef15264b8', +} + +export const seedUserWorkspaces = async ( + workspaceDataSource: DataSource, + schemaName: string, + workspaceId: string, +) => { + await workspaceDataSource + .createQueryBuilder() + .insert() + .into(`${schemaName}.${tableName}`, ['userId', 'workspaceId']) + .orIgnore() + .values([ + { + userId: SeedUserIds.Tim, + workspaceId, + }, + { + userId: SeedUserIds.Jony, + workspaceId, + }, + { + userId: SeedUserIds.Phil, + workspaceId, + }, + ]) + .execute(); +}; + +export const deleteUserWorkspaces = async ( + workspaceDataSource: DataSource, + schemaName: string, + workspaceId: string, +) => { + await workspaceDataSource + .createQueryBuilder() + .delete() + .from(`${schemaName}.${tableName}`) + .where(`"${tableName}"."workspaceId" = :workspaceId`, { + workspaceId, + }) + .execute(); +}; From b83fd794c85838b0844ea39b414f0f19d85b35b4 Mon Sep 17 00:00:00 2001 From: aditya pimpalkar Date: Mon, 26 Feb 2024 22:11:34 +0000 Subject: [PATCH 02/20] add workspaces field to currentUser --- .../twenty-front/src/generated/graphql.tsx | 85 +++++++++++++------ .../modules/users/components/UserProvider.tsx | 8 +- .../users/graphql/queries/getCurrentUser.ts | 63 ++++++++------ .../user-workspace/user-workspace.entity.ts | 8 ++ .../user-workspace/user-workspace.service.ts | 1 + .../src/core/user/dtos/current-user.dto.ts | 13 +++ .../src/core/user/user.resolver.ts | 12 ++- 7 files changed, 129 insertions(+), 61 deletions(-) create mode 100644 packages/twenty-server/src/core/user/dtos/current-user.dto.ts diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 1973079ae0ed..2788156f190f 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -81,6 +81,12 @@ export type ClientConfig = { telemetry: Telemetry; }; +export type CurrentUser = { + __typename?: 'CurrentUser'; + user: User; + workspaces: Array; +}; + export type CursorPaging = { /** Paginate after opaque cursor */ after?: InputMaybe; @@ -367,7 +373,7 @@ export type Query = { checkUserExists: UserExists; checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid; clientConfig: ClientConfig; - currentUser: User; + currentUser: CurrentUser; currentWorkspace: Workspace; findWorkspaceFromInviteHash: Workspace; getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal; @@ -563,6 +569,18 @@ export type UserExists = { exists: Scalars['Boolean']; }; +export type UserWorkspace = { + __typename?: 'UserWorkspace'; + createdAt: Scalars['DateTime']; + deletedAt: Scalars['DateTime']; + id: Scalars['ID']; + updatedAt: Scalars['DateTime']; + user: User; + userId: Scalars['String']; + workspace: Workspace; + workspaceId: Scalars['String']; +}; + export type ValidatePasswordResetToken = { __typename?: 'ValidatePasswordResetToken'; email: Scalars['String']; @@ -868,7 +886,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; -export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } } }; +export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'CurrentUser', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } }, workspaces: Array<{ __typename?: 'UserWorkspace', userId: string, workspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null } }> } }; export type ActivateWorkspaceMutationVariables = Exact<{ input: ActivateWorkspaceInput; @@ -1699,36 +1717,47 @@ export type UploadProfilePictureMutationOptions = Apollo.BaseMutationOptions { setIsLoading(false); } if (queryData?.currentUser) { - setCurrentUser(queryData.currentUser); - setCurrentWorkspace(queryData.currentUser.defaultWorkspace); + setCurrentUser(queryData.currentUser.user); + setCurrentWorkspace(queryData.currentUser.user.defaultWorkspace); } - if (queryData?.currentUser?.workspaceMember) { - const workspaceMember = queryData.currentUser.workspaceMember; + if (queryData?.currentUser?.user.workspaceMember) { + const workspaceMember = queryData.currentUser.user.workspaceMember; setCurrentWorkspaceMember({ ...workspaceMember, colorScheme: (workspaceMember.colorScheme as ColorScheme) ?? 'Light', diff --git a/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUser.ts b/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUser.ts index b7b10ce7892f..5c0bc90f513c 100644 --- a/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUser.ts +++ b/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUser.ts @@ -4,36 +4,47 @@ import { gql } from '@apollo/client'; export const GET_CURRENT_USER = gql` query GetCurrentUser { currentUser { - id - firstName - lastName - email - canImpersonate - supportUserHash - workspaceMember { + user { id - name { - firstName - lastName + firstName + lastName + email + canImpersonate + supportUserHash + workspaceMember { + id + name { + firstName + lastName + } + colorScheme + avatarUrl + locale + } + defaultWorkspace { + id + displayName + logo + domainName + inviteHash + allowImpersonation + subscriptionStatus + activationStatus + featureFlags { + id + key + value + workspaceId + } } - colorScheme - avatarUrl - locale } - defaultWorkspace { - id - displayName - logo - domainName - inviteHash - allowImpersonation - subscriptionStatus - activationStatus - featureFlags { + workspaces { + userId + workspace { id - key - value - workspaceId + displayName + logo + domainName } } } diff --git a/packages/twenty-server/src/core/user-workspace/user-workspace.entity.ts b/packages/twenty-server/src/core/user-workspace/user-workspace.entity.ts index adb1e0e06545..3b78b2dabab8 100644 --- a/packages/twenty-server/src/core/user-workspace/user-workspace.entity.ts +++ b/packages/twenty-server/src/core/user-workspace/user-workspace.entity.ts @@ -6,6 +6,7 @@ import { CreateDateColumn, Entity, JoinColumn, + ManyToOne, PrimaryGeneratedColumn, Unique, UpdateDateColumn, @@ -22,15 +23,22 @@ export class UserWorkspace { @PrimaryGeneratedColumn('uuid') id: string; + @Field() @JoinColumn({ name: 'userId' }) user: User; + @Field({ nullable: false }) @Column() userId: string; + @Field(() => Workspace) + @ManyToOne(() => Workspace, (workspace) => workspace.users, { + onDelete: 'SET NULL', + }) @JoinColumn({ name: 'workspaceId' }) workspace: Workspace; + @Field({ nullable: false }) @Column() workspaceId: string; diff --git a/packages/twenty-server/src/core/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/core/user-workspace/user-workspace.service.ts index bb4d7a83c282..92078584b98f 100644 --- a/packages/twenty-server/src/core/user-workspace/user-workspace.service.ts +++ b/packages/twenty-server/src/core/user-workspace/user-workspace.service.ts @@ -50,6 +50,7 @@ export class UserWorkspaceService extends TypeOrmQueryService { where: { userId, }, + relations: ['workspace'], }); } diff --git a/packages/twenty-server/src/core/user/dtos/current-user.dto.ts b/packages/twenty-server/src/core/user/dtos/current-user.dto.ts new file mode 100644 index 000000000000..71c6240376a2 --- /dev/null +++ b/packages/twenty-server/src/core/user/dtos/current-user.dto.ts @@ -0,0 +1,13 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { UserWorkspace } from 'src/core/user-workspace/user-workspace.entity'; +import { User } from 'src/core/user/user.entity'; + +@ObjectType() +export class CurrentUser { + @Field(() => User) + user: User; + + @Field(() => [UserWorkspace]) + workspaces: UserWorkspace[]; +} diff --git a/packages/twenty-server/src/core/user/user.resolver.ts b/packages/twenty-server/src/core/user/user.resolver.ts index d936d68d671d..17a5079559bd 100644 --- a/packages/twenty-server/src/core/user/user.resolver.ts +++ b/packages/twenty-server/src/core/user/user.resolver.ts @@ -23,6 +23,8 @@ import { assert } from 'src/utils/assert'; import { JwtAuthGuard } from 'src/guards/jwt.auth.guard'; import { User } from 'src/core/user/user.entity'; import { WorkspaceMember } from 'src/core/user/dtos/workspace-member.dto'; +import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service'; +import { CurrentUser } from 'src/core/user/dtos/current-user.dto'; import { UserService } from './services/user.service'; @@ -39,19 +41,23 @@ const getHMACKey = (email?: string, key?: string | null) => { export class UserResolver { constructor( private readonly userService: UserService, + private readonly userWorkspaceService: UserWorkspaceService, private readonly environmentService: EnvironmentService, private readonly fileUploadService: FileUploadService, ) {} - @Query(() => User) - async currentUser(@AuthUser() { id }: User) { + @Query(() => CurrentUser) + async currentUser(@AuthUser() { id }: User): Promise { const user = await this.userService.findById(id, { relations: [{ name: 'defaultWorkspace', query: {} }], }); assert(user, 'User not found'); - return user; + const userWorkspaces = + await this.userWorkspaceService.findUserWorkspaces(id); + + return { user, workspaces: userWorkspaces }; } @ResolveField(() => WorkspaceMember, { From f8d6fb74df63bd81aa94cc7d2fa89b84ac89fa6d Mon Sep 17 00:00:00 2001 From: aditya pimpalkar Date: Mon, 26 Feb 2024 22:12:40 +0000 Subject: [PATCH 03/20] new token generation endpoint for switching workspace --- .../src/core/auth/auth.resolver.ts | 18 ++++++++++ .../src/core/auth/dto/generate-jwt.input.ts | 16 +++++++++ .../src/core/auth/services/token.service.ts | 33 +++++++++++++++++-- 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 packages/twenty-server/src/core/auth/dto/generate-jwt.input.ts diff --git a/packages/twenty-server/src/core/auth/auth.resolver.ts b/packages/twenty-server/src/core/auth/auth.resolver.ts index 686fcee98cb6..07d42b10e7d0 100644 --- a/packages/twenty-server/src/core/auth/auth.resolver.ts +++ b/packages/twenty-server/src/core/auth/auth.resolver.ts @@ -25,6 +25,7 @@ import { UpdatePasswordViaResetTokenInput } from 'src/core/auth/dto/update-passw import { EmailPasswordResetLink } from 'src/core/auth/dto/email-password-reset-link.entity'; import { InvalidatePassword } from 'src/core/auth/dto/invalidate-password.entity'; import { EmailPasswordResetLinkInput } from 'src/core/auth/dto/email-password-reset-link.input'; +import { GenerateJwtInput } from 'src/core/auth/dto/generate-jwt.input'; import { ApiKeyToken, AuthTokens } from './dto/token.entity'; import { TokenService } from './services/token.service'; @@ -128,6 +129,23 @@ export class AuthResolver { return result; } + @Mutation(() => Verify) + @UseGuards(JwtAuthGuard) + async generateJWT(@Args() args: GenerateJwtInput): Promise { + const workspace = await this.workspaceRepository.findOneBy({ + id: args.workspaceId, + }); + + assert(workspace, 'workspace doesnt exist', NotFoundException); + + const token = await this.tokenService.generateSwitchWorkspaceToken( + args.accessToken, + args.workspaceId, + ); + + return token; + } + @Mutation(() => AuthTokens) async renewToken(@Args() args: RefreshTokenInput): Promise { if (!args.refreshToken) { diff --git a/packages/twenty-server/src/core/auth/dto/generate-jwt.input.ts b/packages/twenty-server/src/core/auth/dto/generate-jwt.input.ts new file mode 100644 index 000000000000..2970faaf65dc --- /dev/null +++ b/packages/twenty-server/src/core/auth/dto/generate-jwt.input.ts @@ -0,0 +1,16 @@ +import { ArgsType, Field } from '@nestjs/graphql'; + +import { IsNotEmpty, IsString } from 'class-validator'; + +@ArgsType() +export class GenerateJwtInput { + @Field(() => String) + @IsNotEmpty() + @IsString() + workspaceId: string; + + @Field(() => String) + @IsNotEmpty() + @IsString() + accessToken: string; +} diff --git a/packages/twenty-server/src/core/auth/services/token.service.ts b/packages/twenty-server/src/core/auth/services/token.service.ts index 0c27dc043371..bba73d9f00f5 100644 --- a/packages/twenty-server/src/core/auth/services/token.service.ts +++ b/packages/twenty-server/src/core/auth/services/token.service.ts @@ -39,6 +39,7 @@ import { EmailService } from 'src/integrations/email/email.service'; import { InvalidatePassword } from 'src/core/auth/dto/invalidate-password.entity'; import { EmailPasswordResetLink } from 'src/core/auth/dto/email-password-reset-link.entity'; import { JwtData } from 'src/core/auth/types/jwt-data.type'; +import { Verify } from 'src/core/auth/dto/verify.entity'; @Injectable() export class TokenService { @@ -53,7 +54,10 @@ export class TokenService { private readonly emailService: EmailService, ) {} - async generateAccessToken(userId: string): Promise { + async generateAccessToken( + userId: string, + workspaceId: string | null = null, + ): Promise { const expiresIn = this.environmentService.getAccessTokenExpiresIn(); assert(expiresIn, '', InternalServerErrorException); @@ -74,7 +78,7 @@ export class TokenService { const jwtPayload: JwtPayload = { sub: user.id, - workspaceId: user.defaultWorkspace.id, + workspaceId: workspaceId ? workspaceId : user.defaultWorkspace.id, }; return { @@ -232,6 +236,31 @@ export class TokenService { }; } + async generateSwitchWorkspaceToken( + accessToken: string, + workspaceId: string, + ): Promise { + const secret = this.environmentService.getRefreshTokenSecret(); + const jwtPayload = await this.verifyJwt(accessToken, secret); + + const user = await this.userRepository.findOneBy({ + id: jwtPayload.sub, + }); + + assert(user, 'User not found', NotFoundException); + + const token = await this.generateAccessToken(user.id, workspaceId); + const refreshToken = await this.generateRefreshToken(user.id); + + return { + user, + tokens: { + accessToken: token, + refreshToken, + }, + }; + } + async verifyRefreshToken(refreshToken: string) { const secret = this.environmentService.getRefreshTokenSecret(); const coolDown = this.environmentService.getRefreshTokenCoolDown(); From 96b13239ea6fa9a4f4c936a3269012575fa0b138 Mon Sep 17 00:00:00 2001 From: aditya pimpalkar Date: Mon, 26 Feb 2024 22:12:46 +0000 Subject: [PATCH 04/20] lint fix --- .../1708535112230-addBillingCoreTables.ts | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/1708535112230-addBillingCoreTables.ts b/packages/twenty-server/src/database/typeorm/core/migrations/1708535112230-addBillingCoreTables.ts index c35dd97682d8..d9f0c74af7fa 100644 --- a/packages/twenty-server/src/database/typeorm/core/migrations/1708535112230-addBillingCoreTables.ts +++ b/packages/twenty-server/src/database/typeorm/core/migrations/1708535112230-addBillingCoreTables.ts @@ -1,20 +1,31 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; +import { MigrationInterface, QueryRunner } from 'typeorm'; export class AddBillingCoreTables1708535112230 implements MigrationInterface { - name = 'AddBillingCoreTables1708535112230' + name = 'AddBillingCoreTables1708535112230'; - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`CREATE TABLE "core"."billingSubscriptionItem" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "deletedAt" TIMESTAMP, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "billingSubscriptionId" uuid NOT NULL, "stripeProductId" character varying NOT NULL, "stripePriceId" character varying NOT NULL, "quantity" integer NOT NULL, CONSTRAINT "PK_0287b2d9fca488edcbf748281fc" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE TABLE "core"."billingSubscription" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "deletedAt" TIMESTAMP, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "workspaceId" uuid NOT NULL, "stripeCustomerId" character varying NOT NULL, "stripeSubscriptionId" character varying NOT NULL, "status" character varying NOT NULL, CONSTRAINT "UQ_9120b7586c3471463480b58d20a" UNIQUE ("stripeCustomerId"), CONSTRAINT "UQ_1a858c28c7766d429cbd25f05e8" UNIQUE ("stripeSubscriptionId"), CONSTRAINT "REL_4abfb70314c18da69e1bee1954" UNIQUE ("workspaceId"), CONSTRAINT "PK_6e9c72c32d91640b8087cb53666" PRIMARY KEY ("id"))`); - await queryRunner.query(`ALTER TABLE "core"."billingSubscriptionItem" ADD CONSTRAINT "FK_a602e7c9da619b8290232f6eeab" FOREIGN KEY ("billingSubscriptionId") REFERENCES "core"."billingSubscription"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "core"."billingSubscription" ADD CONSTRAINT "FK_4abfb70314c18da69e1bee1954d" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "core"."billingSubscription" DROP CONSTRAINT "FK_4abfb70314c18da69e1bee1954d"`); - await queryRunner.query(`ALTER TABLE "core"."billingSubscriptionItem" DROP CONSTRAINT "FK_a602e7c9da619b8290232f6eeab"`); - await queryRunner.query(`DROP TABLE "core"."billingSubscription"`); - await queryRunner.query(`DROP TABLE "core"."billingSubscriptionItem"`); - } + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "core"."billingSubscriptionItem" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "deletedAt" TIMESTAMP, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "billingSubscriptionId" uuid NOT NULL, "stripeProductId" character varying NOT NULL, "stripePriceId" character varying NOT NULL, "quantity" integer NOT NULL, CONSTRAINT "PK_0287b2d9fca488edcbf748281fc" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "core"."billingSubscription" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "deletedAt" TIMESTAMP, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "workspaceId" uuid NOT NULL, "stripeCustomerId" character varying NOT NULL, "stripeSubscriptionId" character varying NOT NULL, "status" character varying NOT NULL, CONSTRAINT "UQ_9120b7586c3471463480b58d20a" UNIQUE ("stripeCustomerId"), CONSTRAINT "UQ_1a858c28c7766d429cbd25f05e8" UNIQUE ("stripeSubscriptionId"), CONSTRAINT "REL_4abfb70314c18da69e1bee1954" UNIQUE ("workspaceId"), CONSTRAINT "PK_6e9c72c32d91640b8087cb53666" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "core"."billingSubscriptionItem" ADD CONSTRAINT "FK_a602e7c9da619b8290232f6eeab" FOREIGN KEY ("billingSubscriptionId") REFERENCES "core"."billingSubscription"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."billingSubscription" ADD CONSTRAINT "FK_4abfb70314c18da69e1bee1954d" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."billingSubscription" DROP CONSTRAINT "FK_4abfb70314c18da69e1bee1954d"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."billingSubscriptionItem" DROP CONSTRAINT "FK_a602e7c9da619b8290232f6eeab"`, + ); + await queryRunner.query(`DROP TABLE "core"."billingSubscription"`); + await queryRunner.query(`DROP TABLE "core"."billingSubscriptionItem"`); + } } From ecc3f7b805781e398abf4b1af9f7ffcdabf2c779 Mon Sep 17 00:00:00 2001 From: aditya pimpalkar Date: Mon, 26 Feb 2024 23:38:35 +0000 Subject: [PATCH 05/20] include dependency --- packages/twenty-server/src/core/user/user.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/twenty-server/src/core/user/user.module.ts b/packages/twenty-server/src/core/user/user.module.ts index ea98ff03e089..52c644360435 100644 --- a/packages/twenty-server/src/core/user/user.module.ts +++ b/packages/twenty-server/src/core/user/user.module.ts @@ -14,6 +14,7 @@ import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { userAutoResolverOpts } from './user.auto-resolver-opts'; import { UserService } from './services/user.service'; +import { UserWorkspaceModule } from 'src/core/user-workspace/user-workspace.module'; @Module({ imports: [ @@ -26,6 +27,7 @@ import { UserService } from './services/user.service'; }), DataSourceModule, FileModule, + UserWorkspaceModule, ], exports: [UserService], providers: [UserService, UserResolver, TypeORMService], From a2637bdaf15b60bc95676031c82ba990cb037e9f Mon Sep 17 00:00:00 2001 From: aditya pimpalkar Date: Wed, 28 Feb 2024 00:08:11 +0000 Subject: [PATCH 06/20] requested fixes --- .../twenty-front/src/generated/graphql.tsx | 74 +++++++++---------- .../modules/users/components/UserProvider.tsx | 8 +- .../users/graphql/queries/getCurrentUser.ts | 57 +++++++------- .../src/core/auth/auth.resolver.ts | 21 +++++- .../src/core/auth/dto/generate-jwt.input.ts | 5 -- .../src/core/auth/services/token.service.ts | 13 +--- .../user-workspace/user-workspace.entity.ts | 10 ++- .../src/core/user/dtos/current-user.dto.ts | 13 ---- .../src/core/user/user.entity.ts | 8 ++ .../src/core/user/user.module.ts | 5 +- .../src/core/user/user.resolver.ts | 10 +-- .../src/core/workspace/workspace.entity.ts | 5 ++ 12 files changed, 117 insertions(+), 112 deletions(-) delete mode 100644 packages/twenty-server/src/core/user/dtos/current-user.dto.ts diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 2788156f190f..5328b914415f 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -81,12 +81,6 @@ export type ClientConfig = { telemetry: Telemetry; }; -export type CurrentUser = { - __typename?: 'CurrentUser'; - user: User; - workspaces: Array; -}; - export type CursorPaging = { /** Paginate after opaque cursor */ after?: InputMaybe; @@ -234,6 +228,7 @@ export type Mutation = { deleteUser: User; emailPasswordResetLink: EmailPasswordResetLink; generateApiKeyToken: ApiKeyToken; + generateJWT: Verify; generateTransientToken: TransientToken; impersonate: Verify; renewToken: AuthTokens; @@ -282,6 +277,11 @@ export type MutationGenerateApiKeyTokenArgs = { }; +export type MutationGenerateJwtArgs = { + workspaceId: Scalars['String']; +}; + + export type MutationImpersonateArgs = { userId: Scalars['String']; }; @@ -373,7 +373,7 @@ export type Query = { checkUserExists: UserExists; checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid; clientConfig: ClientConfig; - currentUser: CurrentUser; + currentUser: User; currentWorkspace: Workspace; findWorkspaceFromInviteHash: Workspace; getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal; @@ -554,6 +554,7 @@ export type User = { supportUserHash?: Maybe; updatedAt: Scalars['DateTime']; workspaceMember?: Maybe; + workspaces?: Maybe>; }; export type UserEdge = { @@ -886,7 +887,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; -export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'CurrentUser', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } }, workspaces: Array<{ __typename?: 'UserWorkspace', userId: string, workspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null } }> } }; +export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null }, workspaces?: Array<{ __typename?: 'UserWorkspace', workspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null } }> | null } }; export type ActivateWorkspaceMutationVariables = Exact<{ input: ActivateWorkspaceInput; @@ -1717,42 +1718,39 @@ export type UploadProfilePictureMutationOptions = Apollo.BaseMutationOptions { setIsLoading(false); } if (queryData?.currentUser) { - setCurrentUser(queryData.currentUser.user); - setCurrentWorkspace(queryData.currentUser.user.defaultWorkspace); + setCurrentUser(queryData.currentUser); + setCurrentWorkspace(queryData.currentUser.defaultWorkspace); } - if (queryData?.currentUser?.user.workspaceMember) { - const workspaceMember = queryData.currentUser.user.workspaceMember; + if (queryData?.currentUser?.workspaceMember) { + const workspaceMember = queryData.currentUser.workspaceMember; setCurrentWorkspaceMember({ ...workspaceMember, colorScheme: (workspaceMember.colorScheme as ColorScheme) ?? 'Light', diff --git a/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUser.ts b/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUser.ts index 5c0bc90f513c..33dc3ad22f43 100644 --- a/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUser.ts +++ b/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUser.ts @@ -4,42 +4,39 @@ import { gql } from '@apollo/client'; export const GET_CURRENT_USER = gql` query GetCurrentUser { currentUser { - user { + id + firstName + lastName + email + canImpersonate + supportUserHash + workspaceMember { id - firstName - lastName - email - canImpersonate - supportUserHash - workspaceMember { - id - name { - firstName - lastName - } - colorScheme - avatarUrl - locale + name { + firstName + lastName } - defaultWorkspace { + colorScheme + avatarUrl + locale + } + defaultWorkspace { + id + displayName + logo + domainName + inviteHash + allowImpersonation + subscriptionStatus + activationStatus + featureFlags { id - displayName - logo - domainName - inviteHash - allowImpersonation - subscriptionStatus - activationStatus - featureFlags { - id - key - value - workspaceId - } + key + value + workspaceId } } workspaces { - userId workspace { id displayName diff --git a/packages/twenty-server/src/core/auth/auth.resolver.ts b/packages/twenty-server/src/core/auth/auth.resolver.ts index 07d42b10e7d0..b27ec71b445c 100644 --- a/packages/twenty-server/src/core/auth/auth.resolver.ts +++ b/packages/twenty-server/src/core/auth/auth.resolver.ts @@ -26,6 +26,7 @@ import { EmailPasswordResetLink } from 'src/core/auth/dto/email-password-reset-l import { InvalidatePassword } from 'src/core/auth/dto/invalidate-password.entity'; import { EmailPasswordResetLinkInput } from 'src/core/auth/dto/email-password-reset-link.input'; import { GenerateJwtInput } from 'src/core/auth/dto/generate-jwt.input'; +import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service'; import { ApiKeyToken, AuthTokens } from './dto/token.entity'; import { TokenService } from './services/token.service'; @@ -50,6 +51,7 @@ export class AuthResolver { private authService: AuthService, private tokenService: TokenService, private userService: UserService, + private userWorkspaceService: UserWorkspaceService, ) {} @Query(() => UserExists) @@ -131,15 +133,30 @@ export class AuthResolver { @Mutation(() => Verify) @UseGuards(JwtAuthGuard) - async generateJWT(@Args() args: GenerateJwtInput): Promise { + async generateJWT( + @AuthUser() user: User, + @Args() args: GenerateJwtInput, + ): Promise { + const userExists = await this.userService.findById(user.id); + + assert(userExists, 'User not found', NotFoundException); + const workspace = await this.workspaceRepository.findOneBy({ id: args.workspaceId, }); assert(workspace, 'workspace doesnt exist', NotFoundException); + const userWorkspace = + await this.userWorkspaceService.checkUserWorkspaceExists( + user.id, + workspace.id, + ); + + assert(userWorkspace, 'cannot access workspace', ForbiddenException); + const token = await this.tokenService.generateSwitchWorkspaceToken( - args.accessToken, + user, args.workspaceId, ); diff --git a/packages/twenty-server/src/core/auth/dto/generate-jwt.input.ts b/packages/twenty-server/src/core/auth/dto/generate-jwt.input.ts index 2970faaf65dc..cd8b4461ec68 100644 --- a/packages/twenty-server/src/core/auth/dto/generate-jwt.input.ts +++ b/packages/twenty-server/src/core/auth/dto/generate-jwt.input.ts @@ -8,9 +8,4 @@ export class GenerateJwtInput { @IsNotEmpty() @IsString() workspaceId: string; - - @Field(() => String) - @IsNotEmpty() - @IsString() - accessToken: string; } diff --git a/packages/twenty-server/src/core/auth/services/token.service.ts b/packages/twenty-server/src/core/auth/services/token.service.ts index bba73d9f00f5..237ed076df3a 100644 --- a/packages/twenty-server/src/core/auth/services/token.service.ts +++ b/packages/twenty-server/src/core/auth/services/token.service.ts @@ -56,7 +56,7 @@ export class TokenService { async generateAccessToken( userId: string, - workspaceId: string | null = null, + workspaceId?: string, ): Promise { const expiresIn = this.environmentService.getAccessTokenExpiresIn(); @@ -237,18 +237,9 @@ export class TokenService { } async generateSwitchWorkspaceToken( - accessToken: string, + user: User, workspaceId: string, ): Promise { - const secret = this.environmentService.getRefreshTokenSecret(); - const jwtPayload = await this.verifyJwt(accessToken, secret); - - const user = await this.userRepository.findOneBy({ - id: jwtPayload.sub, - }); - - assert(user, 'User not found', NotFoundException); - const token = await this.generateAccessToken(user.id, workspaceId); const refreshToken = await this.generateRefreshToken(user.id); diff --git a/packages/twenty-server/src/core/user-workspace/user-workspace.entity.ts b/packages/twenty-server/src/core/user-workspace/user-workspace.entity.ts index 3b78b2dabab8..37360d558791 100644 --- a/packages/twenty-server/src/core/user-workspace/user-workspace.entity.ts +++ b/packages/twenty-server/src/core/user-workspace/user-workspace.entity.ts @@ -23,7 +23,11 @@ export class UserWorkspace { @PrimaryGeneratedColumn('uuid') id: string; - @Field() + // @Field(() => [User]) + // @ManyToMany(() => User, (user) => user.userWorkspaces) + // users: User[]; + + @Field(() => User) @JoinColumn({ name: 'userId' }) user: User; @@ -31,6 +35,10 @@ export class UserWorkspace { @Column() userId: string; + // @Field(() => [Workspace]) + // @ManyToMany(() => Workspace, (workspace) => workspace.workspaceUsers) + // workspaces: Workspace[]; + @Field(() => Workspace) @ManyToOne(() => Workspace, (workspace) => workspace.users, { onDelete: 'SET NULL', diff --git a/packages/twenty-server/src/core/user/dtos/current-user.dto.ts b/packages/twenty-server/src/core/user/dtos/current-user.dto.ts deleted file mode 100644 index 71c6240376a2..000000000000 --- a/packages/twenty-server/src/core/user/dtos/current-user.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Field, ObjectType } from '@nestjs/graphql'; - -import { UserWorkspace } from 'src/core/user-workspace/user-workspace.entity'; -import { User } from 'src/core/user/user.entity'; - -@ObjectType() -export class CurrentUser { - @Field(() => User) - user: User; - - @Field(() => [UserWorkspace]) - workspaces: UserWorkspace[]; -} diff --git a/packages/twenty-server/src/core/user/user.entity.ts b/packages/twenty-server/src/core/user/user.entity.ts index 903938c133e9..7fa0456d98a1 100644 --- a/packages/twenty-server/src/core/user/user.entity.ts +++ b/packages/twenty-server/src/core/user/user.entity.ts @@ -14,6 +14,7 @@ import { IDField } from '@ptc-org/nestjs-query-graphql'; import { RefreshToken } from 'src/core/refresh-token/refresh-token.entity'; import { Workspace } from 'src/core/workspace/workspace.entity'; import { WorkspaceMember } from 'src/core/user/dtos/workspace-member.dto'; +import { UserWorkspace } from 'src/core/user-workspace/user-workspace.entity'; @Entity({ name: 'user', schema: 'core' }) @ObjectType('User') @@ -87,4 +88,11 @@ export class User { @Field(() => WorkspaceMember, { nullable: true }) workspaceMember: WorkspaceMember; + + // @Field(() => [Workspace], { nullable: true }) + // @ManyToMany(() => Workspace, (workspace) => workspace.workspaceUsers) + // userWorkspaces: Workspace[]; + + @Field(() => [UserWorkspace], { nullable: true }) + workspaces: UserWorkspace[]; } diff --git a/packages/twenty-server/src/core/user/user.module.ts b/packages/twenty-server/src/core/user/user.module.ts index 52c644360435..75fad2ccce73 100644 --- a/packages/twenty-server/src/core/user/user.module.ts +++ b/packages/twenty-server/src/core/user/user.module.ts @@ -10,17 +10,18 @@ import { UserResolver } from 'src/core/user/user.resolver'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { DataSourceModule } from 'src/metadata/data-source/data-source.module'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; +import { UserWorkspaceModule } from 'src/core/user-workspace/user-workspace.module'; +import { UserWorkspace } from 'src/core/user-workspace/user-workspace.entity'; import { userAutoResolverOpts } from './user.auto-resolver-opts'; import { UserService } from './services/user.service'; -import { UserWorkspaceModule } from 'src/core/user-workspace/user-workspace.module'; @Module({ imports: [ NestjsQueryGraphQLModule.forFeature({ imports: [ - NestjsQueryTypeOrmModule.forFeature([User], 'core'), + NestjsQueryTypeOrmModule.forFeature([User, UserWorkspace], 'core'), TypeORMModule, ], resolvers: userAutoResolverOpts, diff --git a/packages/twenty-server/src/core/user/user.resolver.ts b/packages/twenty-server/src/core/user/user.resolver.ts index 17a5079559bd..b069b45f3f10 100644 --- a/packages/twenty-server/src/core/user/user.resolver.ts +++ b/packages/twenty-server/src/core/user/user.resolver.ts @@ -24,7 +24,6 @@ import { JwtAuthGuard } from 'src/guards/jwt.auth.guard'; import { User } from 'src/core/user/user.entity'; import { WorkspaceMember } from 'src/core/user/dtos/workspace-member.dto'; import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service'; -import { CurrentUser } from 'src/core/user/dtos/current-user.dto'; import { UserService } from './services/user.service'; @@ -46,18 +45,17 @@ export class UserResolver { private readonly fileUploadService: FileUploadService, ) {} - @Query(() => CurrentUser) - async currentUser(@AuthUser() { id }: User): Promise { + @Query(() => User) + async currentUser(@AuthUser() { id }: User): Promise { const user = await this.userService.findById(id, { relations: [{ name: 'defaultWorkspace', query: {} }], }); assert(user, 'User not found'); - const userWorkspaces = - await this.userWorkspaceService.findUserWorkspaces(id); + user.workspaces = await this.userWorkspaceService.findUserWorkspaces(id); - return { user, workspaces: userWorkspaces }; + return user; } @ResolveField(() => WorkspaceMember, { diff --git a/packages/twenty-server/src/core/workspace/workspace.entity.ts b/packages/twenty-server/src/core/workspace/workspace.entity.ts index 2ea41f013934..6fe448e4952c 100644 --- a/packages/twenty-server/src/core/workspace/workspace.entity.ts +++ b/packages/twenty-server/src/core/workspace/workspace.entity.ts @@ -54,6 +54,11 @@ export class Workspace { @OneToMany(() => User, (user) => user.defaultWorkspace) users: User[]; + // @Field(() => [User]) + // @ManyToMany(() => User, (user) => user.userWorkspaces) + // @JoinColumn() + // workspaceUsers: User[]; + @Field() @Column({ default: true }) allowImpersonation: boolean; From 972df459799471cd8cdc4a6a83769541c5bd11be Mon Sep 17 00:00:00 2001 From: aditya pimpalkar Date: Wed, 28 Feb 2024 11:09:41 +0000 Subject: [PATCH 07/20] resolver test pass --- packages/twenty-server/src/core/auth/auth.resolver.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/twenty-server/src/core/auth/auth.resolver.spec.ts b/packages/twenty-server/src/core/auth/auth.resolver.spec.ts index 6472cd1f0edd..97c6706d8279 100644 --- a/packages/twenty-server/src/core/auth/auth.resolver.spec.ts +++ b/packages/twenty-server/src/core/auth/auth.resolver.spec.ts @@ -8,6 +8,7 @@ import { AuthResolver } from './auth.resolver'; import { TokenService } from './services/token.service'; import { AuthService } from './services/auth.service'; +import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service'; describe('AuthResolver', () => { let resolver: AuthResolver; @@ -32,6 +33,10 @@ describe('AuthResolver', () => { provide: UserService, useValue: {}, }, + { + provide: UserWorkspaceService, + useValue: {}, + }, ], }).compile(); From 21b914a4b8ffae2ecd83acc23f6c0681a739ae58 Mon Sep 17 00:00:00 2001 From: aditya pimpalkar Date: Thu, 29 Feb 2024 01:17:25 +0000 Subject: [PATCH 08/20] changing defaultWorkspace and workspaceMember when switching workspaces --- .../src/core/auth/auth.resolver.spec.ts | 2 +- .../src/core/auth/auth.resolver.ts | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/twenty-server/src/core/auth/auth.resolver.spec.ts b/packages/twenty-server/src/core/auth/auth.resolver.spec.ts index 97c6706d8279..09b65a430703 100644 --- a/packages/twenty-server/src/core/auth/auth.resolver.spec.ts +++ b/packages/twenty-server/src/core/auth/auth.resolver.spec.ts @@ -3,12 +3,12 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { Workspace } from 'src/core/workspace/workspace.entity'; import { UserService } from 'src/core/user/services/user.service'; +import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service'; import { AuthResolver } from './auth.resolver'; import { TokenService } from './services/token.service'; import { AuthService } from './services/auth.service'; -import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service'; describe('AuthResolver', () => { let resolver: AuthResolver; diff --git a/packages/twenty-server/src/core/auth/auth.resolver.ts b/packages/twenty-server/src/core/auth/auth.resolver.ts index b27ec71b445c..affb943a4c51 100644 --- a/packages/twenty-server/src/core/auth/auth.resolver.ts +++ b/packages/twenty-server/src/core/auth/auth.resolver.ts @@ -48,6 +48,8 @@ export class AuthResolver { constructor( @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, + @InjectRepository(User, 'core') + private readonly userRepository: Repository, private authService: AuthService, private tokenService: TokenService, private userService: UserService, @@ -128,6 +130,11 @@ export class AuthResolver { const result = await this.authService.verify(email); + if (result.user.id) { + result.user.workspaces = + await this.userWorkspaceService.findUserWorkspaces(result.user.id); + } + return result; } @@ -155,6 +162,20 @@ export class AuthResolver { assert(userWorkspace, 'cannot access workspace', ForbiddenException); + await this.userRepository.save({ + id: user.id, + defaultWorkspace: workspace, + updatedAt: new Date().toISOString(), + }); + + const workspaceMember = await this.userService.loadWorkspaceMember(user); + + if (workspaceMember) { + user.workspaceMember = workspaceMember; + } + + user.defaultWorkspace = workspace; + const token = await this.tokenService.generateSwitchWorkspaceToken( user, args.workspaceId, From 2ba4aecc570764deab32a12e5b59d74cf5ee8fd9 Mon Sep 17 00:00:00 2001 From: aditya pimpalkar Date: Thu, 29 Feb 2024 01:42:16 +0000 Subject: [PATCH 09/20] tests fix --- packages/twenty-server/src/core/auth/auth.resolver.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/twenty-server/src/core/auth/auth.resolver.spec.ts b/packages/twenty-server/src/core/auth/auth.resolver.spec.ts index 09b65a430703..fcfb0d6c858a 100644 --- a/packages/twenty-server/src/core/auth/auth.resolver.spec.ts +++ b/packages/twenty-server/src/core/auth/auth.resolver.spec.ts @@ -9,6 +9,7 @@ import { AuthResolver } from './auth.resolver'; import { TokenService } from './services/token.service'; import { AuthService } from './services/auth.service'; +import { User } from 'src/core/user/user.entity'; describe('AuthResolver', () => { let resolver: AuthResolver; @@ -21,6 +22,10 @@ describe('AuthResolver', () => { provide: getRepositoryToken(Workspace, 'core'), useValue: {}, }, + { + provide: getRepositoryToken(User, 'core'), + useValue: {}, + }, { provide: AuthService, useValue: {}, From 407402b1f8e65f7e90f0a514b801f3bb13a824e1 Mon Sep 17 00:00:00 2001 From: aditya pimpalkar Date: Thu, 29 Feb 2024 23:38:29 +0000 Subject: [PATCH 10/20] requested changes --- .../twenty-front/src/generated/graphql.tsx | 2 +- .../src/core/auth/auth.resolver.spec.ts | 2 +- .../src/core/auth/auth.resolver.ts | 38 +------------------ .../src/core/auth/services/token.service.ts | 33 ++++++++++++++-- .../user-workspace/user-workspace.entity.ts | 15 +++----- .../src/core/user/user.entity.ts | 11 ++++-- .../src/core/workspace/workspace.entity.ts | 9 +++-- .../src/database/typeorm/typeorm.service.ts | 2 + 8 files changed, 54 insertions(+), 58 deletions(-) diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 5328b914415f..dbc69d694f1f 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -228,7 +228,7 @@ export type Mutation = { deleteUser: User; emailPasswordResetLink: EmailPasswordResetLink; generateApiKeyToken: ApiKeyToken; - generateJWT: Verify; + generateJWT: AuthTokens; generateTransientToken: TransientToken; impersonate: Verify; renewToken: AuthTokens; diff --git a/packages/twenty-server/src/core/auth/auth.resolver.spec.ts b/packages/twenty-server/src/core/auth/auth.resolver.spec.ts index fcfb0d6c858a..0eb9ef26564f 100644 --- a/packages/twenty-server/src/core/auth/auth.resolver.spec.ts +++ b/packages/twenty-server/src/core/auth/auth.resolver.spec.ts @@ -4,12 +4,12 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { Workspace } from 'src/core/workspace/workspace.entity'; import { UserService } from 'src/core/user/services/user.service'; import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service'; +import { User } from 'src/core/user/user.entity'; import { AuthResolver } from './auth.resolver'; import { TokenService } from './services/token.service'; import { AuthService } from './services/auth.service'; -import { User } from 'src/core/user/user.entity'; describe('AuthResolver', () => { let resolver: AuthResolver; diff --git a/packages/twenty-server/src/core/auth/auth.resolver.ts b/packages/twenty-server/src/core/auth/auth.resolver.ts index affb943a4c51..3cac446dd621 100644 --- a/packages/twenty-server/src/core/auth/auth.resolver.ts +++ b/packages/twenty-server/src/core/auth/auth.resolver.ts @@ -48,8 +48,6 @@ export class AuthResolver { constructor( @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, - @InjectRepository(User, 'core') - private readonly userRepository: Repository, private authService: AuthService, private tokenService: TokenService, private userService: UserService, @@ -138,44 +136,12 @@ export class AuthResolver { return result; } - @Mutation(() => Verify) + @Mutation(() => AuthTokens) @UseGuards(JwtAuthGuard) async generateJWT( @AuthUser() user: User, @Args() args: GenerateJwtInput, - ): Promise { - const userExists = await this.userService.findById(user.id); - - assert(userExists, 'User not found', NotFoundException); - - const workspace = await this.workspaceRepository.findOneBy({ - id: args.workspaceId, - }); - - assert(workspace, 'workspace doesnt exist', NotFoundException); - - const userWorkspace = - await this.userWorkspaceService.checkUserWorkspaceExists( - user.id, - workspace.id, - ); - - assert(userWorkspace, 'cannot access workspace', ForbiddenException); - - await this.userRepository.save({ - id: user.id, - defaultWorkspace: workspace, - updatedAt: new Date().toISOString(), - }); - - const workspaceMember = await this.userService.loadWorkspaceMember(user); - - if (workspaceMember) { - user.workspaceMember = workspaceMember; - } - - user.defaultWorkspace = workspace; - + ): Promise { const token = await this.tokenService.generateSwitchWorkspaceToken( user, args.workspaceId, diff --git a/packages/twenty-server/src/core/auth/services/token.service.ts b/packages/twenty-server/src/core/auth/services/token.service.ts index 237ed076df3a..7094c81ede96 100644 --- a/packages/twenty-server/src/core/auth/services/token.service.ts +++ b/packages/twenty-server/src/core/auth/services/token.service.ts @@ -29,6 +29,7 @@ import { assert } from 'src/utils/assert'; import { ApiKeyToken, AuthToken, + AuthTokens, PasswordResetToken, } from 'src/core/auth/dto/token.entity'; import { EnvironmentService } from 'src/integrations/environment/environment.service'; @@ -39,7 +40,8 @@ import { EmailService } from 'src/integrations/email/email.service'; import { InvalidatePassword } from 'src/core/auth/dto/invalidate-password.entity'; import { EmailPasswordResetLink } from 'src/core/auth/dto/email-password-reset-link.entity'; import { JwtData } from 'src/core/auth/types/jwt-data.type'; -import { Verify } from 'src/core/auth/dto/verify.entity'; +import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service'; +import { Workspace } from 'src/core/workspace/workspace.entity'; @Injectable() export class TokenService { @@ -51,7 +53,10 @@ export class TokenService { private readonly userRepository: Repository, @InjectRepository(RefreshToken, 'core') private readonly refreshTokenRepository: Repository, + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, private readonly emailService: EmailService, + private readonly userWorkspaceService: UserWorkspaceService, ) {} async generateAccessToken( @@ -239,12 +244,34 @@ export class TokenService { async generateSwitchWorkspaceToken( user: User, workspaceId: string, - ): Promise { + ): Promise { + const userExists = await this.userRepository.findBy({ id: user.id }); + + assert(userExists, 'User not found', NotFoundException); + + const workspace = await this.workspaceRepository.findOneBy({ + id: workspaceId, + }); + + assert(workspace, 'workspace doesnt exist', NotFoundException); + + const userWorkspace = + await this.userWorkspaceService.checkUserWorkspaceExists( + user.id, + workspace.id, + ); + + assert(userWorkspace, 'cannot access workspace', ForbiddenException); + + await this.userRepository.save({ + id: user.id, + defaultWorkspace: workspace, + }); + const token = await this.generateAccessToken(user.id, workspaceId); const refreshToken = await this.generateRefreshToken(user.id); return { - user, tokens: { accessToken: token, refreshToken, diff --git a/packages/twenty-server/src/core/user-workspace/user-workspace.entity.ts b/packages/twenty-server/src/core/user-workspace/user-workspace.entity.ts index 37360d558791..369894ee9daa 100644 --- a/packages/twenty-server/src/core/user-workspace/user-workspace.entity.ts +++ b/packages/twenty-server/src/core/user-workspace/user-workspace.entity.ts @@ -23,11 +23,10 @@ export class UserWorkspace { @PrimaryGeneratedColumn('uuid') id: string; - // @Field(() => [User]) - // @ManyToMany(() => User, (user) => user.userWorkspaces) - // users: User[]; - @Field(() => User) + @ManyToOne(() => User, (user) => user.workspaceUsers, { + onDelete: 'CASCADE', + }) @JoinColumn({ name: 'userId' }) user: User; @@ -35,13 +34,9 @@ export class UserWorkspace { @Column() userId: string; - // @Field(() => [Workspace]) - // @ManyToMany(() => Workspace, (workspace) => workspace.workspaceUsers) - // workspaces: Workspace[]; - @Field(() => Workspace) - @ManyToOne(() => Workspace, (workspace) => workspace.users, { - onDelete: 'SET NULL', + @ManyToOne(() => Workspace, (workspace) => workspace.workspaceUsers, { + onDelete: 'CASCADE', }) @JoinColumn({ name: 'workspaceId' }) workspace: Workspace; diff --git a/packages/twenty-server/src/core/user/user.entity.ts b/packages/twenty-server/src/core/user/user.entity.ts index 7fa0456d98a1..3487f3ff7aa5 100644 --- a/packages/twenty-server/src/core/user/user.entity.ts +++ b/packages/twenty-server/src/core/user/user.entity.ts @@ -73,6 +73,10 @@ export class User { }) defaultWorkspace: Workspace; + @Field() + @Column() + defaultWorkspaceId: string; + @Field({ nullable: true }) @Column({ nullable: true }) passwordResetToken: string; @@ -89,9 +93,10 @@ export class User { @Field(() => WorkspaceMember, { nullable: true }) workspaceMember: WorkspaceMember; - // @Field(() => [Workspace], { nullable: true }) - // @ManyToMany(() => Workspace, (workspace) => workspace.workspaceUsers) - // userWorkspaces: Workspace[]; + @OneToMany(() => UserWorkspace, (userWorkspace) => userWorkspace.user, { + onDelete: 'CASCADE', + }) + workspaceUsers: UserWorkspace[]; @Field(() => [UserWorkspace], { nullable: true }) workspaces: UserWorkspace[]; diff --git a/packages/twenty-server/src/core/workspace/workspace.entity.ts b/packages/twenty-server/src/core/workspace/workspace.entity.ts index 6fe448e4952c..fc7d4d0a3096 100644 --- a/packages/twenty-server/src/core/workspace/workspace.entity.ts +++ b/packages/twenty-server/src/core/workspace/workspace.entity.ts @@ -14,6 +14,7 @@ import { import { User } from 'src/core/user/user.entity'; import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity'; import { BillingSubscription } from 'src/core/billing/entities/billing-subscription.entity'; +import { UserWorkspace } from 'src/core/user-workspace/user-workspace.entity'; @Entity({ name: 'workspace', schema: 'core' }) @ObjectType('Workspace') @@ -54,10 +55,10 @@ export class Workspace { @OneToMany(() => User, (user) => user.defaultWorkspace) users: User[]; - // @Field(() => [User]) - // @ManyToMany(() => User, (user) => user.userWorkspaces) - // @JoinColumn() - // workspaceUsers: User[]; + @OneToMany(() => UserWorkspace, (userWorkspace) => userWorkspace.workspace, { + onDelete: 'CASCADE', + }) + workspaceUsers: UserWorkspace[]; @Field() @Column({ default: true }) diff --git a/packages/twenty-server/src/database/typeorm/typeorm.service.ts b/packages/twenty-server/src/database/typeorm/typeorm.service.ts index 837bacef6964..1aa3f260f16b 100644 --- a/packages/twenty-server/src/database/typeorm/typeorm.service.ts +++ b/packages/twenty-server/src/database/typeorm/typeorm.service.ts @@ -10,6 +10,7 @@ import { RefreshToken } from 'src/core/refresh-token/refresh-token.entity'; import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity'; import { BillingSubscription } from 'src/core/billing/entities/billing-subscription.entity'; import { BillingSubscriptionItem } from 'src/core/billing/entities/billing-subscription-item.entity'; +import { UserWorkspace } from 'src/core/user-workspace/user-workspace.entity'; @Injectable() export class TypeORMService implements OnModuleInit, OnModuleDestroy { @@ -26,6 +27,7 @@ export class TypeORMService implements OnModuleInit, OnModuleDestroy { entities: [ User, Workspace, + UserWorkspace, RefreshToken, FeatureFlagEntity, BillingSubscription, From 0d186ec81f39f4db0d660ae949d2c3862d96cc16 Mon Sep 17 00:00:00 2001 From: aditya pimpalkar Date: Thu, 29 Feb 2024 23:47:48 +0000 Subject: [PATCH 11/20] delete user/workspace edge case handled --- .../core/auth/services/token.service.spec.ts | 10 ++++++++ .../core/user/services/user.service.spec.ts | 5 ++++ .../src/core/user/services/user.service.ts | 24 +++++++++++++++++-- .../services/workspace.service.spec.ts | 10 ++++++++ .../workspace/services/workspace.service.ts | 15 ++++++++++++ .../src/core/workspace/workspace.module.ts | 4 +++- 6 files changed, 65 insertions(+), 3 deletions(-) diff --git a/packages/twenty-server/src/core/auth/services/token.service.spec.ts b/packages/twenty-server/src/core/auth/services/token.service.spec.ts index 709799bcfe32..de11a8550f1e 100644 --- a/packages/twenty-server/src/core/auth/services/token.service.spec.ts +++ b/packages/twenty-server/src/core/auth/services/token.service.spec.ts @@ -9,6 +9,8 @@ import { JwtAuthStrategy } from 'src/core/auth/strategies/jwt.auth.strategy'; import { EmailService } from 'src/integrations/email/email.service'; import { TokenService } from './token.service'; +import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service'; +import { Workspace } from 'src/core/workspace/workspace.entity'; describe('TokenService', () => { let service: TokenService; @@ -33,6 +35,10 @@ describe('TokenService', () => { provide: EmailService, useValue: {}, }, + { + provide: UserWorkspaceService, + useValue: {}, + }, { provide: getRepositoryToken(User, 'core'), useValue: {}, @@ -41,6 +47,10 @@ describe('TokenService', () => { provide: getRepositoryToken(RefreshToken, 'core'), useValue: {}, }, + { + provide: getRepositoryToken(Workspace, 'core'), + useValue: {}, + }, ], }).compile(); diff --git a/packages/twenty-server/src/core/user/services/user.service.spec.ts b/packages/twenty-server/src/core/user/services/user.service.spec.ts index fd59dc7d2067..97cd67f87d41 100644 --- a/packages/twenty-server/src/core/user/services/user.service.spec.ts +++ b/packages/twenty-server/src/core/user/services/user.service.spec.ts @@ -4,6 +4,7 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { User } from 'src/core/user/user.entity'; import { DataSourceService } from 'src/metadata/data-source/data-source.service'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; +import { UserWorkspace } from 'src/core/user-workspace/user-workspace.entity'; import { UserService } from './user.service'; @@ -18,6 +19,10 @@ describe('UserService', () => { provide: getRepositoryToken(User, 'core'), useValue: {}, }, + { + provide: getRepositoryToken(UserWorkspace, 'core'), + useValue: {}, + }, { provide: DataSourceService, useValue: {}, diff --git a/packages/twenty-server/src/core/user/services/user.service.ts b/packages/twenty-server/src/core/user/services/user.service.ts index 8cb85e93b1d2..0c9b38c131dc 100644 --- a/packages/twenty-server/src/core/user/services/user.service.ts +++ b/packages/twenty-server/src/core/user/services/user.service.ts @@ -9,11 +9,14 @@ import { WorkspaceMember } from 'src/core/user/dtos/workspace-member.dto'; import { DataSourceService } from 'src/metadata/data-source/data-source.service'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity'; +import { UserWorkspace } from 'src/core/user-workspace/user-workspace.entity'; export class UserService extends TypeOrmQueryService { constructor( @InjectRepository(User, 'core') private readonly userRepository: Repository, + @InjectRepository(UserWorkspace, 'core') + private readonly userWorkspaceRepository: Repository, private readonly dataSourceService: DataSourceService, private readonly typeORMService: TypeORMService, ) { @@ -101,12 +104,29 @@ export class UserService extends TypeOrmQueryService { } async deleteUser(userId: string): Promise { - const user = await this.userRepository.findOneBy({ - id: userId, + const user = await this.userRepository.findOne({ + where: { + id: userId, + }, + relations: ['defaultWorkspace'], }); assert(user, 'User not found'); + const dataSourceMetadata = + await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( + user.defaultWorkspace.id, + ); + + const workspaceDataSource = + await this.typeORMService.connectToDataSource(dataSourceMetadata); + + await workspaceDataSource?.query( + `DELETE FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "userId" = '${userId}'`, + ); + + await this.userWorkspaceRepository.delete({ userId }); + await this.userRepository.delete(user.id); return user; diff --git a/packages/twenty-server/src/core/workspace/services/workspace.service.spec.ts b/packages/twenty-server/src/core/workspace/services/workspace.service.spec.ts index 46350109df68..e6fd597c5776 100644 --- a/packages/twenty-server/src/core/workspace/services/workspace.service.spec.ts +++ b/packages/twenty-server/src/core/workspace/services/workspace.service.spec.ts @@ -4,6 +4,8 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { Workspace } from 'src/core/workspace/workspace.entity'; import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service'; import { UserService } from 'src/core/user/services/user.service'; +import { UserWorkspace } from 'src/core/user-workspace/user-workspace.entity'; +import { User } from 'src/core/user/user.entity'; import { WorkspaceService } from './workspace.service'; @@ -18,6 +20,14 @@ describe('WorkspaceService', () => { provide: getRepositoryToken(Workspace, 'core'), useValue: {}, }, + { + provide: getRepositoryToken(UserWorkspace, 'core'), + useValue: {}, + }, + { + provide: getRepositoryToken(User, 'core'), + useValue: {}, + }, { provide: WorkspaceManagerService, useValue: {}, diff --git a/packages/twenty-server/src/core/workspace/services/workspace.service.ts b/packages/twenty-server/src/core/workspace/services/workspace.service.ts index 18166c4cc56f..bcaa65295423 100644 --- a/packages/twenty-server/src/core/workspace/services/workspace.service.ts +++ b/packages/twenty-server/src/core/workspace/services/workspace.service.ts @@ -11,11 +11,16 @@ import { Workspace } from 'src/core/workspace/workspace.entity'; import { User } from 'src/core/user/user.entity'; import { UserService } from 'src/core/user/services/user.service'; import { ActivateWorkspaceInput } from 'src/core/workspace/dtos/activate-workspace-input'; +import { UserWorkspace } from 'src/core/user-workspace/user-workspace.entity'; export class WorkspaceService extends TypeOrmQueryService { constructor( @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, + @InjectRepository(UserWorkspace, 'core') + private readonly userWorkspaceRepository: Repository, + @InjectRepository(User, 'core') + private readonly userRepository: Repository, private readonly workspaceManagerService: WorkspaceManagerService, private readonly userService: UserService, ) { @@ -44,6 +49,16 @@ export class WorkspaceService extends TypeOrmQueryService { assert(workspace, 'Workspace not found'); + await this.userWorkspaceRepository.delete({ workspaceId: id }); + + const users = await this.userRepository.find({ + where: { + defaultWorkspaceId: workspace.id, + }, + }); + + await this.userRepository.remove(users); + await this.workspaceManagerService.delete(id); if (shouldDeleteCoreWorkspace) { await this.workspaceRepository.delete(id); diff --git a/packages/twenty-server/src/core/workspace/workspace.module.ts b/packages/twenty-server/src/core/workspace/workspace.module.ts index 049f773c7b67..bd44029e4fee 100644 --- a/packages/twenty-server/src/core/workspace/workspace.module.ts +++ b/packages/twenty-server/src/core/workspace/workspace.module.ts @@ -9,6 +9,8 @@ import { WorkspaceResolver } from 'src/core/workspace/workspace.resolver'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity'; import { UserModule } from 'src/core/user/user.module'; +import { UserWorkspace } from 'src/core/user-workspace/user-workspace.entity'; +import { User } from 'src/core/user/user.entity'; import { Workspace } from './workspace.entity'; import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts'; @@ -21,7 +23,7 @@ import { WorkspaceService } from './services/workspace.service'; NestjsQueryGraphQLModule.forFeature({ imports: [ NestjsQueryTypeOrmModule.forFeature( - [Workspace, FeatureFlagEntity], + [User, Workspace, UserWorkspace, FeatureFlagEntity], 'core', ), WorkspaceManagerModule, From 825d522cabc77514d03479d23bd6c73248940a34 Mon Sep 17 00:00:00 2001 From: aditya pimpalkar Date: Fri, 1 Mar 2024 00:01:52 +0000 Subject: [PATCH 12/20] after merge --- .../twenty-server/src/database/typeorm-seeds/core/demo/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/demo/index.ts b/packages/twenty-server/src/database/typeorm-seeds/core/demo/index.ts index 1a3a12a06aa6..14587fca4720 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/core/demo/index.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/core/demo/index.ts @@ -9,7 +9,6 @@ import { deleteWorkspaces, } from 'src/database/typeorm-seeds/core/demo/workspaces'; import { - seedFeatureFlags, deleteFeatureFlags, } from 'src/database/typeorm-seeds/core/demo/feature-flags'; import { @@ -26,7 +25,6 @@ export const seedCoreSchema = async ( await seedWorkspaces(workspaceDataSource, schemaName, workspaceId); await seedUsers(workspaceDataSource, schemaName, workspaceId); await seedUserWorkspaces(workspaceDataSource, schemaName, workspaceId); - await seedFeatureFlags(workspaceDataSource, schemaName, workspaceId); }; export const deleteCoreSchema = async ( From f78a9309fdb302a1650b3f5e80aa96e1596a98ca Mon Sep 17 00:00:00 2001 From: aditya pimpalkar Date: Fri, 1 Mar 2024 15:04:30 +0000 Subject: [PATCH 13/20] requested changes --- packages/twenty-front/src/generated/graphql.tsx | 7 ++++--- packages/twenty-server/src/core/auth/auth.resolver.ts | 5 ----- .../twenty-server/src/core/auth/services/auth.service.ts | 2 +- .../src/core/auth/services/token.service.ts | 8 +------- .../src/core/user-workspace/user-workspace.entity.ts | 6 +++--- .../src/core/user-workspace/user-workspace.service.ts | 9 --------- packages/twenty-server/src/core/user/user.entity.ts | 8 ++------ packages/twenty-server/src/core/user/user.resolver.ts | 4 +--- .../src/core/workspace/services/workspace.service.ts | 8 -------- .../twenty-server/src/core/workspace/workspace.entity.ts | 5 ----- 10 files changed, 12 insertions(+), 50 deletions(-) diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index b3ff39447614..7ef194a37c2b 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -574,6 +574,7 @@ export type User = { createdAt: Scalars['DateTime']; defaultAvatarUrl?: Maybe; defaultWorkspace: Workspace; + defaultWorkspaceId: Scalars['String']; deletedAt?: Maybe; disabled?: Maybe; email: Scalars['String']; @@ -587,7 +588,7 @@ export type User = { supportUserHash?: Maybe; updatedAt: Scalars['DateTime']; workspaceMember?: Maybe; - workspaces?: Maybe>; + workspaces: Array; }; export type UserEdge = { @@ -611,7 +612,7 @@ export type UserWorkspace = { updatedAt: Scalars['DateTime']; user: User; userId: Scalars['String']; - workspace: Workspace; + workspace?: Maybe; workspaceId: Scalars['String']; }; @@ -935,7 +936,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; -export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null }, workspaces?: Array<{ __typename?: 'UserWorkspace', workspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null } }> | null } }; +export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null } | null }> } }; export type ActivateWorkspaceMutationVariables = Exact<{ input: ActivateWorkspaceInput; diff --git a/packages/twenty-server/src/core/auth/auth.resolver.ts b/packages/twenty-server/src/core/auth/auth.resolver.ts index 3cac446dd621..842baa162bd3 100644 --- a/packages/twenty-server/src/core/auth/auth.resolver.ts +++ b/packages/twenty-server/src/core/auth/auth.resolver.ts @@ -128,11 +128,6 @@ export class AuthResolver { const result = await this.authService.verify(email); - if (result.user.id) { - result.user.workspaces = - await this.userWorkspaceService.findUserWorkspaces(result.user.id); - } - return result; } diff --git a/packages/twenty-server/src/core/auth/services/auth.service.ts b/packages/twenty-server/src/core/auth/services/auth.service.ts index 18ac914fb5cd..a712b5e2ec07 100644 --- a/packages/twenty-server/src/core/auth/services/auth.service.ts +++ b/packages/twenty-server/src/core/auth/services/auth.service.ts @@ -98,7 +98,7 @@ export class AuthService { where: { email, }, - relations: ['defaultWorkspace'], + relations: ['defaultWorkspace', 'workspaces'], }); assert(user, "This user doesn't exist", NotFoundException); diff --git a/packages/twenty-server/src/core/auth/services/token.service.ts b/packages/twenty-server/src/core/auth/services/token.service.ts index 7094c81ede96..33bc4a77bd38 100644 --- a/packages/twenty-server/src/core/auth/services/token.service.ts +++ b/packages/twenty-server/src/core/auth/services/token.service.ts @@ -255,13 +255,7 @@ export class TokenService { assert(workspace, 'workspace doesnt exist', NotFoundException); - const userWorkspace = - await this.userWorkspaceService.checkUserWorkspaceExists( - user.id, - workspace.id, - ); - - assert(userWorkspace, 'cannot access workspace', ForbiddenException); + assert(workspace.users.map((u) => u.id).includes(user.id), 'user does not belong to workspace', ForbiddenException); await this.userRepository.save({ id: user.id, diff --git a/packages/twenty-server/src/core/user-workspace/user-workspace.entity.ts b/packages/twenty-server/src/core/user-workspace/user-workspace.entity.ts index 369894ee9daa..58f97710bac8 100644 --- a/packages/twenty-server/src/core/user-workspace/user-workspace.entity.ts +++ b/packages/twenty-server/src/core/user-workspace/user-workspace.entity.ts @@ -24,7 +24,7 @@ export class UserWorkspace { id: string; @Field(() => User) - @ManyToOne(() => User, (user) => user.workspaceUsers, { + @ManyToOne(() => User, (user) => user.workspaces, { onDelete: 'CASCADE', }) @JoinColumn({ name: 'userId' }) @@ -34,8 +34,8 @@ export class UserWorkspace { @Column() userId: string; - @Field(() => Workspace) - @ManyToOne(() => Workspace, (workspace) => workspace.workspaceUsers, { + @Field(() => Workspace, { nullable: true }) + @ManyToOne(() => Workspace, (workspace) => workspace.users, { onDelete: 'CASCADE', }) @JoinColumn({ name: 'workspaceId' }) diff --git a/packages/twenty-server/src/core/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/core/user-workspace/user-workspace.service.ts index 92078584b98f..6e7b41c6a2f7 100644 --- a/packages/twenty-server/src/core/user-workspace/user-workspace.service.ts +++ b/packages/twenty-server/src/core/user-workspace/user-workspace.service.ts @@ -45,15 +45,6 @@ export class UserWorkspaceService extends TypeOrmQueryService { ); } - async findUserWorkspaces(userId: string): Promise { - return this.userWorkspaceRepository.find({ - where: { - userId, - }, - relations: ['workspace'], - }); - } - async checkUserWorkspaceExists( userId: string, workspaceId: string, diff --git a/packages/twenty-server/src/core/user/user.entity.ts b/packages/twenty-server/src/core/user/user.entity.ts index 3487f3ff7aa5..e81c438ed791 100644 --- a/packages/twenty-server/src/core/user/user.entity.ts +++ b/packages/twenty-server/src/core/user/user.entity.ts @@ -93,11 +93,7 @@ export class User { @Field(() => WorkspaceMember, { nullable: true }) workspaceMember: WorkspaceMember; - @OneToMany(() => UserWorkspace, (userWorkspace) => userWorkspace.user, { - onDelete: 'CASCADE', - }) - workspaceUsers: UserWorkspace[]; - - @Field(() => [UserWorkspace], { nullable: true }) + @Field(() => [UserWorkspace]) + @OneToMany(() => UserWorkspace, (userWorkspace) => userWorkspace.user) workspaces: UserWorkspace[]; } diff --git a/packages/twenty-server/src/core/user/user.resolver.ts b/packages/twenty-server/src/core/user/user.resolver.ts index b069b45f3f10..10e9f838989a 100644 --- a/packages/twenty-server/src/core/user/user.resolver.ts +++ b/packages/twenty-server/src/core/user/user.resolver.ts @@ -48,13 +48,11 @@ export class UserResolver { @Query(() => User) async currentUser(@AuthUser() { id }: User): Promise { const user = await this.userService.findById(id, { - relations: [{ name: 'defaultWorkspace', query: {} }], + relations: [{ name: 'defaultWorkspace', query: {} }, { name: 'workspaces', query: {} }], }); assert(user, 'User not found'); - user.workspaces = await this.userWorkspaceService.findUserWorkspaces(id); - return user; } diff --git a/packages/twenty-server/src/core/workspace/services/workspace.service.ts b/packages/twenty-server/src/core/workspace/services/workspace.service.ts index bcaa65295423..94fa543f1fbb 100644 --- a/packages/twenty-server/src/core/workspace/services/workspace.service.ts +++ b/packages/twenty-server/src/core/workspace/services/workspace.service.ts @@ -51,14 +51,6 @@ export class WorkspaceService extends TypeOrmQueryService { await this.userWorkspaceRepository.delete({ workspaceId: id }); - const users = await this.userRepository.find({ - where: { - defaultWorkspaceId: workspace.id, - }, - }); - - await this.userRepository.remove(users); - await this.workspaceManagerService.delete(id); if (shouldDeleteCoreWorkspace) { await this.workspaceRepository.delete(id); diff --git a/packages/twenty-server/src/core/workspace/workspace.entity.ts b/packages/twenty-server/src/core/workspace/workspace.entity.ts index fc7d4d0a3096..26ea05d87224 100644 --- a/packages/twenty-server/src/core/workspace/workspace.entity.ts +++ b/packages/twenty-server/src/core/workspace/workspace.entity.ts @@ -55,11 +55,6 @@ export class Workspace { @OneToMany(() => User, (user) => user.defaultWorkspace) users: User[]; - @OneToMany(() => UserWorkspace, (userWorkspace) => userWorkspace.workspace, { - onDelete: 'CASCADE', - }) - workspaceUsers: UserWorkspace[]; - @Field() @Column({ default: true }) allowImpersonation: boolean; From 88e65443847c7b235a64d59b64effd69d6e2da0d Mon Sep 17 00:00:00 2001 From: aditya pimpalkar Date: Fri, 1 Mar 2024 15:16:15 +0000 Subject: [PATCH 14/20] :wq! --- .vscode/settings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 8431fcdba733..c57b880bcd56 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,21 +4,21 @@ "[typescript]": { "editor.formatOnSave": false, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true, + "source.fixAll.eslint": "explicit", "source.addMissingImports": "always" } }, "[javascript]": { "editor.formatOnSave": false, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true, + "source.fixAll.eslint": "explicit", "source.addMissingImports": "always" } }, "[typescriptreact]": { "editor.formatOnSave": false, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true, + "source.fixAll.eslint": "explicit", "source.addMissingImports": "always" } }, From d652afa3ba64df87cf02dc5a61231e9f9be4c957 Mon Sep 17 00:00:00 2001 From: aditya pimpalkar Date: Fri, 1 Mar 2024 15:45:00 +0000 Subject: [PATCH 15/20] workspace manytoone relation --- .../src/core/user-workspace/user-workspace.entity.ts | 2 +- .../twenty-server/src/core/workspace/workspace.entity.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/twenty-server/src/core/user-workspace/user-workspace.entity.ts b/packages/twenty-server/src/core/user-workspace/user-workspace.entity.ts index 58f97710bac8..b8e2a5e063f8 100644 --- a/packages/twenty-server/src/core/user-workspace/user-workspace.entity.ts +++ b/packages/twenty-server/src/core/user-workspace/user-workspace.entity.ts @@ -35,7 +35,7 @@ export class UserWorkspace { userId: string; @Field(() => Workspace, { nullable: true }) - @ManyToOne(() => Workspace, (workspace) => workspace.users, { + @ManyToOne(() => Workspace, (workspace) => workspace.workspaceUsers, { onDelete: 'CASCADE', }) @JoinColumn({ name: 'workspaceId' }) diff --git a/packages/twenty-server/src/core/workspace/workspace.entity.ts b/packages/twenty-server/src/core/workspace/workspace.entity.ts index 26ea05d87224..fc7d4d0a3096 100644 --- a/packages/twenty-server/src/core/workspace/workspace.entity.ts +++ b/packages/twenty-server/src/core/workspace/workspace.entity.ts @@ -55,6 +55,11 @@ export class Workspace { @OneToMany(() => User, (user) => user.defaultWorkspace) users: User[]; + @OneToMany(() => UserWorkspace, (userWorkspace) => userWorkspace.workspace, { + onDelete: 'CASCADE', + }) + workspaceUsers: UserWorkspace[]; + @Field() @Column({ default: true }) allowImpersonation: boolean; From 0a7f69e486042d531dd351fbba5f992cf0548e57 Mon Sep 17 00:00:00 2001 From: aditya pimpalkar Date: Fri, 1 Mar 2024 17:58:04 +0000 Subject: [PATCH 16/20] lint fix / import fix --- .../src/core/auth/services/token.service.spec.ts | 4 ++-- .../twenty-server/src/core/auth/services/token.service.ts | 6 +++++- .../src/core/user/services/user.service.spec.ts | 5 ----- .../twenty-server/src/core/user/services/user.service.ts | 5 ----- packages/twenty-server/src/core/user/user.resolver.ts | 5 ++++- .../src/core/workspace/services/workspace.service.spec.ts | 1 - .../src/database/typeorm-seeds/core/demo/index.ts | 4 +--- 7 files changed, 12 insertions(+), 18 deletions(-) diff --git a/packages/twenty-server/src/core/auth/services/token.service.spec.ts b/packages/twenty-server/src/core/auth/services/token.service.spec.ts index de11a8550f1e..80401f79247d 100644 --- a/packages/twenty-server/src/core/auth/services/token.service.spec.ts +++ b/packages/twenty-server/src/core/auth/services/token.service.spec.ts @@ -7,11 +7,11 @@ import { RefreshToken } from 'src/core/refresh-token/refresh-token.entity'; import { User } from 'src/core/user/user.entity'; import { JwtAuthStrategy } from 'src/core/auth/strategies/jwt.auth.strategy'; import { EmailService } from 'src/integrations/email/email.service'; - -import { TokenService } from './token.service'; import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service'; import { Workspace } from 'src/core/workspace/workspace.entity'; +import { TokenService } from './token.service'; + describe('TokenService', () => { let service: TokenService; diff --git a/packages/twenty-server/src/core/auth/services/token.service.ts b/packages/twenty-server/src/core/auth/services/token.service.ts index 33bc4a77bd38..cdec26c37eea 100644 --- a/packages/twenty-server/src/core/auth/services/token.service.ts +++ b/packages/twenty-server/src/core/auth/services/token.service.ts @@ -255,7 +255,11 @@ export class TokenService { assert(workspace, 'workspace doesnt exist', NotFoundException); - assert(workspace.users.map((u) => u.id).includes(user.id), 'user does not belong to workspace', ForbiddenException); + assert( + workspace.workspaceUsers.map((u) => u.id).includes(user.id), + 'user does not belong to workspace', + ForbiddenException, + ); await this.userRepository.save({ id: user.id, diff --git a/packages/twenty-server/src/core/user/services/user.service.spec.ts b/packages/twenty-server/src/core/user/services/user.service.spec.ts index 97cd67f87d41..fd59dc7d2067 100644 --- a/packages/twenty-server/src/core/user/services/user.service.spec.ts +++ b/packages/twenty-server/src/core/user/services/user.service.spec.ts @@ -4,7 +4,6 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { User } from 'src/core/user/user.entity'; import { DataSourceService } from 'src/metadata/data-source/data-source.service'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; -import { UserWorkspace } from 'src/core/user-workspace/user-workspace.entity'; import { UserService } from './user.service'; @@ -19,10 +18,6 @@ describe('UserService', () => { provide: getRepositoryToken(User, 'core'), useValue: {}, }, - { - provide: getRepositoryToken(UserWorkspace, 'core'), - useValue: {}, - }, { provide: DataSourceService, useValue: {}, diff --git a/packages/twenty-server/src/core/user/services/user.service.ts b/packages/twenty-server/src/core/user/services/user.service.ts index ed6bc8b8f70c..c088176a291d 100644 --- a/packages/twenty-server/src/core/user/services/user.service.ts +++ b/packages/twenty-server/src/core/user/services/user.service.ts @@ -9,14 +9,11 @@ import { WorkspaceMember } from 'src/core/user/dtos/workspace-member.dto'; import { DataSourceService } from 'src/metadata/data-source/data-source.service'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity'; -import { UserWorkspace } from 'src/core/user-workspace/user-workspace.entity'; export class UserService extends TypeOrmQueryService { constructor( @InjectRepository(User, 'core') private readonly userRepository: Repository, - @InjectRepository(UserWorkspace, 'core') - private readonly userWorkspaceRepository: Repository, private readonly dataSourceService: DataSourceService, private readonly typeORMService: TypeORMService, ) { @@ -107,8 +104,6 @@ export class UserService extends TypeOrmQueryService { `DELETE FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "userId" = '${userId}'`, ); - await this.userWorkspaceRepository.delete({ userId }); - await this.userRepository.delete(user.id); return user; diff --git a/packages/twenty-server/src/core/user/user.resolver.ts b/packages/twenty-server/src/core/user/user.resolver.ts index 10e9f838989a..f664d3f35030 100644 --- a/packages/twenty-server/src/core/user/user.resolver.ts +++ b/packages/twenty-server/src/core/user/user.resolver.ts @@ -48,7 +48,10 @@ export class UserResolver { @Query(() => User) async currentUser(@AuthUser() { id }: User): Promise { const user = await this.userService.findById(id, { - relations: [{ name: 'defaultWorkspace', query: {} }, { name: 'workspaces', query: {} }], + relations: [ + { name: 'defaultWorkspace', query: {} }, + { name: 'workspaces', query: {} }, + ], }); assert(user, 'User not found'); diff --git a/packages/twenty-server/src/core/workspace/services/workspace.service.spec.ts b/packages/twenty-server/src/core/workspace/services/workspace.service.spec.ts index 78c68f26b2e2..2ee3828fbdb0 100644 --- a/packages/twenty-server/src/core/workspace/services/workspace.service.spec.ts +++ b/packages/twenty-server/src/core/workspace/services/workspace.service.spec.ts @@ -3,7 +3,6 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { Workspace } from 'src/core/workspace/workspace.entity'; import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service'; -import { UserService } from 'src/core/user/services/user.service'; import { UserWorkspace } from 'src/core/user-workspace/user-workspace.entity'; import { User } from 'src/core/user/user.entity'; import { BillingService } from 'src/core/billing/billing.service'; diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/demo/index.ts b/packages/twenty-server/src/database/typeorm-seeds/core/demo/index.ts index 14587fca4720..3fa7d8b270bc 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/core/demo/index.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/core/demo/index.ts @@ -8,9 +8,7 @@ import { seedWorkspaces, deleteWorkspaces, } from 'src/database/typeorm-seeds/core/demo/workspaces'; -import { - deleteFeatureFlags, -} from 'src/database/typeorm-seeds/core/demo/feature-flags'; +import { deleteFeatureFlags } from 'src/database/typeorm-seeds/core/demo/feature-flags'; import { deleteUserWorkspaces, seedUserWorkspaces, From d74758d19f5d6271478997304997f7f8894b0196 Mon Sep 17 00:00:00 2001 From: aditya pimpalkar Date: Fri, 1 Mar 2024 18:09:19 +0000 Subject: [PATCH 17/20] gql codegen --- packages/twenty-front/src/generated/graphql.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 7ef194a37c2b..e698c1189d3d 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -607,7 +607,7 @@ export type UserExists = { export type UserWorkspace = { __typename?: 'UserWorkspace'; createdAt: Scalars['DateTime']; - deletedAt: Scalars['DateTime']; + deletedAt?: Maybe; id: Scalars['ID']; updatedAt: Scalars['DateTime']; user: User; From f0f03ccec655515a14b6891b91bdfea7f8ad2d13 Mon Sep 17 00:00:00 2001 From: martmull Date: Fri, 1 Mar 2024 19:16:44 +0100 Subject: [PATCH 18/20] Fix migrations and generateJWT --- .../src/core/auth/services/token.service.ts | 9 ++++--- .../1709314035408-updateUserWorkspace.ts | 27 +++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/1709314035408-updateUserWorkspace.ts diff --git a/packages/twenty-server/src/core/auth/services/token.service.ts b/packages/twenty-server/src/core/auth/services/token.service.ts index cdec26c37eea..825caef41ce1 100644 --- a/packages/twenty-server/src/core/auth/services/token.service.ts +++ b/packages/twenty-server/src/core/auth/services/token.service.ts @@ -249,14 +249,17 @@ export class TokenService { assert(userExists, 'User not found', NotFoundException); - const workspace = await this.workspaceRepository.findOneBy({ - id: workspaceId, + const workspace = await this.workspaceRepository.findOne({ + where: { id: workspaceId }, + relations: ['workspaceUsers'], }); assert(workspace, 'workspace doesnt exist', NotFoundException); assert( - workspace.workspaceUsers.map((u) => u.id).includes(user.id), + workspace.workspaceUsers + .map((userWorkspace) => userWorkspace.userId) + .includes(user.id), 'user does not belong to workspace', ForbiddenException, ); diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/1709314035408-updateUserWorkspace.ts b/packages/twenty-server/src/database/typeorm/core/migrations/1709314035408-updateUserWorkspace.ts new file mode 100644 index 000000000000..d71fea077e76 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/1709314035408-updateUserWorkspace.ts @@ -0,0 +1,27 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdateUserWorkspace1709314035408 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "core"."user" + ADD CONSTRAINT "FK_37fdc7357af701e595c5c3a9bd6" + FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") + ON DELETE CASCADE ON UPDATE NO ACTION + `); + await queryRunner.query(` + ALTER TABLE "core"."userWorkspace" + ADD CONSTRAINT "FK_cb488f32c6a0827b938edadf221" + FOREIGN KEY ("userId") REFERENCES "core"."user"("id") + ON DELETE CASCADE ON UPDATE NO ACTION + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" DROP CONSTRAINT "FK_cb488f32c6a0827b938edadf221"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."userWorkspace" DROP CONSTRAINT "FK_37fdc7357af701e595c5c3a9bd6"`, + ); + } +} From 2eed6782a20dfe445a3991b149279405d854f566 Mon Sep 17 00:00:00 2001 From: aditya pimpalkar Date: Fri, 1 Mar 2024 19:52:57 +0000 Subject: [PATCH 19/20] migration fix --- .../core/migrations/1709314035408-updateUserWorkspace.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/1709314035408-updateUserWorkspace.ts b/packages/twenty-server/src/database/typeorm/core/migrations/1709314035408-updateUserWorkspace.ts index d71fea077e76..b3bd23110bdd 100644 --- a/packages/twenty-server/src/database/typeorm/core/migrations/1709314035408-updateUserWorkspace.ts +++ b/packages/twenty-server/src/database/typeorm/core/migrations/1709314035408-updateUserWorkspace.ts @@ -1,9 +1,10 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; export class UpdateUserWorkspace1709314035408 implements MigrationInterface { + name = 'UpdateUserWorkspace1709314035408'; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` - ALTER TABLE "core"."user" + ALTER TABLE "core"."userWorkspace" ADD CONSTRAINT "FK_37fdc7357af701e595c5c3a9bd6" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION From 39df75d1aef12727e0817fa0dbaef2ff9154259d Mon Sep 17 00:00:00 2001 From: aditya pimpalkar Date: Sat, 2 Mar 2024 23:43:40 +0000 Subject: [PATCH 20/20] relations fix --- .../src/core/auth/services/auth.service.ts | 2 +- .../twenty-server/src/core/user/user.resolver.ts | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/twenty-server/src/core/auth/services/auth.service.ts b/packages/twenty-server/src/core/auth/services/auth.service.ts index a712b5e2ec07..9ebbd063b6cb 100644 --- a/packages/twenty-server/src/core/auth/services/auth.service.ts +++ b/packages/twenty-server/src/core/auth/services/auth.service.ts @@ -98,7 +98,7 @@ export class AuthService { where: { email, }, - relations: ['defaultWorkspace', 'workspaces'], + relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'], }); assert(user, "This user doesn't exist", NotFoundException); diff --git a/packages/twenty-server/src/core/user/user.resolver.ts b/packages/twenty-server/src/core/user/user.resolver.ts index f664d3f35030..431a5336af96 100644 --- a/packages/twenty-server/src/core/user/user.resolver.ts +++ b/packages/twenty-server/src/core/user/user.resolver.ts @@ -26,6 +26,8 @@ import { WorkspaceMember } from 'src/core/user/dtos/workspace-member.dto'; import { UserWorkspaceService } from 'src/core/user-workspace/user-workspace.service'; import { UserService } from './services/user.service'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; const getHMACKey = (email?: string, key?: string | null) => { if (!email || !key) return null; @@ -39,6 +41,8 @@ const getHMACKey = (email?: string, key?: string | null) => { @Resolver(() => User) export class UserResolver { constructor( + @InjectRepository(User, 'core') + private readonly userRepository: Repository, private readonly userService: UserService, private readonly userWorkspaceService: UserWorkspaceService, private readonly environmentService: EnvironmentService, @@ -47,11 +51,11 @@ export class UserResolver { @Query(() => User) async currentUser(@AuthUser() { id }: User): Promise { - const user = await this.userService.findById(id, { - relations: [ - { name: 'defaultWorkspace', query: {} }, - { name: 'workspaces', query: {} }, - ], + const user = await this.userRepository.findOne({ + where: { + id, + }, + relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'], }); assert(user, 'User not found');