From b25f50e2881b798d611023dcab9577a3df55077e Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Wed, 16 Jul 2025 18:51:46 +0200 Subject: [PATCH] Rework locale computation on BE (#13247) Context: Users are complaining to see their workspace in a language they don't know. This behavior is transient, happens on data model update and disappear on refresh I've check the cache for users that got the issue and did not spot any weird language ==> I think we somehow fallback the the request header locale. I feel we should always use the userWorkspace.locale, request locale should not be used in BE in my opinion except for unauthenticated endpoints. I'm also adding logs to understand the locale issue In this PR: rename user.workspaces into user.userWorkspaces which is more correct improve / simplify LOCALES typing --- .../src/generated-metadata/graphql.ts | 1 + .../twenty-front/src/generated/graphql.ts | 1 + ...case-user-and-invitation-emails.command.ts | 2 +- .../hooks/use-cached-metadata.ts | 43 +++++++++++++------ .../__tests__/admin-panel.service.spec.ts | 6 +-- .../admin-panel/admin-panel.service.ts | 12 +++--- .../approved-access-domain.service.ts | 3 +- .../engine/core-modules/auth/auth.resolver.ts | 3 +- .../auth/controllers/sso-auth.controller.ts | 2 +- .../auth/services/auth.service.ts | 22 +++++++--- .../email-verification.resolver.ts | 1 + .../core-modules/sso/services/sso.service.ts | 2 +- .../user-workspace/user-workspace.entity.ts | 7 +-- .../user-workspace.service.spec.ts | 38 ++++++++-------- .../user-workspace/user-workspace.service.ts | 22 +++++----- .../user/services/user.service.ts | 8 ++-- .../engine/core-modules/user/user.entity.ts | 2 +- .../engine/core-modules/user/user.resolver.ts | 11 ++++- .../services/workspace-invitation.service.ts | 7 ++- .../engine/middlewares/middleware.service.ts | 4 +- 20 files changed, 119 insertions(+), 78 deletions(-) diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 518b7672ee..299a09614c 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -2727,6 +2727,7 @@ export type User = { supportUserHash?: Maybe; updatedAt: Scalars['DateTime']; userVars?: Maybe; + userWorkspaces: Array; workspaceMember?: Maybe; workspaceMembers?: Maybe>; workspaces: Array; diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 72e911bc86..7e7bb76f65 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -2565,6 +2565,7 @@ export type User = { supportUserHash?: Maybe; updatedAt: Scalars['DateTime']; userVars?: Maybe; + userWorkspaces: Array; workspaceMember?: Maybe; workspaceMembers?: Maybe>; workspaces: Array; diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/0-54/0-54-lowercase-user-and-invitation-emails.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/0-54/0-54-lowercase-user-and-invitation-emails.command.ts index 6e04d4c0ba..a91782840d 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/0-54/0-54-lowercase-user-and-invitation-emails.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/0-54/0-54-lowercase-user-and-invitation-emails.command.ts @@ -49,7 +49,7 @@ export class LowercaseUserAndInvitationEmailsCommand extends ActiveOrSuspendedWo private async lowercaseUserEmails(workspaceId: string, dryRun: boolean) { const users = await this.userRepository.find({ where: { - workspaces: { + userWorkspaces: { workspaceId, }, email: Raw((alias) => `LOWER(${alias}) != ${alias}`), diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-config/hooks/use-cached-metadata.ts b/packages/twenty-server/src/engine/api/graphql/graphql-config/hooks/use-cached-metadata.ts index 24695ff3fe..8b0296763b 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-config/hooks/use-cached-metadata.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-config/hooks/use-cached-metadata.ts @@ -1,7 +1,10 @@ import { createHash } from 'crypto'; -import { isNonEmptyString } from '@sniptt/guards'; +import { Request } from 'express'; import { Plugin } from 'graphql-yoga'; +import { isDefined } from 'twenty-shared/utils'; + +import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; export type CacheMetadataPluginConfig = { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -12,19 +15,26 @@ export type CacheMetadataPluginConfig = { }; export function useCachedMetadata(config: CacheMetadataPluginConfig): Plugin { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const computeCacheKey = (serverContext: any) => { - const workspaceId = serverContext.req.workspace?.id ?? 'anonymous'; - const workspaceMetadataVersion = - serverContext.req.workspaceMetadataVersion ?? '0'; - const operationName = getOperationName(serverContext); - const locale = serverContext.req.locale; - const localeCacheKey = isNonEmptyString(locale) ? `:${locale}` : ''; + const computeCacheKey = ({ + operationName, + request, + }: { + operationName: string; + request: Pick; + }) => { + const workspace = request.workspace; + + if (!isDefined(workspace)) { + throw new InternalServerError('Workspace is not defined'); + } + + const workspaceMetadataVersion = workspace.metadataVersion ?? '0'; + const locale = request.locale; const queryHash = createHash('sha256') - .update(serverContext.req.body.query) + .update(request.body.query) .digest('hex'); - return `graphql:operations:${operationName}:${workspaceId}:${workspaceMetadataVersion}${localeCacheKey}:${queryHash}`; + return `graphql:operations:${operationName}:${workspace.id}:${workspaceMetadataVersion}:${locale}:${queryHash}`; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -37,7 +47,11 @@ export function useCachedMetadata(config: CacheMetadataPluginConfig): Plugin { return; } - const cacheKey = computeCacheKey(serverContext); + const cacheKey = computeCacheKey({ + operationName: getOperationName(serverContext), + // TODO: we should probably override the graphql-yoga request type to include the workspace and locale + request: (serverContext as unknown as { req: Request }).req, + }); const cachedResponse = await config.cacheGetter(cacheKey); if (cachedResponse) { @@ -51,7 +65,10 @@ export function useCachedMetadata(config: CacheMetadataPluginConfig): Plugin { return; } - const cacheKey = computeCacheKey(serverContext); + const cacheKey = computeCacheKey({ + operationName: getOperationName(serverContext), + request: (serverContext as unknown as { req: Request }).req, + }); const cachedResponse = await config.cacheGetter(cacheKey); diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/__tests__/admin-panel.service.spec.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/__tests__/admin-panel.service.spec.ts index dc12a7c23e..ee43035916 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/__tests__/admin-panel.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/__tests__/admin-panel.service.spec.ts @@ -91,7 +91,7 @@ describe('AdminPanelService', () => { const mockUser = { id: 'user-id', email: 'user@example.com', - workspaces: [ + userWorkspaces: [ { workspace: { id: 'workspace-id', @@ -114,12 +114,12 @@ describe('AdminPanelService', () => { expect.objectContaining({ where: expect.objectContaining({ id: 'user-id', - workspaces: { + userWorkspaces: { workspaceId: 'workspace-id', workspace: { allowImpersonation: true }, }, }), - relations: ['workspaces', 'workspaces.workspace'], + relations: { userWorkspaces: { workspace: true } }, }), ); diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts index 9b536cc369..6bbf0ff925 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts @@ -39,14 +39,14 @@ export class AdminPanelService { const user = await this.userRepository.findOne({ where: { id: userId, - workspaces: { + userWorkspaces: { workspaceId, workspace: { allowImpersonation: true, }, }, }, - relations: ['workspaces', 'workspaces.workspace'], + relations: { userWorkspaces: { workspace: true } }, }); userValidator.assertIsDefinedOrThrow( @@ -59,14 +59,14 @@ export class AdminPanelService { const loginToken = await this.loginTokenService.generateLoginToken( user.email, - user.workspaces[0].workspace.id, + user.userWorkspaces[0].workspace.id, ); return { workspace: { - id: user.workspaces[0].workspace.id, + id: user.userWorkspaces[0].workspace.id, workspaceUrls: this.domainManagerService.getWorkspaceUrls( - user.workspaces[0].workspace, + user.userWorkspaces[0].workspace, ), }, loginToken, @@ -101,7 +101,7 @@ export class AdminPanelService { firstName: targetUser.firstName, lastName: targetUser.lastName, }, - workspaces: targetUser.workspaces.map((userWorkspace) => ({ + workspaces: targetUser.userWorkspaces.map((userWorkspace) => ({ id: userWorkspace.workspace.id, name: userWorkspace.workspace.displayName ?? '', totalUsers: userWorkspace.workspace.workspaceUsers.length, diff --git a/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.service.ts b/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.service.ts index 1e2bc929aa..0e8297f8d5 100644 --- a/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.service.ts +++ b/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.service.ts @@ -6,7 +6,6 @@ import crypto from 'crypto'; import { t } from '@lingui/core/macro'; import { render } from '@react-email/render'; import { SendApprovedAccessDomainValidation } from 'twenty-emails'; -import { APP_LOCALES } from 'twenty-shared/translations'; import { Repository } from 'typeorm'; import { ApprovedAccessDomain as ApprovedAccessDomainEntity } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity'; @@ -78,7 +77,7 @@ export class ApprovedAccessDomainService { lastName: sender.name.lastName, }, serverUrl: this.twentyConfigService.get('SERVER_URL'), - locale: 'en' as keyof typeof APP_LOCALES, + locale: 'en', }); const html = await render(emailTemplate); const text = await render(emailTemplate, { diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts index b4d43a6234..625719bda6 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts @@ -528,14 +528,13 @@ export class AuthResolver { async updatePasswordViaResetToken( @Args() { passwordResetToken, newPassword }: UpdatePasswordViaResetTokenInput, - @Context() context: I18nContext, ): Promise { const { id } = await this.resetPasswordService.validatePasswordResetToken( passwordResetToken, ); - await this.authService.updatePassword(id, newPassword, context.req.locale); + await this.authService.updatePassword(id, newPassword); return await this.resetPasswordService.invalidatePasswordResetToken(id); } diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts index 9d4f93b973..bddb41ee29 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts @@ -115,7 +115,7 @@ export class SSOAuthController { const workspaceIdentityProvider = await this.workspaceSSOIdentityProviderRepository.findOne({ where: { id: req.user.identityProviderId }, - relations: ['workspace'], + relations: { workspace: true }, }); try { diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts index a11e94a193..91484715c6 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts @@ -9,7 +9,6 @@ import { render } from '@react-email/render'; import { addMilliseconds } from 'date-fns'; import ms from 'ms'; import { PasswordUpdateNotifyEmail } from 'twenty-emails'; -import { APP_LOCALES } from 'twenty-shared/translations'; import { isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; @@ -142,7 +141,7 @@ export class AuthService { where: { email: input.email, }, - relations: ['workspaces'], + relations: { userWorkspaces: true }, }); if (!user) { @@ -424,7 +423,6 @@ export class AuthService { async updatePassword( userId: string, newPassword: string, - locale: keyof typeof APP_LOCALES, ): Promise { if (!userId) { throw new AuthException( @@ -433,7 +431,10 @@ export class AuthService { ); } - const user = await this.userRepository.findOneBy({ id: userId }); + const user = await this.userRepository.findOne({ + where: { id: userId }, + relations: { userWorkspaces: true }, + }); if (!user) { throw new AuthException( @@ -442,6 +443,15 @@ export class AuthService { ); } + const [firstUserWorkspace] = user.userWorkspaces; + + if (!firstUserWorkspace) { + throw new AuthException( + 'User does not have a workspace', + AuthExceptionCode.USER_WORKSPACE_NOT_FOUND, + ); + } + const isPasswordValid = PASSWORD_REGEX.test(newPassword); if (!isPasswordValid) { @@ -461,13 +471,13 @@ export class AuthService { userName: `${user.firstName} ${user.lastName}`, email: user.email, link: this.domainManagerService.getBaseUrl().toString(), - locale, + locale: firstUserWorkspace.locale, }); const html = await render(emailTemplate, { pretty: true }); const text = await render(emailTemplate, { plainText: true }); - i18n.activate(locale); + i18n.activate(firstUserWorkspace.locale); this.emailService.send({ from: `${this.twentyConfigService.get( diff --git a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts index 86c19cdd9c..f5443a8a4e 100644 --- a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts @@ -23,6 +23,7 @@ export class EmailVerificationResolver { private readonly domainManagerService: DomainManagerService, ) {} + // TODO: this should be an authenticated endpoint @Mutation(() => ResendEmailVerificationTokenOutput) @UseGuards(PublicEndpointGuard) async resendEmailVerificationToken( diff --git a/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts index 5d38b00fd4..eb92c554fe 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts +++ b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts @@ -132,7 +132,7 @@ export class SSOService { async findSSOIdentityProviderById(identityProviderId: string) { return (await this.workspaceSSOIdentityProviderRepository.findOne({ where: { id: identityProviderId }, - relations: ['workspace'], + relations: { workspace: true }, })) as (SSOConfiguration & WorkspaceSSOIdentityProvider) | null; } diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts index 6cda98867c..1b67352272 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts @@ -2,6 +2,7 @@ import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; import { IDField } from '@ptc-org/nestjs-query-graphql'; import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants'; +import { APP_LOCALES } from 'twenty-shared/translations'; import { Column, CreateDateColumn, @@ -46,7 +47,7 @@ export class UserWorkspace { id: string; @Field(() => User) - @ManyToOne(() => User, (user) => user.workspaces, { + @ManyToOne(() => User, (user) => user.userWorkspaces, { onDelete: 'CASCADE', }) @JoinColumn({ name: 'userId' }) @@ -71,8 +72,8 @@ export class UserWorkspace { defaultAvatarUrl: string; @Field(() => String, { nullable: false }) - @Column({ nullable: false, default: 'en' }) - locale: string; + @Column({ nullable: false, default: 'en', type: 'varchar' }) + locale: keyof typeof APP_LOCALES; @Field() @CreateDateColumn({ type: 'timestamptz' }) diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts index d336d185f3..977bd8f665 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts @@ -615,7 +615,7 @@ describe('UserWorkspaceService', () => { } as unknown as Workspace; const user = { email, - workspaces: [ + userWorkspaces: [ { workspaceId: workspace1.id, workspace: workspace1, @@ -645,12 +645,14 @@ describe('UserWorkspaceService', () => { where: { email, }, - relations: [ - 'workspaces', - 'workspaces.workspace', - 'workspaces.workspace.workspaceSSOIdentityProviders', - 'workspaces.workspace.approvedAccessDomains', - ], + relations: { + userWorkspaces: { + workspace: { + workspaceSSOIdentityProviders: true, + approvedAccessDomains: true, + }, + }, + }, }); expect(result).toEqual({ @@ -694,7 +696,7 @@ describe('UserWorkspaceService', () => { const user = { email, - workspaces: [ + userWorkspaces: [ { workspaceId: workspace1.id, workspace: workspace1, @@ -727,12 +729,14 @@ describe('UserWorkspaceService', () => { where: { email, }, - relations: [ - 'workspaces', - 'workspaces.workspace', - 'workspaces.workspace.workspaceSSOIdentityProviders', - 'workspaces.workspace.approvedAccessDomains', - ], + relations: { + userWorkspaces: { + workspace: { + workspaceSSOIdentityProviders: true, + approvedAccessDomains: true, + }, + }, + }, }); expect(result).toEqual({ @@ -792,7 +796,7 @@ describe('UserWorkspaceService', () => { } as unknown as Workspace; const user = { id: userId, - workspaces: [{ workspace: workspace1 }, { workspace: workspace2 }], + userWorkspaces: [{ workspace: workspace1 }, { workspace: workspace2 }], } as unknown as User; jest.spyOn(userRepository, 'findOne').mockResolvedValue(user); @@ -803,9 +807,9 @@ describe('UserWorkspaceService', () => { where: { id: userId, }, - relations: ['workspaces', 'workspaces.workspace'], + relations: { userWorkspaces: { workspace: true } }, order: { - workspaces: { + userWorkspaces: { workspace: { createdAt: 'ASC', }, diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts index 6eb9609aa0..cf870bc4dd 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts @@ -222,9 +222,9 @@ export class UserWorkspaceService extends TypeOrmQueryService { where: { id: userId, }, - relations: ['workspaces', 'workspaces.workspace'], + relations: { userWorkspaces: { workspace: true } }, order: { - workspaces: { + userWorkspaces: { workspace: { createdAt: 'ASC', }, @@ -232,7 +232,7 @@ export class UserWorkspaceService extends TypeOrmQueryService { }, }); - const workspace = user?.workspaces?.[0]?.workspace; + const workspace = user?.userWorkspaces?.[0]?.workspace; workspaceValidator.assertIsDefinedOrThrow( workspace, @@ -250,16 +250,18 @@ export class UserWorkspaceService extends TypeOrmQueryService { where: { email, }, - relations: [ - 'workspaces', - 'workspaces.workspace', - 'workspaces.workspace.workspaceSSOIdentityProviders', - 'workspaces.workspace.approvedAccessDomains', - ], + relations: { + userWorkspaces: { + workspace: { + workspaceSSOIdentityProviders: true, + approvedAccessDomains: true, + }, + }, + }, }); const alreadyMemberWorkspaces = user - ? user.workspaces.map(({ workspace }) => ({ workspace })) + ? user.userWorkspaces.map(({ workspace }) => ({ workspace })) : []; const alreadyMemberWorkspacesIds = alreadyMemberWorkspaces.map( diff --git a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts index 7e4e92d53e..1e2ffc73f8 100644 --- a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts @@ -97,13 +97,13 @@ export class UserService extends TypeOrmQueryService { where: { id: userId, }, - relations: ['workspaces'], + relations: { userWorkspaces: true }, }); userValidator.assertIsDefinedOrThrow(user); const prepareForUserDeletionInWorkspaces = await Promise.all( - user.workspaces.map(async (userWorkspace) => { + user.userWorkspaces.map(async (userWorkspace) => { const { workspaceId } = userWorkspace; const workspaceMemberRepository = @@ -200,11 +200,11 @@ export class UserService extends TypeOrmQueryService { const user = await this.userRepository.findOne({ where: { id: userId, - workspaces: { + userWorkspaces: { workspaceId, }, }, - relations: ['workspaces'], + relations: { userWorkspaces: true }, }); userValidator.assertIsDefinedOrThrow( diff --git a/packages/twenty-server/src/engine/core-modules/user/user.entity.ts b/packages/twenty-server/src/engine/core-modules/user/user.entity.ts index 1a0f57d9ea..32976643be 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.entity.ts @@ -112,7 +112,7 @@ export class User { @Field(() => [UserWorkspace]) @OneToMany(() => UserWorkspace, (userWorkspace) => userWorkspace.user) - workspaces: Relation; + userWorkspaces: Relation; @Field(() => OnboardingStatus, { nullable: true }) onboardingStatus: OnboardingStatus; diff --git a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts index 245760bdc3..f30d6d8460 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts @@ -121,7 +121,7 @@ export class UserResolver { where: { id: userId, }, - relations: ['workspaces'], + relations: { userWorkspaces: true }, }); userValidator.assertIsDefinedOrThrow( @@ -133,7 +133,7 @@ export class UserResolver { return user; } - const currentUserWorkspace = user.workspaces.find( + const currentUserWorkspace = user.userWorkspaces.find( (userWorkspace) => userWorkspace.workspaceId === workspace.id, ); @@ -387,6 +387,13 @@ export class UserResolver { return workspace; } + @ResolveField(() => [UserWorkspace], { + nullable: false, + }) + async workspaces(@Parent() user: User) { + return user.userWorkspaces; + } + @ResolveField(() => AvailableWorkspaces) async availableWorkspaces( @AuthUser() user: User, diff --git a/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts b/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts index 6292e09fbd..3890775965 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts @@ -9,7 +9,6 @@ import { render } from '@react-email/render'; import { addMilliseconds } from 'date-fns'; import ms from 'ms'; import { SendInviteLinkEmail } from 'twenty-emails'; -import { APP_LOCALES } from 'twenty-shared/translations'; import { IsNull, Repository } from 'typeorm'; import { @@ -61,7 +60,7 @@ export class WorkspaceInvitationService { value: workspacePersonalInviteToken, type: AppTokenType.InvitationToken, }, - relations: ['workspace'], + relations: { workspace: true }, }); if (!appToken) { @@ -119,7 +118,7 @@ export class WorkspaceInvitationService { value: invitationToken, type: AppTokenType.InvitationToken, }, - relations: ['workspace'], + relations: { workspace: true }, }); if (!appToken) { @@ -298,7 +297,7 @@ export class WorkspaceInvitationService { lastName: sender.name.lastName, }, serverUrl: this.twentyConfigService.get('SERVER_URL'), - locale: sender.locale as keyof typeof APP_LOCALES, + locale: sender.locale, }; const emailTemplate = SendInviteLinkEmail(emailData); diff --git a/packages/twenty-server/src/engine/middlewares/middleware.service.ts b/packages/twenty-server/src/engine/middlewares/middleware.service.ts index 9787064a2c..dbac7a3385 100644 --- a/packages/twenty-server/src/engine/middlewares/middleware.service.ts +++ b/packages/twenty-server/src/engine/middlewares/middleware.service.ts @@ -161,8 +161,8 @@ export class MiddlewareService { request.authProvider = data.authProvider; request.locale = - ((data.userWorkspace?.locale ?? - request.headers['x-locale']) as keyof typeof APP_LOCALES) ?? + data.userWorkspace?.locale ?? + (request.headers['x-locale'] as keyof typeof APP_LOCALES) ?? SOURCE_LOCALE; }