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
This commit is contained in:
@ -2727,6 +2727,7 @@ export type User = {
|
|||||||
supportUserHash?: Maybe<Scalars['String']>;
|
supportUserHash?: Maybe<Scalars['String']>;
|
||||||
updatedAt: Scalars['DateTime'];
|
updatedAt: Scalars['DateTime'];
|
||||||
userVars?: Maybe<Scalars['JSONObject']>;
|
userVars?: Maybe<Scalars['JSONObject']>;
|
||||||
|
userWorkspaces: Array<UserWorkspace>;
|
||||||
workspaceMember?: Maybe<WorkspaceMember>;
|
workspaceMember?: Maybe<WorkspaceMember>;
|
||||||
workspaceMembers?: Maybe<Array<WorkspaceMember>>;
|
workspaceMembers?: Maybe<Array<WorkspaceMember>>;
|
||||||
workspaces: Array<UserWorkspace>;
|
workspaces: Array<UserWorkspace>;
|
||||||
|
|||||||
@ -2565,6 +2565,7 @@ export type User = {
|
|||||||
supportUserHash?: Maybe<Scalars['String']>;
|
supportUserHash?: Maybe<Scalars['String']>;
|
||||||
updatedAt: Scalars['DateTime'];
|
updatedAt: Scalars['DateTime'];
|
||||||
userVars?: Maybe<Scalars['JSONObject']>;
|
userVars?: Maybe<Scalars['JSONObject']>;
|
||||||
|
userWorkspaces: Array<UserWorkspace>;
|
||||||
workspaceMember?: Maybe<WorkspaceMember>;
|
workspaceMember?: Maybe<WorkspaceMember>;
|
||||||
workspaceMembers?: Maybe<Array<WorkspaceMember>>;
|
workspaceMembers?: Maybe<Array<WorkspaceMember>>;
|
||||||
workspaces: Array<UserWorkspace>;
|
workspaces: Array<UserWorkspace>;
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export class LowercaseUserAndInvitationEmailsCommand extends ActiveOrSuspendedWo
|
|||||||
private async lowercaseUserEmails(workspaceId: string, dryRun: boolean) {
|
private async lowercaseUserEmails(workspaceId: string, dryRun: boolean) {
|
||||||
const users = await this.userRepository.find({
|
const users = await this.userRepository.find({
|
||||||
where: {
|
where: {
|
||||||
workspaces: {
|
userWorkspaces: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
},
|
},
|
||||||
email: Raw((alias) => `LOWER(${alias}) != ${alias}`),
|
email: Raw((alias) => `LOWER(${alias}) != ${alias}`),
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
|
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { Request } from 'express';
|
||||||
import { Plugin } from 'graphql-yoga';
|
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 = {
|
export type CacheMetadataPluginConfig = {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@ -12,19 +15,26 @@ export type CacheMetadataPluginConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function useCachedMetadata(config: CacheMetadataPluginConfig): Plugin {
|
export function useCachedMetadata(config: CacheMetadataPluginConfig): Plugin {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
const computeCacheKey = ({
|
||||||
const computeCacheKey = (serverContext: any) => {
|
operationName,
|
||||||
const workspaceId = serverContext.req.workspace?.id ?? 'anonymous';
|
request,
|
||||||
const workspaceMetadataVersion =
|
}: {
|
||||||
serverContext.req.workspaceMetadataVersion ?? '0';
|
operationName: string;
|
||||||
const operationName = getOperationName(serverContext);
|
request: Pick<Request, 'workspace' | 'locale' | 'body'>;
|
||||||
const locale = serverContext.req.locale;
|
}) => {
|
||||||
const localeCacheKey = isNonEmptyString(locale) ? `:${locale}` : '';
|
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')
|
const queryHash = createHash('sha256')
|
||||||
.update(serverContext.req.body.query)
|
.update(request.body.query)
|
||||||
.digest('hex');
|
.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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@ -37,7 +47,11 @@ export function useCachedMetadata(config: CacheMetadataPluginConfig): Plugin {
|
|||||||
return;
|
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);
|
const cachedResponse = await config.cacheGetter(cacheKey);
|
||||||
|
|
||||||
if (cachedResponse) {
|
if (cachedResponse) {
|
||||||
@ -51,7 +65,10 @@ export function useCachedMetadata(config: CacheMetadataPluginConfig): Plugin {
|
|||||||
return;
|
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);
|
const cachedResponse = await config.cacheGetter(cacheKey);
|
||||||
|
|
||||||
|
|||||||
@ -91,7 +91,7 @@ describe('AdminPanelService', () => {
|
|||||||
const mockUser = {
|
const mockUser = {
|
||||||
id: 'user-id',
|
id: 'user-id',
|
||||||
email: 'user@example.com',
|
email: 'user@example.com',
|
||||||
workspaces: [
|
userWorkspaces: [
|
||||||
{
|
{
|
||||||
workspace: {
|
workspace: {
|
||||||
id: 'workspace-id',
|
id: 'workspace-id',
|
||||||
@ -114,12 +114,12 @@ describe('AdminPanelService', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
where: expect.objectContaining({
|
where: expect.objectContaining({
|
||||||
id: 'user-id',
|
id: 'user-id',
|
||||||
workspaces: {
|
userWorkspaces: {
|
||||||
workspaceId: 'workspace-id',
|
workspaceId: 'workspace-id',
|
||||||
workspace: { allowImpersonation: true },
|
workspace: { allowImpersonation: true },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
relations: ['workspaces', 'workspaces.workspace'],
|
relations: { userWorkspaces: { workspace: true } },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -39,14 +39,14 @@ export class AdminPanelService {
|
|||||||
const user = await this.userRepository.findOne({
|
const user = await this.userRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
id: userId,
|
id: userId,
|
||||||
workspaces: {
|
userWorkspaces: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
workspace: {
|
workspace: {
|
||||||
allowImpersonation: true,
|
allowImpersonation: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relations: ['workspaces', 'workspaces.workspace'],
|
relations: { userWorkspaces: { workspace: true } },
|
||||||
});
|
});
|
||||||
|
|
||||||
userValidator.assertIsDefinedOrThrow(
|
userValidator.assertIsDefinedOrThrow(
|
||||||
@ -59,14 +59,14 @@ export class AdminPanelService {
|
|||||||
|
|
||||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||||
user.email,
|
user.email,
|
||||||
user.workspaces[0].workspace.id,
|
user.userWorkspaces[0].workspace.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
workspace: {
|
workspace: {
|
||||||
id: user.workspaces[0].workspace.id,
|
id: user.userWorkspaces[0].workspace.id,
|
||||||
workspaceUrls: this.domainManagerService.getWorkspaceUrls(
|
workspaceUrls: this.domainManagerService.getWorkspaceUrls(
|
||||||
user.workspaces[0].workspace,
|
user.userWorkspaces[0].workspace,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
loginToken,
|
loginToken,
|
||||||
@ -101,7 +101,7 @@ export class AdminPanelService {
|
|||||||
firstName: targetUser.firstName,
|
firstName: targetUser.firstName,
|
||||||
lastName: targetUser.lastName,
|
lastName: targetUser.lastName,
|
||||||
},
|
},
|
||||||
workspaces: targetUser.workspaces.map((userWorkspace) => ({
|
workspaces: targetUser.userWorkspaces.map((userWorkspace) => ({
|
||||||
id: userWorkspace.workspace.id,
|
id: userWorkspace.workspace.id,
|
||||||
name: userWorkspace.workspace.displayName ?? '',
|
name: userWorkspace.workspace.displayName ?? '',
|
||||||
totalUsers: userWorkspace.workspace.workspaceUsers.length,
|
totalUsers: userWorkspace.workspace.workspaceUsers.length,
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import crypto from 'crypto';
|
|||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { render } from '@react-email/render';
|
import { render } from '@react-email/render';
|
||||||
import { SendApprovedAccessDomainValidation } from 'twenty-emails';
|
import { SendApprovedAccessDomainValidation } from 'twenty-emails';
|
||||||
import { APP_LOCALES } from 'twenty-shared/translations';
|
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { ApprovedAccessDomain as ApprovedAccessDomainEntity } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity';
|
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,
|
lastName: sender.name.lastName,
|
||||||
},
|
},
|
||||||
serverUrl: this.twentyConfigService.get('SERVER_URL'),
|
serverUrl: this.twentyConfigService.get('SERVER_URL'),
|
||||||
locale: 'en' as keyof typeof APP_LOCALES,
|
locale: 'en',
|
||||||
});
|
});
|
||||||
const html = await render(emailTemplate);
|
const html = await render(emailTemplate);
|
||||||
const text = await render(emailTemplate, {
|
const text = await render(emailTemplate, {
|
||||||
|
|||||||
@ -528,14 +528,13 @@ export class AuthResolver {
|
|||||||
async updatePasswordViaResetToken(
|
async updatePasswordViaResetToken(
|
||||||
@Args()
|
@Args()
|
||||||
{ passwordResetToken, newPassword }: UpdatePasswordViaResetTokenInput,
|
{ passwordResetToken, newPassword }: UpdatePasswordViaResetTokenInput,
|
||||||
@Context() context: I18nContext,
|
|
||||||
): Promise<InvalidatePassword> {
|
): Promise<InvalidatePassword> {
|
||||||
const { id } =
|
const { id } =
|
||||||
await this.resetPasswordService.validatePasswordResetToken(
|
await this.resetPasswordService.validatePasswordResetToken(
|
||||||
passwordResetToken,
|
passwordResetToken,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.authService.updatePassword(id, newPassword, context.req.locale);
|
await this.authService.updatePassword(id, newPassword);
|
||||||
|
|
||||||
return await this.resetPasswordService.invalidatePasswordResetToken(id);
|
return await this.resetPasswordService.invalidatePasswordResetToken(id);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -115,7 +115,7 @@ export class SSOAuthController {
|
|||||||
const workspaceIdentityProvider =
|
const workspaceIdentityProvider =
|
||||||
await this.workspaceSSOIdentityProviderRepository.findOne({
|
await this.workspaceSSOIdentityProviderRepository.findOne({
|
||||||
where: { id: req.user.identityProviderId },
|
where: { id: req.user.identityProviderId },
|
||||||
relations: ['workspace'],
|
relations: { workspace: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import { render } from '@react-email/render';
|
|||||||
import { addMilliseconds } from 'date-fns';
|
import { addMilliseconds } from 'date-fns';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
import { PasswordUpdateNotifyEmail } from 'twenty-emails';
|
import { PasswordUpdateNotifyEmail } from 'twenty-emails';
|
||||||
import { APP_LOCALES } from 'twenty-shared/translations';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
@ -142,7 +141,7 @@ export class AuthService {
|
|||||||
where: {
|
where: {
|
||||||
email: input.email,
|
email: input.email,
|
||||||
},
|
},
|
||||||
relations: ['workspaces'],
|
relations: { userWorkspaces: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@ -424,7 +423,6 @@ export class AuthService {
|
|||||||
async updatePassword(
|
async updatePassword(
|
||||||
userId: string,
|
userId: string,
|
||||||
newPassword: string,
|
newPassword: string,
|
||||||
locale: keyof typeof APP_LOCALES,
|
|
||||||
): Promise<UpdatePassword> {
|
): Promise<UpdatePassword> {
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
throw new AuthException(
|
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) {
|
if (!user) {
|
||||||
throw new AuthException(
|
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);
|
const isPasswordValid = PASSWORD_REGEX.test(newPassword);
|
||||||
|
|
||||||
if (!isPasswordValid) {
|
if (!isPasswordValid) {
|
||||||
@ -461,13 +471,13 @@ export class AuthService {
|
|||||||
userName: `${user.firstName} ${user.lastName}`,
|
userName: `${user.firstName} ${user.lastName}`,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
link: this.domainManagerService.getBaseUrl().toString(),
|
link: this.domainManagerService.getBaseUrl().toString(),
|
||||||
locale,
|
locale: firstUserWorkspace.locale,
|
||||||
});
|
});
|
||||||
|
|
||||||
const html = await render(emailTemplate, { pretty: true });
|
const html = await render(emailTemplate, { pretty: true });
|
||||||
const text = await render(emailTemplate, { plainText: true });
|
const text = await render(emailTemplate, { plainText: true });
|
||||||
|
|
||||||
i18n.activate(locale);
|
i18n.activate(firstUserWorkspace.locale);
|
||||||
|
|
||||||
this.emailService.send({
|
this.emailService.send({
|
||||||
from: `${this.twentyConfigService.get(
|
from: `${this.twentyConfigService.get(
|
||||||
|
|||||||
@ -23,6 +23,7 @@ export class EmailVerificationResolver {
|
|||||||
private readonly domainManagerService: DomainManagerService,
|
private readonly domainManagerService: DomainManagerService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
// TODO: this should be an authenticated endpoint
|
||||||
@Mutation(() => ResendEmailVerificationTokenOutput)
|
@Mutation(() => ResendEmailVerificationTokenOutput)
|
||||||
@UseGuards(PublicEndpointGuard)
|
@UseGuards(PublicEndpointGuard)
|
||||||
async resendEmailVerificationToken(
|
async resendEmailVerificationToken(
|
||||||
|
|||||||
@ -132,7 +132,7 @@ export class SSOService {
|
|||||||
async findSSOIdentityProviderById(identityProviderId: string) {
|
async findSSOIdentityProviderById(identityProviderId: string) {
|
||||||
return (await this.workspaceSSOIdentityProviderRepository.findOne({
|
return (await this.workspaceSSOIdentityProviderRepository.findOne({
|
||||||
where: { id: identityProviderId },
|
where: { id: identityProviderId },
|
||||||
relations: ['workspace'],
|
relations: { workspace: true },
|
||||||
})) as (SSOConfiguration & WorkspaceSSOIdentityProvider) | null;
|
})) as (SSOConfiguration & WorkspaceSSOIdentityProvider) | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
|||||||
|
|
||||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||||
import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants';
|
import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants';
|
||||||
|
import { APP_LOCALES } from 'twenty-shared/translations';
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
@ -46,7 +47,7 @@ export class UserWorkspace {
|
|||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@Field(() => User)
|
@Field(() => User)
|
||||||
@ManyToOne(() => User, (user) => user.workspaces, {
|
@ManyToOne(() => User, (user) => user.userWorkspaces, {
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
})
|
})
|
||||||
@JoinColumn({ name: 'userId' })
|
@JoinColumn({ name: 'userId' })
|
||||||
@ -71,8 +72,8 @@ export class UserWorkspace {
|
|||||||
defaultAvatarUrl: string;
|
defaultAvatarUrl: string;
|
||||||
|
|
||||||
@Field(() => String, { nullable: false })
|
@Field(() => String, { nullable: false })
|
||||||
@Column({ nullable: false, default: 'en' })
|
@Column({ nullable: false, default: 'en', type: 'varchar' })
|
||||||
locale: string;
|
locale: keyof typeof APP_LOCALES;
|
||||||
|
|
||||||
@Field()
|
@Field()
|
||||||
@CreateDateColumn({ type: 'timestamptz' })
|
@CreateDateColumn({ type: 'timestamptz' })
|
||||||
|
|||||||
@ -615,7 +615,7 @@ describe('UserWorkspaceService', () => {
|
|||||||
} as unknown as Workspace;
|
} as unknown as Workspace;
|
||||||
const user = {
|
const user = {
|
||||||
email,
|
email,
|
||||||
workspaces: [
|
userWorkspaces: [
|
||||||
{
|
{
|
||||||
workspaceId: workspace1.id,
|
workspaceId: workspace1.id,
|
||||||
workspace: workspace1,
|
workspace: workspace1,
|
||||||
@ -645,12 +645,14 @@ describe('UserWorkspaceService', () => {
|
|||||||
where: {
|
where: {
|
||||||
email,
|
email,
|
||||||
},
|
},
|
||||||
relations: [
|
relations: {
|
||||||
'workspaces',
|
userWorkspaces: {
|
||||||
'workspaces.workspace',
|
workspace: {
|
||||||
'workspaces.workspace.workspaceSSOIdentityProviders',
|
workspaceSSOIdentityProviders: true,
|
||||||
'workspaces.workspace.approvedAccessDomains',
|
approvedAccessDomains: true,
|
||||||
],
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
@ -694,7 +696,7 @@ describe('UserWorkspaceService', () => {
|
|||||||
|
|
||||||
const user = {
|
const user = {
|
||||||
email,
|
email,
|
||||||
workspaces: [
|
userWorkspaces: [
|
||||||
{
|
{
|
||||||
workspaceId: workspace1.id,
|
workspaceId: workspace1.id,
|
||||||
workspace: workspace1,
|
workspace: workspace1,
|
||||||
@ -727,12 +729,14 @@ describe('UserWorkspaceService', () => {
|
|||||||
where: {
|
where: {
|
||||||
email,
|
email,
|
||||||
},
|
},
|
||||||
relations: [
|
relations: {
|
||||||
'workspaces',
|
userWorkspaces: {
|
||||||
'workspaces.workspace',
|
workspace: {
|
||||||
'workspaces.workspace.workspaceSSOIdentityProviders',
|
workspaceSSOIdentityProviders: true,
|
||||||
'workspaces.workspace.approvedAccessDomains',
|
approvedAccessDomains: true,
|
||||||
],
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
@ -792,7 +796,7 @@ describe('UserWorkspaceService', () => {
|
|||||||
} as unknown as Workspace;
|
} as unknown as Workspace;
|
||||||
const user = {
|
const user = {
|
||||||
id: userId,
|
id: userId,
|
||||||
workspaces: [{ workspace: workspace1 }, { workspace: workspace2 }],
|
userWorkspaces: [{ workspace: workspace1 }, { workspace: workspace2 }],
|
||||||
} as unknown as User;
|
} as unknown as User;
|
||||||
|
|
||||||
jest.spyOn(userRepository, 'findOne').mockResolvedValue(user);
|
jest.spyOn(userRepository, 'findOne').mockResolvedValue(user);
|
||||||
@ -803,9 +807,9 @@ describe('UserWorkspaceService', () => {
|
|||||||
where: {
|
where: {
|
||||||
id: userId,
|
id: userId,
|
||||||
},
|
},
|
||||||
relations: ['workspaces', 'workspaces.workspace'],
|
relations: { userWorkspaces: { workspace: true } },
|
||||||
order: {
|
order: {
|
||||||
workspaces: {
|
userWorkspaces: {
|
||||||
workspace: {
|
workspace: {
|
||||||
createdAt: 'ASC',
|
createdAt: 'ASC',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -222,9 +222,9 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
|
|||||||
where: {
|
where: {
|
||||||
id: userId,
|
id: userId,
|
||||||
},
|
},
|
||||||
relations: ['workspaces', 'workspaces.workspace'],
|
relations: { userWorkspaces: { workspace: true } },
|
||||||
order: {
|
order: {
|
||||||
workspaces: {
|
userWorkspaces: {
|
||||||
workspace: {
|
workspace: {
|
||||||
createdAt: 'ASC',
|
createdAt: 'ASC',
|
||||||
},
|
},
|
||||||
@ -232,7 +232,7 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const workspace = user?.workspaces?.[0]?.workspace;
|
const workspace = user?.userWorkspaces?.[0]?.workspace;
|
||||||
|
|
||||||
workspaceValidator.assertIsDefinedOrThrow(
|
workspaceValidator.assertIsDefinedOrThrow(
|
||||||
workspace,
|
workspace,
|
||||||
@ -250,16 +250,18 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
|
|||||||
where: {
|
where: {
|
||||||
email,
|
email,
|
||||||
},
|
},
|
||||||
relations: [
|
relations: {
|
||||||
'workspaces',
|
userWorkspaces: {
|
||||||
'workspaces.workspace',
|
workspace: {
|
||||||
'workspaces.workspace.workspaceSSOIdentityProviders',
|
workspaceSSOIdentityProviders: true,
|
||||||
'workspaces.workspace.approvedAccessDomains',
|
approvedAccessDomains: true,
|
||||||
],
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const alreadyMemberWorkspaces = user
|
const alreadyMemberWorkspaces = user
|
||||||
? user.workspaces.map(({ workspace }) => ({ workspace }))
|
? user.userWorkspaces.map(({ workspace }) => ({ workspace }))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const alreadyMemberWorkspacesIds = alreadyMemberWorkspaces.map(
|
const alreadyMemberWorkspacesIds = alreadyMemberWorkspaces.map(
|
||||||
|
|||||||
@ -97,13 +97,13 @@ export class UserService extends TypeOrmQueryService<User> {
|
|||||||
where: {
|
where: {
|
||||||
id: userId,
|
id: userId,
|
||||||
},
|
},
|
||||||
relations: ['workspaces'],
|
relations: { userWorkspaces: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
userValidator.assertIsDefinedOrThrow(user);
|
userValidator.assertIsDefinedOrThrow(user);
|
||||||
|
|
||||||
const prepareForUserDeletionInWorkspaces = await Promise.all(
|
const prepareForUserDeletionInWorkspaces = await Promise.all(
|
||||||
user.workspaces.map(async (userWorkspace) => {
|
user.userWorkspaces.map(async (userWorkspace) => {
|
||||||
const { workspaceId } = userWorkspace;
|
const { workspaceId } = userWorkspace;
|
||||||
|
|
||||||
const workspaceMemberRepository =
|
const workspaceMemberRepository =
|
||||||
@ -200,11 +200,11 @@ export class UserService extends TypeOrmQueryService<User> {
|
|||||||
const user = await this.userRepository.findOne({
|
const user = await this.userRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
id: userId,
|
id: userId,
|
||||||
workspaces: {
|
userWorkspaces: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relations: ['workspaces'],
|
relations: { userWorkspaces: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
userValidator.assertIsDefinedOrThrow(
|
userValidator.assertIsDefinedOrThrow(
|
||||||
|
|||||||
@ -112,7 +112,7 @@ export class User {
|
|||||||
|
|
||||||
@Field(() => [UserWorkspace])
|
@Field(() => [UserWorkspace])
|
||||||
@OneToMany(() => UserWorkspace, (userWorkspace) => userWorkspace.user)
|
@OneToMany(() => UserWorkspace, (userWorkspace) => userWorkspace.user)
|
||||||
workspaces: Relation<UserWorkspace[]>;
|
userWorkspaces: Relation<UserWorkspace[]>;
|
||||||
|
|
||||||
@Field(() => OnboardingStatus, { nullable: true })
|
@Field(() => OnboardingStatus, { nullable: true })
|
||||||
onboardingStatus: OnboardingStatus;
|
onboardingStatus: OnboardingStatus;
|
||||||
|
|||||||
@ -121,7 +121,7 @@ export class UserResolver {
|
|||||||
where: {
|
where: {
|
||||||
id: userId,
|
id: userId,
|
||||||
},
|
},
|
||||||
relations: ['workspaces'],
|
relations: { userWorkspaces: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
userValidator.assertIsDefinedOrThrow(
|
userValidator.assertIsDefinedOrThrow(
|
||||||
@ -133,7 +133,7 @@ export class UserResolver {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentUserWorkspace = user.workspaces.find(
|
const currentUserWorkspace = user.userWorkspaces.find(
|
||||||
(userWorkspace) => userWorkspace.workspaceId === workspace.id,
|
(userWorkspace) => userWorkspace.workspaceId === workspace.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -387,6 +387,13 @@ export class UserResolver {
|
|||||||
return workspace;
|
return workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => [UserWorkspace], {
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
|
async workspaces(@Parent() user: User) {
|
||||||
|
return user.userWorkspaces;
|
||||||
|
}
|
||||||
|
|
||||||
@ResolveField(() => AvailableWorkspaces)
|
@ResolveField(() => AvailableWorkspaces)
|
||||||
async availableWorkspaces(
|
async availableWorkspaces(
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import { render } from '@react-email/render';
|
|||||||
import { addMilliseconds } from 'date-fns';
|
import { addMilliseconds } from 'date-fns';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
import { SendInviteLinkEmail } from 'twenty-emails';
|
import { SendInviteLinkEmail } from 'twenty-emails';
|
||||||
import { APP_LOCALES } from 'twenty-shared/translations';
|
|
||||||
import { IsNull, Repository } from 'typeorm';
|
import { IsNull, Repository } from 'typeorm';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -61,7 +60,7 @@ export class WorkspaceInvitationService {
|
|||||||
value: workspacePersonalInviteToken,
|
value: workspacePersonalInviteToken,
|
||||||
type: AppTokenType.InvitationToken,
|
type: AppTokenType.InvitationToken,
|
||||||
},
|
},
|
||||||
relations: ['workspace'],
|
relations: { workspace: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!appToken) {
|
if (!appToken) {
|
||||||
@ -119,7 +118,7 @@ export class WorkspaceInvitationService {
|
|||||||
value: invitationToken,
|
value: invitationToken,
|
||||||
type: AppTokenType.InvitationToken,
|
type: AppTokenType.InvitationToken,
|
||||||
},
|
},
|
||||||
relations: ['workspace'],
|
relations: { workspace: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!appToken) {
|
if (!appToken) {
|
||||||
@ -298,7 +297,7 @@ export class WorkspaceInvitationService {
|
|||||||
lastName: sender.name.lastName,
|
lastName: sender.name.lastName,
|
||||||
},
|
},
|
||||||
serverUrl: this.twentyConfigService.get('SERVER_URL'),
|
serverUrl: this.twentyConfigService.get('SERVER_URL'),
|
||||||
locale: sender.locale as keyof typeof APP_LOCALES,
|
locale: sender.locale,
|
||||||
};
|
};
|
||||||
|
|
||||||
const emailTemplate = SendInviteLinkEmail(emailData);
|
const emailTemplate = SendInviteLinkEmail(emailData);
|
||||||
|
|||||||
@ -161,8 +161,8 @@ export class MiddlewareService {
|
|||||||
request.authProvider = data.authProvider;
|
request.authProvider = data.authProvider;
|
||||||
|
|
||||||
request.locale =
|
request.locale =
|
||||||
((data.userWorkspace?.locale ??
|
data.userWorkspace?.locale ??
|
||||||
request.headers['x-locale']) as keyof typeof APP_LOCALES) ??
|
(request.headers['x-locale'] as keyof typeof APP_LOCALES) ??
|
||||||
SOURCE_LOCALE;
|
SOURCE_LOCALE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user