From a6abe091636c42a6a4bed0b49d40dfd0e252b8c2 Mon Sep 17 00:00:00 2001 From: Weiko Date: Wed, 22 Nov 2023 14:12:39 +0100 Subject: [PATCH] Move Impersonate from User to Workspace (#2630) * Fix impersonate * align core typeorm config with metadata config + add allowImpersonation to workspace * move allowImpersonation to workspace * remove allowImpersonation from workspaceMember workspace table --- front/src/generated-metadata/graphql.ts | 5 ++ front/src/generated/graphql.tsx | 18 +++++--- .../auth/states/currentWorkspaceState.ts | 4 +- .../components/ToggleImpersonate.tsx | 46 +++++++++++++++++++ .../graphql/fragments/userQueryFragment.ts | 1 + .../users/graphql/queries/getCurrentUser.ts | 1 + .../graphql/mutations/updateWorkspace.ts | 1 + .../queries/getWorkspaceFromInviteHash.ts | 1 + front/src/pages/auth/CreateWorkspace.tsx | 2 + front/src/pages/settings/SettingsProfile.tsx | 8 ---- .../src/pages/settings/SettingsWorkspace.tsx | 9 +++- server/ormconfig.ts | 16 ------- server/package.json | 2 +- server/src/core/auth/auth.module.ts | 15 ++---- server/src/core/auth/auth.resolver.ts | 2 +- .../controllers/google-auth.controller.ts | 4 +- server/src/core/auth/services/auth.service.ts | 15 ++++-- .../src/core/auth/services/token.service.ts | 4 +- .../core/auth/strategies/jwt.auth.strategy.ts | 4 +- .../refresh-token/refresh-token.module.ts | 7 +-- server/src/core/user/services/user.service.ts | 2 +- server/src/core/user/user.entity.ts | 4 +- server/src/core/user/user.module.ts | 9 ++-- .../workspace/dtos/update-workspace-input.ts | 7 ++- .../workspace/services/workspace.service.ts | 2 +- server/src/core/workspace/workspace.entity.ts | 4 ++ server/src/core/workspace/workspace.module.ts | 9 ++-- .../field-metadata/workspace-member.ts | 18 -------- .../database/typeorm/core/core.datasource.ts | 21 +++++++++ ...387203-addAllowImpersonationToWorkspace.ts | 43 +++++++++++++++++ .../typeorm/metadata/metadata.datasource.ts | 10 +--- server/src/database/typeorm/typeorm.module.ts | 11 +++++ .../standard-objects/workspace-member.ts | 13 ------ 33 files changed, 199 insertions(+), 119 deletions(-) create mode 100644 front/src/modules/settings/workspace/components/ToggleImpersonate.tsx delete mode 100644 server/ormconfig.ts create mode 100644 server/src/database/typeorm/core/core.datasource.ts create mode 100644 server/src/database/typeorm/core/migrations/1700654387203-addAllowImpersonationToWorkspace.ts diff --git a/front/src/generated-metadata/graphql.ts b/front/src/generated-metadata/graphql.ts index 7dad74137..607b20d92 100644 --- a/front/src/generated-metadata/graphql.ts +++ b/front/src/generated-metadata/graphql.ts @@ -83,11 +83,13 @@ export type CreateOneRelationInput = { export type CreateRelationInput = { description?: InputMaybe; + fromDescription?: InputMaybe; fromIcon?: InputMaybe; fromLabel: Scalars['String']['input']; fromName: Scalars['String']['input']; fromObjectMetadataId: Scalars['String']['input']; relationType: RelationMetadataType; + toDescription?: InputMaybe; toIcon?: InputMaybe; toLabel: Scalars['String']['input']; toName: Scalars['String']['input']; @@ -393,7 +395,9 @@ export type UpdateFieldInput = { export type UpdateObjectInput = { description?: InputMaybe; icon?: InputMaybe; + imageIdentifierFieldMetadataId?: InputMaybe; isActive?: InputMaybe; + labelIdentifierFieldMetadataId?: InputMaybe; labelPlural?: InputMaybe; labelSingular?: InputMaybe; namePlural?: InputMaybe; @@ -457,6 +461,7 @@ export type UserWorkspaceMemberName = { export type Workspace = { __typename?: 'Workspace'; + allowImpersonation: Scalars['Boolean']['output']; createdAt: Scalars['DateTime']['output']; deletedAt?: Maybe; displayName?: Maybe; diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index 879a3ebf3..488940e60 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -399,6 +399,7 @@ export type Telemetry = { }; export type UpdateWorkspaceInput = { + allowImpersonation?: InputMaybe; displayName?: InputMaybe; domainName?: InputMaybe; inviteHash?: InputMaybe; @@ -460,6 +461,7 @@ export type Verify = { export type Workspace = { __typename?: 'Workspace'; + allowImpersonation: Scalars['Boolean']; createdAt: Scalars['DateTime']; deletedAt?: Maybe; displayName?: Maybe; @@ -607,7 +609,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?: 'UserWorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, allowImpersonation: boolean, name: { __typename?: 'UserWorkspaceMemberName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | 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?: 'UserWorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, allowImpersonation: boolean, name: { __typename?: 'UserWorkspaceMemberName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; export type RenewTokenMutationVariables = Exact<{ refreshToken: Scalars['String']; @@ -630,7 +632,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?: 'UserWorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, allowImpersonation: boolean, name: { __typename?: 'UserWorkspaceMemberName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | 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?: 'UserWorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, allowImpersonation: boolean, name: { __typename?: 'UserWorkspaceMemberName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; export type CheckUserExistsQueryVariables = Exact<{ email: Scalars['String']; @@ -644,7 +646,7 @@ export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>; export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null } } }; -export type UserQueryFragmentFragment = { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'UserWorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, allowImpersonation: boolean, name: { __typename?: 'UserWorkspaceMemberName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null } }; +export type UserQueryFragmentFragment = { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'UserWorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, allowImpersonation: boolean, name: { __typename?: 'UserWorkspaceMemberName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean } }; export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>; @@ -661,7 +663,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; -export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'UserWorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, allowImpersonation: boolean, name: { __typename?: 'UserWorkspaceMemberName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null } } }; +export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'UserWorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, allowImpersonation: boolean, name: { __typename?: 'UserWorkspaceMemberName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean } } }; export type DeleteCurrentWorkspaceMutationVariables = Exact<{ [key: string]: never; }>; @@ -673,7 +675,7 @@ export type UpdateWorkspaceMutationVariables = Exact<{ }>; -export type UpdateWorkspaceMutation = { __typename?: 'Mutation', updateWorkspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null } }; +export type UpdateWorkspaceMutation = { __typename?: 'Mutation', updateWorkspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null, allowImpersonation: boolean } }; export type UploadWorkspaceLogoMutationVariables = Exact<{ file: Scalars['Upload']; @@ -687,7 +689,7 @@ export type GetWorkspaceFromInviteHashQueryVariables = Exact<{ }>; -export type GetWorkspaceFromInviteHashQuery = { __typename?: 'Query', findWorkspaceFromInviteHash: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null } }; +export type GetWorkspaceFromInviteHashQuery = { __typename?: 'Query', findWorkspaceFromInviteHash: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, allowImpersonation: boolean } }; export const AuthTokenFragmentFragmentDoc = gql` fragment AuthTokenFragment on AuthToken { @@ -730,6 +732,7 @@ export const UserQueryFragmentFragmentDoc = gql` logo domainName inviteHash + allowImpersonation } } `; @@ -1162,6 +1165,7 @@ export const GetCurrentUserDocument = gql` logo domainName inviteHash + allowImpersonation } } } @@ -1232,6 +1236,7 @@ export const UpdateWorkspaceDocument = gql` domainName displayName logo + allowImpersonation } } `; @@ -1298,6 +1303,7 @@ export const GetWorkspaceFromInviteHashDocument = gql` id displayName logo + allowImpersonation } } `; diff --git a/front/src/modules/auth/states/currentWorkspaceState.ts b/front/src/modules/auth/states/currentWorkspaceState.ts index 34e17a4ef..597b883ec 100644 --- a/front/src/modules/auth/states/currentWorkspaceState.ts +++ b/front/src/modules/auth/states/currentWorkspaceState.ts @@ -1,10 +1,10 @@ import { atom } from 'recoil'; -import { Workspace } from '~/generated-metadata/graphql'; +import { Workspace } from '~/generated/graphql'; export type CurrentWorkspace = Pick< Workspace, - 'id' | 'inviteHash' | 'logo' | 'displayName' + 'id' | 'inviteHash' | 'logo' | 'displayName' | 'allowImpersonation' >; export const currentWorkspaceState = atom({ diff --git a/front/src/modules/settings/workspace/components/ToggleImpersonate.tsx b/front/src/modules/settings/workspace/components/ToggleImpersonate.tsx new file mode 100644 index 000000000..453cfc21d --- /dev/null +++ b/front/src/modules/settings/workspace/components/ToggleImpersonate.tsx @@ -0,0 +1,46 @@ +import { useRecoilState } from 'recoil'; + +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { Toggle } from '@/ui/input/components/Toggle'; +import { useUpdateWorkspaceMutation } from '~/generated/graphql'; + +export const ToggleImpersonate = () => { + const { enqueueSnackBar } = useSnackBar(); + + const [currentWorkspace, setCurrentWorkspace] = useRecoilState( + currentWorkspaceState, + ); + + const [updateWorkspace] = useUpdateWorkspaceMutation(); + + const handleChange = async (value: boolean) => { + try { + if (!currentWorkspace?.id) { + throw new Error('User is not logged in'); + } + await updateWorkspace({ + variables: { + input: { + allowImpersonation: value, + }, + }, + }); + setCurrentWorkspace({ + ...currentWorkspace, + allowImpersonation: value, + }); + } catch (err: any) { + enqueueSnackBar(err?.message, { + variant: 'error', + }); + } + }; + + return ( + + ); +}; diff --git a/front/src/modules/users/graphql/fragments/userQueryFragment.ts b/front/src/modules/users/graphql/fragments/userQueryFragment.ts index 5cbcc2a0c..9de53c9d0 100644 --- a/front/src/modules/users/graphql/fragments/userQueryFragment.ts +++ b/front/src/modules/users/graphql/fragments/userQueryFragment.ts @@ -25,6 +25,7 @@ export const USER_QUERY_FRAGMENT = gql` logo domainName inviteHash + allowImpersonation } } `; diff --git a/front/src/modules/users/graphql/queries/getCurrentUser.ts b/front/src/modules/users/graphql/queries/getCurrentUser.ts index a374d762b..f865a0b48 100644 --- a/front/src/modules/users/graphql/queries/getCurrentUser.ts +++ b/front/src/modules/users/graphql/queries/getCurrentUser.ts @@ -26,6 +26,7 @@ export const GET_CURRENT_USER = gql` logo domainName inviteHash + allowImpersonation } } } diff --git a/front/src/modules/workspace/graphql/mutations/updateWorkspace.ts b/front/src/modules/workspace/graphql/mutations/updateWorkspace.ts index d4c4021b7..1d9a9b9fb 100644 --- a/front/src/modules/workspace/graphql/mutations/updateWorkspace.ts +++ b/front/src/modules/workspace/graphql/mutations/updateWorkspace.ts @@ -7,6 +7,7 @@ export const UPDATE_WORKSPACE = gql` domainName displayName logo + allowImpersonation } } `; diff --git a/front/src/modules/workspace/graphql/queries/getWorkspaceFromInviteHash.ts b/front/src/modules/workspace/graphql/queries/getWorkspaceFromInviteHash.ts index eaca90095..b18a9d2c6 100644 --- a/front/src/modules/workspace/graphql/queries/getWorkspaceFromInviteHash.ts +++ b/front/src/modules/workspace/graphql/queries/getWorkspaceFromInviteHash.ts @@ -6,6 +6,7 @@ export const GET_WORKSPACE_FROM_INVITE_HASH = gql` id displayName logo + allowImpersonation } } `; diff --git a/front/src/pages/auth/CreateWorkspace.tsx b/front/src/pages/auth/CreateWorkspace.tsx index e82520ff1..acd646e66 100644 --- a/front/src/pages/auth/CreateWorkspace.tsx +++ b/front/src/pages/auth/CreateWorkspace.tsx @@ -77,6 +77,8 @@ export const CreateWorkspace = () => { setCurrentWorkspace({ id: result.data?.updateWorkspace?.id ?? '', displayName: data.name, + allowImpersonation: + result.data?.updateWorkspace?.allowImpersonation ?? false, }); if (result.errors || !result.data?.updateWorkspace) { diff --git a/front/src/pages/settings/SettingsProfile.tsx b/front/src/pages/settings/SettingsProfile.tsx index 9ecb2b14f..1cf55e69a 100644 --- a/front/src/pages/settings/SettingsProfile.tsx +++ b/front/src/pages/settings/SettingsProfile.tsx @@ -5,7 +5,6 @@ import { DeleteAccount } from '@/settings/profile/components/DeleteAccount'; import { EmailField } from '@/settings/profile/components/EmailField'; import { NameFields } from '@/settings/profile/components/NameFields'; import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader'; -import { ToggleField } from '@/settings/profile/components/ToggleField'; import { IconSettings } from '@/ui/display/icon'; import { H1Title } from '@/ui/display/typography/components/H1Title'; import { H2Title } from '@/ui/display/typography/components/H2Title'; @@ -35,13 +34,6 @@ export const SettingsProfile = () => ( /> -
- } - description="Grant Twenty support temporary access to your account so we can troubleshoot problems or recover content on your behalf. You can revoke access at any time." - /> -
diff --git a/front/src/pages/settings/SettingsWorkspace.tsx b/front/src/pages/settings/SettingsWorkspace.tsx index 48feaadee..e3f82fd60 100644 --- a/front/src/pages/settings/SettingsWorkspace.tsx +++ b/front/src/pages/settings/SettingsWorkspace.tsx @@ -3,6 +3,7 @@ import styled from '@emotion/styled'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { DeleteWorkspace } from '@/settings/profile/components/DeleteWorkspace'; import { NameField } from '@/settings/workspace/components/NameField'; +import { ToggleImpersonate } from '@/settings/workspace/components/ToggleImpersonate'; import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader'; import { IconSettings } from '@/ui/display/icon'; import { H1Title } from '@/ui/display/typography/components/H1Title'; @@ -26,7 +27,13 @@ export const SettingsWorkspace = () => ( - +
+ } + description="Grant Twenty support temporary access to your workspace so we can troubleshoot problems or recover content on your behalf. You can revoke access at any time." + /> +
diff --git a/server/ormconfig.ts b/server/ormconfig.ts deleted file mode 100644 index cc37a46bf..000000000 --- a/server/ormconfig.ts +++ /dev/null @@ -1,16 +0,0 @@ -import dotenv from 'dotenv'; -import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions'; -dotenv.config(); - -export default { - url: process.env.PG_DATABASE_URL, - type: 'postgres', - entities: [__dirname + '/src/core/**/*.entity{.ts,.js}'], - synchronize: false, - migrationsRun: false, - migrationsTableName: '_typeorm_migrations', - migrations: [__dirname + '/migrations/**/*{.ts,.js}'], - cli: { - migrationsDir: __dirname + '/migrations', - }, -} as PostgresConnectionOptions; diff --git a/server/package.json b/server/package.json index 5506137b5..b7b75b519 100644 --- a/server/package.json +++ b/server/package.json @@ -21,7 +21,7 @@ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "./scripts/run-integration.sh", "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js", - "typeorm:migrate": "yarn typeorm migration:run -d ./src/database/typeorm/metadata/metadata.datasource.ts", + "typeorm:migrate": "yarn typeorm migration:run -d ./src/database/typeorm/metadata/metadata.datasource.ts && yarn typeorm migration:run -d ./src/database/typeorm/core/core.datasource.ts", "database:init": "yarn database:setup && yarn database:seed", "database:setup": "npx ts-node ./scripts/setup-db.ts && yarn database:migrate", "database:truncate": "npx ts-node ./scripts/truncate-db.ts", diff --git a/server/src/core/auth/auth.module.ts b/server/src/core/auth/auth.module.ts index e89e456c7..9dce7de38 100644 --- a/server/src/core/auth/auth.module.ts +++ b/server/src/core/auth/auth.module.ts @@ -3,19 +3,15 @@ import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql'; - import { EnvironmentService } from 'src/integrations/environment/environment.service'; import { FileModule } from 'src/core/file/file.module'; import { Workspace } from 'src/core/workspace/workspace.entity'; import { User } from 'src/core/user/user.entity'; import { RefreshToken } from 'src/core/refresh-token/refresh-token.entity'; import { DataSourceModule } from 'src/metadata/data-source/data-source.module'; -import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { UserModule } from 'src/core/user/user.module'; import { WorkspaceManagerModule } from 'src/workspace/workspace-manager/workspace-manager.module'; - -import config from '../../../ormconfig'; +import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { AuthResolver } from './auth.resolver'; @@ -44,13 +40,8 @@ const jwtModule = JwtModule.registerAsync({ DataSourceModule, UserModule, WorkspaceManagerModule, - TypeOrmModule.forRoot(config), - NestjsQueryGraphQLModule.forFeature({ - imports: [ - TypeOrmModule.forFeature([Workspace, User, RefreshToken]), - TypeORMModule, - ], - }), + TypeORMModule, + TypeOrmModule.forFeature([Workspace, User, RefreshToken], 'core'), ], controllers: [GoogleAuthController, VerifyAuthController], providers: [AuthService, TokenService, JwtAuthStrategy, AuthResolver], diff --git a/server/src/core/auth/auth.resolver.ts b/server/src/core/auth/auth.resolver.ts index ae205fd14..74a86cc0f 100644 --- a/server/src/core/auth/auth.resolver.ts +++ b/server/src/core/auth/auth.resolver.ts @@ -34,7 +34,7 @@ import { ImpersonateInput } from './dto/impersonate.input'; @Resolver() export class AuthResolver { constructor( - @InjectRepository(Workspace) + @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, private authService: AuthService, private tokenService: TokenService, diff --git a/server/src/core/auth/controllers/google-auth.controller.ts b/server/src/core/auth/controllers/google-auth.controller.ts index 96ca2957a..fb83222d0 100644 --- a/server/src/core/auth/controllers/google-auth.controller.ts +++ b/server/src/core/auth/controllers/google-auth.controller.ts @@ -21,8 +21,8 @@ export class GoogleAuthController { private readonly environmentService: EnvironmentService, private readonly typeORMService: TypeORMService, private readonly authService: AuthService, - @InjectRepository(Workspace) - @InjectRepository(User, 'metadata') + @InjectRepository(Workspace, 'core') + @InjectRepository(User, 'core') private readonly userRepository: Repository, ) {} diff --git a/server/src/core/auth/services/auth.service.ts b/server/src/core/auth/services/auth.service.ts index c0a55ae80..6400e326d 100644 --- a/server/src/core/auth/services/auth.service.ts +++ b/server/src/core/auth/services/auth.service.ts @@ -44,9 +44,9 @@ export class AuthService { private readonly userService: UserService, private readonly workspaceManagerService: WorkspaceManagerService, private readonly fileUploadService: FileUploadService, - @InjectRepository(Workspace) + @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, - @InjectRepository(User) + @InjectRepository(User, 'core') private readonly userRepository: Repository, ) {} @@ -194,13 +194,18 @@ export class AuthService { } async impersonate(userId: string) { - const user = await this.userRepository.findOneBy({ - id: userId, + const user = await this.userRepository.findOne({ + where: { + id: userId, + }, + relations: ['defaultWorkspace'], }); assert(user, "This user doesn't exist", NotFoundException); - // Todo: check if workspace member can be impersonated + if (!user.defaultWorkspace.allowImpersonation) { + throw new ForbiddenException('Impersonation not allowed'); + } const accessToken = await this.tokenService.generateAccessToken(user.id); const refreshToken = await this.tokenService.generateRefreshToken(user.id); diff --git a/server/src/core/auth/services/token.service.ts b/server/src/core/auth/services/token.service.ts index 3bc084171..1b36fe4a5 100644 --- a/server/src/core/auth/services/token.service.ts +++ b/server/src/core/auth/services/token.service.ts @@ -26,9 +26,9 @@ export class TokenService { constructor( private readonly jwtService: JwtService, private readonly environmentService: EnvironmentService, - @InjectRepository(User) + @InjectRepository(User, 'core') private readonly userRepository: Repository, - @InjectRepository(RefreshToken) + @InjectRepository(RefreshToken, 'core') private readonly refreshTokenRepository: Repository, ) {} diff --git a/server/src/core/auth/strategies/jwt.auth.strategy.ts b/server/src/core/auth/strategies/jwt.auth.strategy.ts index c96eff791..429b1be19 100644 --- a/server/src/core/auth/strategies/jwt.auth.strategy.ts +++ b/server/src/core/auth/strategies/jwt.auth.strategy.ts @@ -25,9 +25,9 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') { private readonly environmentService: EnvironmentService, private readonly typeORMService: TypeORMService, private readonly dataSourceService: DataSourceService, - @InjectRepository(Workspace) + @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, - @InjectRepository(User) + @InjectRepository(User, 'core') private readonly userRepository: Repository, ) { super({ diff --git a/server/src/core/refresh-token/refresh-token.module.ts b/server/src/core/refresh-token/refresh-token.module.ts index d3d616559..0e2321895 100644 --- a/server/src/core/refresh-token/refresh-token.module.ts +++ b/server/src/core/refresh-token/refresh-token.module.ts @@ -1,12 +1,8 @@ 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'; -// eslint-disable-next-line no-restricted-imports -import config from '../../../ormconfig'; - import { RefreshToken } from './refresh-token.entity'; import { refreshTokenAutoResolverOpts } from './refresh-token.auto-resolver-opts'; @@ -14,9 +10,8 @@ import { RefreshTokenService } from './services/refresh-token.service'; @Module({ imports: [ - TypeOrmModule.forRoot(config), NestjsQueryGraphQLModule.forFeature({ - imports: [NestjsQueryTypeOrmModule.forFeature([RefreshToken])], + imports: [NestjsQueryTypeOrmModule.forFeature([RefreshToken], 'core')], services: [RefreshTokenService], resolvers: refreshTokenAutoResolverOpts, }), diff --git a/server/src/core/user/services/user.service.ts b/server/src/core/user/services/user.service.ts index 868de5312..f7c791c0d 100644 --- a/server/src/core/user/services/user.service.ts +++ b/server/src/core/user/services/user.service.ts @@ -11,7 +11,7 @@ import { TypeORMService } from 'src/database/typeorm/typeorm.service'; export class UserService extends TypeOrmQueryService { constructor( - @InjectRepository(User) + @InjectRepository(User, 'core') private readonly userRepository: Repository, private readonly dataSourceService: DataSourceService, private readonly typeORMService: TypeORMService, diff --git a/server/src/core/user/user.entity.ts b/server/src/core/user/user.entity.ts index 0d9cdce24..646ad0da8 100644 --- a/server/src/core/user/user.entity.ts +++ b/server/src/core/user/user.entity.ts @@ -23,11 +23,11 @@ export class User { id: string; @Field() - @Column({ nullable: true }) + @Column({ default: '' }) firstName: string; @Field() - @Column({ nullable: true }) + @Column({ default: '' }) lastName: string; @Field() diff --git a/server/src/core/user/user.module.ts b/server/src/core/user/user.module.ts index 567b7e1b7..ea98ff03e 100644 --- a/server/src/core/user/user.module.ts +++ b/server/src/core/user/user.module.ts @@ -1,6 +1,5 @@ /* 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'; @@ -12,17 +11,17 @@ import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { DataSourceModule } from 'src/metadata/data-source/data-source.module'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; -import config from '../../../ormconfig'; - import { userAutoResolverOpts } from './user.auto-resolver-opts'; import { UserService } from './services/user.service'; @Module({ imports: [ - TypeOrmModule.forRoot(config), NestjsQueryGraphQLModule.forFeature({ - imports: [NestjsQueryTypeOrmModule.forFeature([User]), TypeORMModule], + imports: [ + NestjsQueryTypeOrmModule.forFeature([User], 'core'), + TypeORMModule, + ], resolvers: userAutoResolverOpts, }), DataSourceModule, diff --git a/server/src/core/workspace/dtos/update-workspace-input.ts b/server/src/core/workspace/dtos/update-workspace-input.ts index 4ba333e00..75b40376c 100644 --- a/server/src/core/workspace/dtos/update-workspace-input.ts +++ b/server/src/core/workspace/dtos/update-workspace-input.ts @@ -1,6 +1,6 @@ import { Field, InputType } from '@nestjs/graphql'; -import { IsOptional, IsString } from 'class-validator'; +import { IsBoolean, IsOptional, IsString } from 'class-validator'; @InputType() export class UpdateWorkspaceInput { @@ -23,4 +23,9 @@ export class UpdateWorkspaceInput { @IsString() @IsOptional() inviteHash?: string; + + @Field({ nullable: true }) + @IsBoolean() + @IsOptional() + allowImpersonation?: boolean; } diff --git a/server/src/core/workspace/services/workspace.service.ts b/server/src/core/workspace/services/workspace.service.ts index 0662a0799..c1e5d04b4 100644 --- a/server/src/core/workspace/services/workspace.service.ts +++ b/server/src/core/workspace/services/workspace.service.ts @@ -10,7 +10,7 @@ import { Workspace } from 'src/core/workspace/workspace.entity'; export class WorkspaceService extends TypeOrmQueryService { constructor( - @InjectRepository(Workspace) + @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, private readonly workspaceManagerService: WorkspaceManagerService, ) { diff --git a/server/src/core/workspace/workspace.entity.ts b/server/src/core/workspace/workspace.entity.ts index 76f7cd539..9186e19b0 100644 --- a/server/src/core/workspace/workspace.entity.ts +++ b/server/src/core/workspace/workspace.entity.ts @@ -49,4 +49,8 @@ export class Workspace { @OneToMany(() => User, (user) => user.defaultWorkspace) users: User[]; + + @Field() + @Column({ default: true }) + allowImpersonation: boolean; } diff --git a/server/src/core/workspace/workspace.module.ts b/server/src/core/workspace/workspace.module.ts index 3874dcc6d..a8bfea917 100644 --- a/server/src/core/workspace/workspace.module.ts +++ b/server/src/core/workspace/workspace.module.ts @@ -1,5 +1,4 @@ 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'; @@ -7,9 +6,7 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { FileModule } from 'src/core/file/file.module'; import { WorkspaceManagerModule } from 'src/workspace/workspace-manager/workspace-manager.module'; import { WorkspaceResolver } from 'src/core/workspace/workspace.resolver'; - -// eslint-disable-next-line no-restricted-imports -import config from '../../../ormconfig'; +import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { Workspace } from './workspace.entity'; import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts'; @@ -18,10 +15,10 @@ import { WorkspaceService } from './services/workspace.service'; @Module({ imports: [ - TypeOrmModule.forRoot(config), + TypeORMModule, NestjsQueryGraphQLModule.forFeature({ imports: [ - NestjsQueryTypeOrmModule.forFeature([Workspace]), + NestjsQueryTypeOrmModule.forFeature([Workspace], 'core'), WorkspaceManagerModule, FileModule, ], diff --git a/server/src/database/typeorm-seeds/metadata/field-metadata/workspace-member.ts b/server/src/database/typeorm-seeds/metadata/field-metadata/workspace-member.ts index 5aff162c2..b020b1698 100644 --- a/server/src/database/typeorm-seeds/metadata/field-metadata/workspace-member.ts +++ b/server/src/database/typeorm-seeds/metadata/field-metadata/workspace-member.ts @@ -162,24 +162,6 @@ export const seedWorkspaceMemberFieldMetadata = async ( isSystem: false, defaultValue: undefined, }, - { - id: SeedWorkspaceMemberFieldMetadataIds.AllowImpersonation, - objectMetadataId: SeedObjectMetadataIds.WorkspaceMember, - isCustom: false, - workspaceId: SeedWorkspaceId, - isActive: true, - type: FieldMetadataType.BOOLEAN, - name: 'allowImpersonation', - label: 'Admin Access', - targetColumnMap: { - value: 'allowImpersonation', - }, - description: 'Allow Admin Access', - icon: 'IconEye', - isNullable: false, - isSystem: false, - defaultValue: { value: false }, - }, { id: SeedWorkspaceMemberFieldMetadataIds.ColorScheme, objectMetadataId: SeedObjectMetadataIds.WorkspaceMember, diff --git a/server/src/database/typeorm/core/core.datasource.ts b/server/src/database/typeorm/core/core.datasource.ts new file mode 100644 index 000000000..5966fa038 --- /dev/null +++ b/server/src/database/typeorm/core/core.datasource.ts @@ -0,0 +1,21 @@ +import { ConfigService } from '@nestjs/config'; +import { TypeOrmModuleOptions } from '@nestjs/typeorm'; + +import { DataSource, DataSourceOptions } from 'typeorm'; +import { config } from 'dotenv'; +config(); +const configService = new ConfigService(); +export const typeORMCoreModuleOptions: TypeOrmModuleOptions = { + url: configService.get('PG_DATABASE_URL'), + type: 'postgres', + logging: ['error'], + schema: 'core', + entities: ['dist/src/core/**/*.entity{.ts,.js}'], + synchronize: false, + migrationsRun: false, + migrationsTableName: '_typeorm_migrations', + migrations: ['dist/src/database/typeorm/core/migrations/*{.ts,.js}'], +}; +export const connectionSource = new DataSource( + typeORMCoreModuleOptions as DataSourceOptions, +); diff --git a/server/src/database/typeorm/core/migrations/1700654387203-addAllowImpersonationToWorkspace.ts b/server/src/database/typeorm/core/migrations/1700654387203-addAllowImpersonationToWorkspace.ts new file mode 100644 index 000000000..c4b6d12cf --- /dev/null +++ b/server/src/database/typeorm/core/migrations/1700654387203-addAllowImpersonationToWorkspace.ts @@ -0,0 +1,43 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddAllowImpersonationToWorkspace1700654387203 + implements MigrationInterface +{ + name = 'AddAllowImpersonationToWorkspace1700654387203'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."user" DROP CONSTRAINT "FK_5d77e050eabd28d203b301235a7"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."refreshToken" DROP CONSTRAINT "FK_610102b60fea1455310ccd299de"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ADD "allowImpersonation" boolean NOT NULL DEFAULT true`, + ); + await queryRunner.query( + `ALTER TABLE "core"."user" ADD CONSTRAINT "FK_2ec910029395fa7655621c88908" FOREIGN KEY ("defaultWorkspaceId") REFERENCES "core"."workspace"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."refreshToken" ADD CONSTRAINT "FK_7008a2b0fb083127f60b5f4448e" FOREIGN KEY ("userId") REFERENCES "core"."user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."refreshToken" DROP CONSTRAINT "FK_7008a2b0fb083127f60b5f4448e"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."user" DROP CONSTRAINT "FK_2ec910029395fa7655621c88908"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" DROP COLUMN "allowImpersonation"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."refreshToken" ADD CONSTRAINT "FK_610102b60fea1455310ccd299de" FOREIGN KEY ("userId") REFERENCES "core"."user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "core"."user" ADD CONSTRAINT "FK_5d77e050eabd28d203b301235a7" FOREIGN KEY ("defaultWorkspaceId") REFERENCES "core"."workspace"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } +} diff --git a/server/src/database/typeorm/metadata/metadata.datasource.ts b/server/src/database/typeorm/metadata/metadata.datasource.ts index a0d50e630..139cd2a98 100644 --- a/server/src/database/typeorm/metadata/metadata.datasource.ts +++ b/server/src/database/typeorm/metadata/metadata.datasource.ts @@ -10,17 +10,11 @@ export const typeORMMetadataModuleOptions: TypeOrmModuleOptions = { type: 'postgres', logging: ['error'], schema: 'metadata', - entities: [ - 'dist/src/metadata/**/*.entity{.ts,.js}', - 'dist/src/core/**/*.entity{.ts,.js}', - ], + entities: ['dist/src/metadata/**/*.entity{.ts,.js}'], synchronize: false, migrationsRun: false, migrationsTableName: '_typeorm_migrations', - migrations: [ - 'dist/src/database/typeorm/metadata/migrations/*{.ts,.js}', - 'dist/src/database/typeorm/core/migrations/*{.ts,.js}', - ], + migrations: ['dist/src/database/typeorm/metadata/migrations/*{.ts,.js}'], }; export const connectionSource = new DataSource( typeORMMetadataModuleOptions as DataSourceOptions, diff --git a/server/src/database/typeorm/typeorm.module.ts b/server/src/database/typeorm/typeorm.module.ts index 955f44f8f..1b76eda41 100644 --- a/server/src/database/typeorm/typeorm.module.ts +++ b/server/src/database/typeorm/typeorm.module.ts @@ -1,6 +1,8 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; +import { typeORMCoreModuleOptions } from 'src/database/typeorm/core/core.datasource'; + import { TypeORMService } from './typeorm.service'; import { typeORMMetadataModuleOptions } from './metadata/metadata.datasource'; @@ -10,12 +12,21 @@ const metadataTypeORMFactory = async (): Promise => ({ name: 'metadata', }); +const coreTypeORMFactory = async (): Promise => ({ + ...typeORMCoreModuleOptions, + name: 'core', +}); + @Module({ imports: [ TypeOrmModule.forRootAsync({ useFactory: metadataTypeORMFactory, name: 'metadata', }), + TypeOrmModule.forRootAsync({ + useFactory: coreTypeORMFactory, + name: 'core', + }), ], providers: [TypeORMService], exports: [TypeORMService], diff --git a/server/src/workspace/workspace-manager/standard-objects/workspace-member.ts b/server/src/workspace/workspace-manager/standard-objects/workspace-member.ts index e2d5b7997..b308360c4 100644 --- a/server/src/workspace/workspace-manager/standard-objects/workspace-member.ts +++ b/server/src/workspace/workspace-manager/standard-objects/workspace-member.ts @@ -26,19 +26,6 @@ const workspaceMemberMetadata = { isNullable: false, defaultValue: { firstName: '', lastName: '' }, }, - { - isCustom: false, - isActive: true, - type: FieldMetadataType.BOOLEAN, - name: 'allowImpersonation', - label: 'Admin Access', - targetColumnMap: { - value: 'allowImpersonation', - }, - description: 'Allow Admin Access', - icon: 'IconEye', - isNullable: false, - }, { isCustom: false, isActive: true,