5899 display a banner to alert users which need to reconnect their account (#6301)
Closes #5899 <img width="1280" alt="Index - banner" src="https://github.com/twentyhq/twenty/assets/71827178/313cf20d-eb34-496a-8c7c-7589fbd55954"> --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -1,18 +1,18 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { assert } from 'src/utils/assert';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event';
|
||||
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
|
||||
import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event';
|
||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { assert } from 'src/utils/assert';
|
||||
|
||||
export class UserService extends TypeOrmQueryService<User> {
|
||||
constructor(
|
||||
|
||||
@ -0,0 +1,110 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { KeyValuePairType } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||
import { KeyValuePairService } from 'src/engine/core-modules/key-value-pair/key-value-pair.service';
|
||||
import { mergeUserVars } from 'src/engine/core-modules/user/user-vars/utils/merge-user-vars.util';
|
||||
|
||||
@Injectable()
|
||||
export class UserVarsService<
|
||||
KeyValueTypesMap extends Record<string, any> = Record<string, any>,
|
||||
> {
|
||||
constructor(private readonly keyValuePairService: KeyValuePairService) {}
|
||||
|
||||
public async get<K extends keyof KeyValueTypesMap>({
|
||||
userId,
|
||||
workspaceId,
|
||||
key,
|
||||
}: {
|
||||
userId?: string;
|
||||
workspaceId?: string;
|
||||
key: Extract<K, string>;
|
||||
}): Promise<KeyValueTypesMap[K]> {
|
||||
const userVarWorkspaceLevel = await this.keyValuePairService.get({
|
||||
type: KeyValuePairType.USER_VAR,
|
||||
userId: null,
|
||||
workspaceId,
|
||||
key,
|
||||
});
|
||||
|
||||
if (userVarWorkspaceLevel.length > 1) {
|
||||
throw new Error(
|
||||
`Multiple values found for key ${key} at workspace level`,
|
||||
);
|
||||
}
|
||||
|
||||
const userVarUserLevel = await this.keyValuePairService.get({
|
||||
type: KeyValuePairType.USER_VAR,
|
||||
userId,
|
||||
key,
|
||||
});
|
||||
|
||||
if (userVarUserLevel.length > 1) {
|
||||
throw new Error(`Multiple values found for key ${key} at user level`);
|
||||
}
|
||||
|
||||
return mergeUserVars([...userVarUserLevel, ...userVarWorkspaceLevel]).get(
|
||||
key,
|
||||
) as KeyValueTypesMap[K];
|
||||
}
|
||||
|
||||
public async getAll({
|
||||
userId,
|
||||
workspaceId,
|
||||
}: {
|
||||
userId?: string;
|
||||
workspaceId?: string;
|
||||
}): Promise<Map<Extract<keyof KeyValueTypesMap, string>, any>> {
|
||||
const userVarsWorkspaceLevel = await this.keyValuePairService.get({
|
||||
type: KeyValuePairType.USER_VAR,
|
||||
userId: null,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const userVarsUserLevel = await this.keyValuePairService.get({
|
||||
type: KeyValuePairType.USER_VAR,
|
||||
userId,
|
||||
});
|
||||
|
||||
return mergeUserVars<Extract<keyof KeyValueTypesMap, string>>([
|
||||
...userVarsWorkspaceLevel,
|
||||
...userVarsUserLevel,
|
||||
]);
|
||||
}
|
||||
|
||||
set<K extends keyof KeyValueTypesMap>({
|
||||
userId,
|
||||
workspaceId,
|
||||
key,
|
||||
value,
|
||||
}: {
|
||||
userId?: string;
|
||||
workspaceId?: string;
|
||||
key: Extract<K, string>;
|
||||
value: KeyValueTypesMap[K];
|
||||
}) {
|
||||
return this.keyValuePairService.set({
|
||||
userId,
|
||||
workspaceId,
|
||||
key: key,
|
||||
value,
|
||||
type: KeyValuePairType.USER_VAR,
|
||||
});
|
||||
}
|
||||
|
||||
async delete({
|
||||
userId,
|
||||
workspaceId,
|
||||
key,
|
||||
}: {
|
||||
userId?: string;
|
||||
workspaceId?: string;
|
||||
key: Extract<keyof KeyValueTypesMap, string>;
|
||||
}) {
|
||||
return this.keyValuePairService.delete({
|
||||
userId,
|
||||
workspaceId,
|
||||
key,
|
||||
type: KeyValuePairType.USER_VAR,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
/* eslint-disable no-restricted-imports */
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { KeyValuePairModule } from 'src/engine/core-modules/key-value-pair/key-value-pair.module';
|
||||
import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service';
|
||||
|
||||
@Module({
|
||||
imports: [KeyValuePairModule],
|
||||
exports: [UserVarsService],
|
||||
providers: [UserVarsService],
|
||||
})
|
||||
export class UserVarsModule {}
|
||||
@ -0,0 +1,128 @@
|
||||
import { mergeUserVars } from 'src/engine/core-modules/user/user-vars/utils/merge-user-vars.util';
|
||||
|
||||
describe('mergeUserVars', () => {
|
||||
it('should merge user vars correctly', () => {
|
||||
const userVars = [
|
||||
{
|
||||
key: 'key1',
|
||||
value: JSON.parse('"value1"'),
|
||||
userId: 'userId1',
|
||||
workspaceId: 'workspaceId1',
|
||||
},
|
||||
{
|
||||
key: 'key2',
|
||||
value: JSON.parse('"value2"'),
|
||||
userId: 'userId1',
|
||||
workspaceId: null,
|
||||
},
|
||||
{
|
||||
key: 'key3',
|
||||
value: JSON.parse('"value3"'),
|
||||
userId: null,
|
||||
workspaceId: 'workspaceId1',
|
||||
},
|
||||
];
|
||||
|
||||
const mergedUserVars = mergeUserVars(userVars);
|
||||
|
||||
expect(mergedUserVars).toEqual(
|
||||
new Map([
|
||||
['key1', JSON.parse('"value1"')],
|
||||
['key2', JSON.parse('"value2"')],
|
||||
['key3', JSON.parse('"value3"')],
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should merge user vars correctly when user vars are empty', () => {
|
||||
const userVars = [];
|
||||
|
||||
const mergedUserVars = mergeUserVars(userVars);
|
||||
|
||||
expect(mergedUserVars).toEqual(new Map());
|
||||
});
|
||||
|
||||
it('should overwrite user vars correctly', () => {
|
||||
const userVars1 = [
|
||||
{
|
||||
key: 'key',
|
||||
value: JSON.parse('"value1"'),
|
||||
userId: 'userId',
|
||||
workspaceId: 'workspaceId',
|
||||
},
|
||||
{
|
||||
key: 'key',
|
||||
value: JSON.parse('"value2"'),
|
||||
userId: 'userId',
|
||||
workspaceId: null,
|
||||
},
|
||||
{
|
||||
key: 'key',
|
||||
value: JSON.parse('"value3"'),
|
||||
userId: null,
|
||||
workspaceId: 'workspaceId',
|
||||
},
|
||||
];
|
||||
|
||||
const mergedUserVars1 = mergeUserVars(userVars1);
|
||||
|
||||
const userVars2 = [
|
||||
{
|
||||
key: 'key',
|
||||
value: JSON.parse('"value1"'),
|
||||
userId: 'userId',
|
||||
workspaceId: 'workspaceId',
|
||||
},
|
||||
{
|
||||
key: 'key',
|
||||
value: JSON.parse('"value2"'),
|
||||
userId: 'userId',
|
||||
workspaceId: null,
|
||||
},
|
||||
];
|
||||
|
||||
const mergedUserVars2 = mergeUserVars(userVars2);
|
||||
|
||||
const userVars3 = [
|
||||
{
|
||||
key: 'key',
|
||||
value: JSON.parse('"value2"'),
|
||||
userId: 'userId',
|
||||
workspaceId: null,
|
||||
},
|
||||
{
|
||||
key: 'key',
|
||||
value: JSON.parse('"value3"'),
|
||||
userId: null,
|
||||
workspaceId: 'workspaceId',
|
||||
},
|
||||
];
|
||||
|
||||
const mergedUserVars3 = mergeUserVars(userVars3);
|
||||
|
||||
const userVars4 = [
|
||||
{
|
||||
key: 'key',
|
||||
value: JSON.parse('"value1"'),
|
||||
userId: 'userId',
|
||||
workspaceId: 'workspaceId',
|
||||
},
|
||||
{
|
||||
key: 'key',
|
||||
value: JSON.parse('"value3"'),
|
||||
userId: null,
|
||||
workspaceId: 'workspaceId',
|
||||
},
|
||||
];
|
||||
|
||||
const mergedUserVars4 = mergeUserVars(userVars4);
|
||||
|
||||
expect(mergedUserVars1).toEqual(new Map([['key', JSON.parse('"value1"')]]));
|
||||
|
||||
expect(mergedUserVars2).toEqual(new Map([['key', JSON.parse('"value1"')]]));
|
||||
|
||||
expect(mergedUserVars3).toEqual(new Map([['key', JSON.parse('"value2"')]]));
|
||||
|
||||
expect(mergedUserVars4).toEqual(new Map([['key', JSON.parse('"value1"')]]));
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,31 @@
|
||||
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||
|
||||
export const mergeUserVars = <T>(
|
||||
userVars: Pick<KeyValuePair, 'key' | 'value' | 'userId' | 'workspaceId'>[],
|
||||
): Map<T, JSON> => {
|
||||
const workspaceUserVarMap = new Map<T, JSON>();
|
||||
const userUserVarMap = new Map<T, JSON>();
|
||||
const userWorkspaceUserVarMap = new Map<T, JSON>();
|
||||
|
||||
for (const { key, value, userId, workspaceId } of userVars) {
|
||||
if (!userId && workspaceId) {
|
||||
workspaceUserVarMap.set(key as T, value);
|
||||
}
|
||||
|
||||
if (userId && !workspaceId) {
|
||||
userUserVarMap.set(key as T, value);
|
||||
}
|
||||
|
||||
if (userId && workspaceId) {
|
||||
userWorkspaceUserVarMap.set(key as T, value);
|
||||
}
|
||||
}
|
||||
|
||||
const mergedUserVars = new Map<T, JSON>([
|
||||
...workspaceUserVarMap,
|
||||
...userUserVarMap,
|
||||
...userWorkspaceUserVarMap,
|
||||
]);
|
||||
|
||||
return mergedUserVars;
|
||||
};
|
||||
@ -1,17 +1,20 @@
|
||||
/* eslint-disable no-restricted-imports */
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
|
||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
||||
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
|
||||
import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { UserResolver } from 'src/engine/core-modules/user/user.resolver';
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
||||
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
|
||||
import { userAutoResolverOpts } from './user.auto-resolver-opts';
|
||||
|
||||
@ -30,6 +33,8 @@ import { UserService } from './services/user.service';
|
||||
FileUploadModule,
|
||||
WorkspaceModule,
|
||||
OnboardingModule,
|
||||
TypeOrmModule.forFeature([KeyValuePair], 'core'),
|
||||
UserVarsModule,
|
||||
],
|
||||
exports: [UserService],
|
||||
providers: [UserService, UserResolver, TypeORMService],
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import {
|
||||
Args,
|
||||
Mutation,
|
||||
@ -6,30 +7,33 @@ import {
|
||||
ResolveField,
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { GraphQLJSONObject } from 'graphql-type-json';
|
||||
import { FileUpload, GraphQLUpload } from 'graphql-upload';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { SupportDriver } from 'src/engine/integrations/environment/interfaces/support.interface';
|
||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||
import { SupportDriver } from 'src/engine/integrations/environment/interfaces/support.interface';
|
||||
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
|
||||
import { assert } from 'src/utils/assert';
|
||||
import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
|
||||
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
|
||||
import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
|
||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
|
||||
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { LoadServiceWithWorkspaceContext } from 'src/engine/twenty-orm/context/load-service-with-workspace.context';
|
||||
import { assert } from 'src/utils/assert';
|
||||
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||
|
||||
const getHMACKey = (email?: string, key?: string | null) => {
|
||||
if (!email || !key) return null;
|
||||
@ -50,13 +54,14 @@ export class UserResolver {
|
||||
private readonly fileUploadService: FileUploadService,
|
||||
private readonly onboardingService: OnboardingService,
|
||||
private readonly loadServiceWithWorkspaceContext: LoadServiceWithWorkspaceContext,
|
||||
private readonly userVarService: UserVarsService,
|
||||
) {}
|
||||
|
||||
@Query(() => User)
|
||||
async currentUser(@AuthUser() { id }: User): Promise<User> {
|
||||
async currentUser(@AuthUser() { id: userId }: User): Promise<User> {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
id,
|
||||
id: userId,
|
||||
},
|
||||
relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'],
|
||||
});
|
||||
@ -66,6 +71,28 @@ export class UserResolver {
|
||||
return user;
|
||||
}
|
||||
|
||||
@ResolveField(() => GraphQLJSONObject)
|
||||
async userVars(
|
||||
@Parent() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<Record<string, any>> {
|
||||
const userVars = await this.userVarService.getAll({
|
||||
userId: user.id,
|
||||
workspaceId: workspace?.id ?? user.defaultWorkspaceId,
|
||||
});
|
||||
|
||||
const userVarAllowList = [
|
||||
'SYNC_EMAIL_ONBOARDING_STEP',
|
||||
'ACCOUNTS_TO_RECONNECT',
|
||||
];
|
||||
|
||||
const filteredMap = new Map(
|
||||
[...userVars].filter(([key]) => userVarAllowList.includes(key)),
|
||||
);
|
||||
|
||||
return Object.fromEntries(filteredMap);
|
||||
}
|
||||
|
||||
@ResolveField(() => WorkspaceMember, {
|
||||
nullable: true,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user