diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 18141a155d3..8dd1145363c 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -546,7 +546,7 @@ export type User = { canImpersonate: Scalars['Boolean']; createdAt: Scalars['DateTime']; defaultAvatarUrl?: Maybe; - defaultWorkspace?: Maybe; + defaultWorkspace: Workspace; deletedAt?: Maybe; disabled?: Maybe; email: Scalars['String']; @@ -790,7 +790,7 @@ export type ImpersonateMutationVariables = Exact<{ }>; -export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', 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, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } | null }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; +export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', 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, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; export type RenewTokenMutationVariables = Exact<{ refreshToken: Scalars['String']; @@ -821,7 +821,7 @@ export type VerifyMutationVariables = Exact<{ }>; -export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', 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, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } | null }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; +export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', 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, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; export type CheckUserExistsQueryVariables = Exact<{ email: Scalars['String']; @@ -858,7 +858,7 @@ export type UploadImageMutationVariables = Exact<{ export type UploadImageMutation = { __typename?: 'Mutation', uploadImage: string }; -export type UserQueryFragmentFragment = { __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, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } | null }; +export type UserQueryFragmentFragment = { __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, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } }; export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>; @@ -877,7 +877,7 @@ export type CreateWorkspaceMutationVariables = Exact<{ }>; -export type CreateWorkspaceMutation = { __typename?: 'Mutation', createWorkspace: { __typename?: 'User', id: string, 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, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } | null } }; +export type CreateWorkspaceMutation = { __typename?: 'Mutation', createWorkspace: { __typename?: 'User', id: string, 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, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } } }; export type DeleteCurrentWorkspaceMutationVariables = Exact<{ [key: string]: never; }>; diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx index 9843b28c155..75fa7376875 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx @@ -141,13 +141,12 @@ export const useSignInUp = () => { if ( billing?.isBillingEnabled && - (!currentWorkspace || - currentWorkspace.subscriptionStatus !== 'active') + currentWorkspace.subscriptionStatus !== 'active' ) { navigate(AppPath.PlanRequired); return; } - if (currentWorkspace?.displayName) { + if (currentWorkspace.displayName) { navigate(AppPath.Index); return; } diff --git a/packages/twenty-front/src/modules/auth/utils/getOnboardingStatus.ts b/packages/twenty-front/src/modules/auth/utils/getOnboardingStatus.ts index e6c7b081f04..2b5cabb3fdf 100644 --- a/packages/twenty-front/src/modules/auth/utils/getOnboardingStatus.ts +++ b/packages/twenty-front/src/modules/auth/utils/getOnboardingStatus.ts @@ -27,14 +27,6 @@ export const getOnboardingStatus = ({ if (!isLoggedIn) { return OnboardingStatus.OngoingUserCreation; } - // if the user has not been fetched yet, we can't know the onboarding status - if (currentWorkspace && !currentWorkspaceMember) { - return undefined; - } - - if (!currentWorkspace && isBillingEnabled) { - return OnboardingStatus.Incomplete; - } if ( isBillingEnabled && @@ -50,9 +42,12 @@ export const getOnboardingStatus = ({ if (!currentWorkspace?.displayName) { return OnboardingStatus.OngoingWorkspaceCreation; } + if (!currentWorkspaceMember) { + return undefined; + } if ( - !currentWorkspaceMember?.name.firstName || - !currentWorkspaceMember?.name.lastName + !currentWorkspaceMember.name.firstName || + !currentWorkspaceMember.name.lastName ) { return OnboardingStatus.OngoingProfileCreation; } diff --git a/packages/twenty-front/src/pages/auth/PasswordReset.tsx b/packages/twenty-front/src/pages/auth/PasswordReset.tsx index b28e361ab70..72d8816950f 100644 --- a/packages/twenty-front/src/pages/auth/PasswordReset.tsx +++ b/packages/twenty-front/src/pages/auth/PasswordReset.tsx @@ -157,13 +157,13 @@ export const PasswordReset = () => { if ( billing?.isBillingEnabled && - (!currentWorkspace || currentWorkspace.subscriptionStatus !== 'active') + currentWorkspace.subscriptionStatus !== 'active' ) { navigate(AppPath.PlanRequired); return; } - if (currentWorkspace?.displayName) { + if (currentWorkspace.displayName) { navigate(AppPath.Index); return; } diff --git a/packages/twenty-server/src/core/api-rest/api-rest-query-builder/api-rest-query-builder.factory.ts b/packages/twenty-server/src/core/api-rest/api-rest-query-builder/api-rest-query-builder.factory.ts index 966bb845aab..df8360cb9fd 100644 --- a/packages/twenty-server/src/core/api-rest/api-rest-query-builder/api-rest-query-builder.factory.ts +++ b/packages/twenty-server/src/core/api-rest/api-rest-query-builder/api-rest-query-builder.factory.ts @@ -1,8 +1,4 @@ -import { - BadRequestException, - Injectable, - UnauthorizedException, -} from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { Request } from 'express'; @@ -46,10 +42,6 @@ export class ApiRestQueryBuilderFactory { }> { const workspace = await this.tokenService.validateToken(request); - if (!workspace) { - throw new UnauthorizedException(); - } - const objectMetadataItems = await this.objectMetadataService.findManyWithinWorkspace(workspace.id); 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 38202265c4c..946bb698569 100644 --- a/packages/twenty-server/src/core/auth/services/auth.service.ts +++ b/packages/twenty-server/src/core/auth/services/auth.service.ts @@ -108,7 +108,7 @@ export class AuthService { } const passwordHash = password ? await hashPassword(password) : undefined; - let workspace: Workspace | null = null; + let workspace: Workspace | null; if (workspaceInviteHash) { workspace = await this.workspaceRepository.findOneBy({ @@ -126,6 +126,15 @@ export class AuthService { 'Sign up is disabled', ForbiddenException, ); + + const workspaceToCreate = this.workspaceRepository.create({ + displayName: '', + domainName: '', + inviteHash: v4(), + subscriptionStatus: 'incomplete', + }); + + workspace = await this.workspaceRepository.save(workspaceToCreate); } let imagePath: string | undefined = undefined; @@ -155,7 +164,7 @@ export class AuthService { defaultAvatarUrl: imagePath, canImpersonate: false, passwordHash, - defaultWorkspace: workspace || undefined, + defaultWorkspace: workspace, }); return await this.userRepository.save(userToCreate); @@ -166,12 +175,24 @@ export class AuthService { where: { email, }, + relations: ['defaultWorkspace'], }); assert(user, "This user doesn't exist", NotFoundException); + assert( + user.defaultWorkspace, + 'User has no default workspace', + NotFoundException, + ); + // passwordHash is hidden for security reasons user.passwordHash = ''; + const workspaceMember = await this.userService.loadWorkspaceMember(user); + + if (workspaceMember) { + user.workspaceMember = workspaceMember; + } const accessToken = await this.tokenService.generateAccessToken(user.id); const refreshToken = await this.tokenService.generateRefreshToken(user.id); 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 dce0d750f8a..8fbc41bfb0e 100644 --- a/packages/twenty-server/src/core/auth/services/token.service.ts +++ b/packages/twenty-server/src/core/auth/services/token.service.ts @@ -68,9 +68,13 @@ export class TokenService { throw new NotFoundException('User is not found'); } + if (!user.defaultWorkspace) { + throw new NotFoundException('User does not have a default workspace'); + } + const jwtPayload: JwtPayload = { sub: user.id, - workspaceId: user.defaultWorkspace?.id || undefined, + workspaceId: user.defaultWorkspace.id, }; return { @@ -188,7 +192,7 @@ export class TokenService { return !!token; } - async validateToken(request: Request): Promise { + async validateToken(request: Request): Promise { const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request); if (!token) { diff --git a/packages/twenty-server/src/core/auth/strategies/jwt.auth.strategy.ts b/packages/twenty-server/src/core/auth/strategies/jwt.auth.strategy.ts index e43f3b9ff8e..437f0ac8e00 100644 --- a/packages/twenty-server/src/core/auth/strategies/jwt.auth.strategy.ts +++ b/packages/twenty-server/src/core/auth/strategies/jwt.auth.strategy.ts @@ -16,12 +16,8 @@ import { User } from 'src/core/user/user.entity'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { DataSourceService } from 'src/metadata/data-source/data-source.service'; -export type JwtPayload = { - sub: string; - workspaceId?: string; - jti?: string; -}; -export type PassportUser = { user?: User; workspace?: Workspace | null }; +export type JwtPayload = { sub: string; workspaceId: string; jti?: string }; +export type PassportUser = { user?: User; workspace: Workspace }; @Injectable() export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') { @@ -46,7 +42,11 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') { id: payload.workspaceId ?? payload.sub, }); - if (workspace && payload.jti) { + if (!workspace) { + throw new UnauthorizedException(); + } + + if (payload.jti) { const dataSourceMetadata = await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( workspace.id, @@ -68,7 +68,7 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') { let user; - if (payload.workspaceId || !workspace) { + if (payload.workspaceId) { user = await this.userRepository.findOne({ where: { id: payload.sub }, relations: ['defaultWorkspace'], diff --git a/packages/twenty-server/src/core/open-api/open-api.service.ts b/packages/twenty-server/src/core/open-api/open-api.service.ts index 15c057b6d47..80409e429cd 100644 --- a/packages/twenty-server/src/core/open-api/open-api.service.ts +++ b/packages/twenty-server/src/core/open-api/open-api.service.ts @@ -32,10 +32,6 @@ export class OpenApiService { try { const workspace = await this.tokenService.validateToken(request); - if (!workspace) { - return schema; - } - objectMetadataItems = await this.objectMetadataService.findManyWithinWorkspace(workspace.id); } catch (err) { 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 1802f69e42c..e9692772ddf 100644 --- a/packages/twenty-server/src/core/user/services/user.service.ts +++ b/packages/twenty-server/src/core/user/services/user.service.ts @@ -2,7 +2,6 @@ import { InjectRepository } from '@nestjs/typeorm'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { Repository } from 'typeorm'; -import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; import { assert } from 'src/utils/assert'; import { User } from 'src/core/user/user.entity'; @@ -88,10 +87,6 @@ export class UserService extends TypeOrmQueryService { ); } - async updateUser(userId: string, data: QueryDeepPartialEntity) { - await this.userRepository.update(userId, data); - } - async getUser(userId: string): Promise { return await this.userRepository.findOneOrFail({ where: { id: userId }, diff --git a/packages/twenty-server/src/core/user/user.entity.ts b/packages/twenty-server/src/core/user/user.entity.ts index 564d051a5d6..903938c133e 100644 --- a/packages/twenty-server/src/core/user/user.entity.ts +++ b/packages/twenty-server/src/core/user/user.entity.ts @@ -66,7 +66,7 @@ export class User { @Column({ nullable: true }) deletedAt: Date; - @Field(() => Workspace, { nullable: true }) + @Field(() => Workspace, { nullable: false }) @ManyToOne(() => Workspace, (workspace) => workspace.users, { onDelete: 'SET NULL', }) 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 ab3181adff7..fde676e39dd 100644 --- a/packages/twenty-server/src/core/workspace/services/workspace.service.ts +++ b/packages/twenty-server/src/core/workspace/services/workspace.service.ts @@ -1,10 +1,10 @@ import { InjectRepository } from '@nestjs/typeorm'; +import { BadRequestException } from '@nestjs/common'; import assert from 'assert'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { Repository } from 'typeorm'; -import { v4 } from 'uuid'; import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service'; import { Workspace } from 'src/core/workspace/workspace.entity'; @@ -23,25 +23,16 @@ export class WorkspaceService extends TypeOrmQueryService { } async createWorkspace(user: User, data: CreateWorkspaceInput) { - const workspaceToCreate = this.workspaceRepository.create({ - displayName: '', - domainName: '', - inviteHash: v4(), - subscriptionStatus: 'active', - ...data, + if (!data.displayName || !data.displayName.length) { + throw new BadRequestException("'displayName' not provided"); + } + await this.workspaceRepository.update(user.defaultWorkspace.id, { + displayName: data.displayName, }); - const workspace = await this.workspaceRepository.save(workspaceToCreate); + await this.workspaceManagerService.init(user.defaultWorkspace.id); + await this.userService.createWorkspaceMember(user); - await this.userService.updateUser(user.id, { - defaultWorkspace: workspace, - }); - - await this.workspaceManagerService.init(workspace.id); - const updatedUser = await this.userService.getUser(user.id); - - await this.userService.createWorkspaceMember(updatedUser); - - return updatedUser; + return await this.userService.getUser(user.id); } async deleteWorkspace(id: string) { diff --git a/packages/twenty-server/src/graphql-config.service.ts b/packages/twenty-server/src/graphql-config.service.ts index 1be7bac829a..aa543c8e4ce 100644 --- a/packages/twenty-server/src/graphql-config.service.ts +++ b/packages/twenty-server/src/graphql-config.service.ts @@ -62,10 +62,6 @@ export class GraphQLConfigService const workspace = await this.tokenService.validateToken(context.req); - if (!workspace) { - return new GraphQLSchema({}); - } - return await this.createSchema(context, workspace); } catch (error) { if (error instanceof UnauthorizedException) {