Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4e81c91de | ||
|
|
7812486d32 | ||
|
|
bace6c1802 |
@@ -3294,7 +3294,6 @@ type Mutation {
|
||||
updateWorkspace(data: UpdateWorkspaceInput!): Workspace!
|
||||
deleteCurrentWorkspace: Workspace!
|
||||
checkCustomDomainValidRecords: DomainValidRecords
|
||||
installApplication(appRegistrationId: String!, version: String): Boolean!
|
||||
runWorkspaceMigration(workspaceMigration: WorkspaceMigrationInput!): Boolean!
|
||||
uninstallApplication(universalIdentifier: String!): Boolean!
|
||||
createOIDCIdentityProvider(input: SetupOIDCSsoInput!): SetupSso!
|
||||
@@ -3315,7 +3314,8 @@ type Mutation {
|
||||
deleteEmailingDomain(id: String!): Boolean!
|
||||
verifyEmailingDomain(id: String!): EmailingDomain!
|
||||
createOneAppToken(input: CreateOneAppTokenInput!): AppToken!
|
||||
installMarketplaceApp(universalIdentifier: String!, version: String): Application!
|
||||
installMarketplaceApp(universalIdentifier: String!, version: String): Boolean! @deprecated(reason: "Use installApplication instead")
|
||||
installApplication(universalIdentifier: String!, version: String): Application!
|
||||
syncMarketplaceCatalog: Boolean!
|
||||
createDevelopmentApplication(universalIdentifier: String!, name: String!): DevelopmentApplication!
|
||||
generateApplicationToken(applicationId: UUID!): ApplicationTokenPair!
|
||||
|
||||
@@ -2827,7 +2827,6 @@ export interface Mutation {
|
||||
updateWorkspace: Workspace
|
||||
deleteCurrentWorkspace: Workspace
|
||||
checkCustomDomainValidRecords?: DomainValidRecords
|
||||
installApplication: Scalars['Boolean']
|
||||
runWorkspaceMigration: Scalars['Boolean']
|
||||
uninstallApplication: Scalars['Boolean']
|
||||
createOIDCIdentityProvider: SetupSso
|
||||
@@ -2848,7 +2847,9 @@ export interface Mutation {
|
||||
deleteEmailingDomain: Scalars['Boolean']
|
||||
verifyEmailingDomain: EmailingDomain
|
||||
createOneAppToken: AppToken
|
||||
installMarketplaceApp: Application
|
||||
/** @deprecated Use installApplication instead */
|
||||
installMarketplaceApp: Scalars['Boolean']
|
||||
installApplication: Application
|
||||
syncMarketplaceCatalog: Scalars['Boolean']
|
||||
createDevelopmentApplication: DevelopmentApplication
|
||||
generateApplicationToken: ApplicationTokenPair
|
||||
@@ -5879,7 +5880,6 @@ export interface MutationGenqlSelection{
|
||||
updateWorkspace?: (WorkspaceGenqlSelection & { __args: {data: UpdateWorkspaceInput} })
|
||||
deleteCurrentWorkspace?: WorkspaceGenqlSelection
|
||||
checkCustomDomainValidRecords?: DomainValidRecordsGenqlSelection
|
||||
installApplication?: { __args: {appRegistrationId: Scalars['String'], version?: (Scalars['String'] | null)} }
|
||||
runWorkspaceMigration?: { __args: {workspaceMigration: WorkspaceMigrationInput} }
|
||||
uninstallApplication?: { __args: {universalIdentifier: Scalars['String']} }
|
||||
createOIDCIdentityProvider?: (SetupSsoGenqlSelection & { __args: {input: SetupOIDCSsoInput} })
|
||||
@@ -5900,7 +5900,9 @@ export interface MutationGenqlSelection{
|
||||
deleteEmailingDomain?: { __args: {id: Scalars['String']} }
|
||||
verifyEmailingDomain?: (EmailingDomainGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
createOneAppToken?: (AppTokenGenqlSelection & { __args: {input: CreateOneAppTokenInput} })
|
||||
installMarketplaceApp?: (ApplicationGenqlSelection & { __args: {universalIdentifier: Scalars['String'], version?: (Scalars['String'] | null)} })
|
||||
/** @deprecated Use installApplication instead */
|
||||
installMarketplaceApp?: { __args: {universalIdentifier: Scalars['String'], version?: (Scalars['String'] | null)} }
|
||||
installApplication?: (ApplicationGenqlSelection & { __args: {universalIdentifier: Scalars['String'], version?: (Scalars['String'] | null)} })
|
||||
syncMarketplaceCatalog?: boolean | number
|
||||
createDevelopmentApplication?: (DevelopmentApplicationGenqlSelection & { __args: {universalIdentifier: Scalars['String'], name: Scalars['String']} })
|
||||
generateApplicationToken?: (ApplicationTokenPairGenqlSelection & { __args: {applicationId: Scalars['UUID']} })
|
||||
|
||||
@@ -8388,18 +8388,6 @@ export default {
|
||||
"checkCustomDomainValidRecords": [
|
||||
228
|
||||
],
|
||||
"installApplication": [
|
||||
6,
|
||||
{
|
||||
"appRegistrationId": [
|
||||
1,
|
||||
"String!"
|
||||
],
|
||||
"version": [
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"runWorkspaceMigration": [
|
||||
6,
|
||||
{
|
||||
@@ -8602,6 +8590,18 @@ export default {
|
||||
}
|
||||
],
|
||||
"installMarketplaceApp": [
|
||||
6,
|
||||
{
|
||||
"universalIdentifier": [
|
||||
1,
|
||||
"String!"
|
||||
],
|
||||
"version": [
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"installApplication": [
|
||||
58,
|
||||
{
|
||||
"universalIdentifier": [
|
||||
|
||||
@@ -2457,8 +2457,9 @@ export type Mutation = {
|
||||
impersonate: Impersonate;
|
||||
initiateOTPProvisioning: InitiateTwoFactorAuthenticationProvisioning;
|
||||
initiateOTPProvisioningForAuthenticatedUser: InitiateTwoFactorAuthenticationProvisioning;
|
||||
installApplication: Scalars['Boolean'];
|
||||
installMarketplaceApp: Application;
|
||||
installApplication: Application;
|
||||
/** @deprecated Use installApplication instead */
|
||||
installMarketplaceApp: Scalars['Boolean'];
|
||||
refreshEnterpriseValidityToken: Scalars['Boolean'];
|
||||
removeQueryFromEventStream: Scalars['Boolean'];
|
||||
removeRoleFromAgent: Scalars['Boolean'];
|
||||
@@ -3087,7 +3088,7 @@ export type MutationInitiateOtpProvisioningArgs = {
|
||||
|
||||
|
||||
export type MutationInstallApplicationArgs = {
|
||||
appRegistrationId: Scalars['String'];
|
||||
universalIdentifier: Scalars['String'];
|
||||
version?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
@@ -6620,13 +6621,12 @@ export type MarketplaceAppDetailFieldsFragment = { __typename?: 'MarketplaceAppD
|
||||
|
||||
export type MarketplaceAppFieldsFragment = { __typename?: 'MarketplaceApp', id: string, name: string, description: string, author: string, category: string, logo?: string | null, sourcePackage?: string | null, isFeatured: boolean };
|
||||
|
||||
export type InstallMarketplaceAppMutationVariables = Exact<{
|
||||
export type InstallApplicationMutationVariables = Exact<{
|
||||
universalIdentifier: Scalars['String'];
|
||||
version?: InputMaybe<Scalars['String']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type InstallMarketplaceAppMutation = { __typename?: 'Mutation', installMarketplaceApp: { __typename?: 'Application', id: string } };
|
||||
export type InstallApplicationMutation = { __typename?: 'Mutation', installApplication: { __typename?: 'Application', id: string } };
|
||||
|
||||
export type UpgradeApplicationMutationVariables = Exact<{
|
||||
appRegistrationId: Scalars['String'];
|
||||
@@ -8014,7 +8014,7 @@ export const FindManyAvailablePackagesDocument = {"kind":"Document","definitions
|
||||
export const FindManyLogicFunctionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyLogicFunctions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findManyLogicFunctions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LogicFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LogicFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"timeoutSeconds"}},{"kind":"Field","name":{"kind":"Name","value":"sourceHandlerPath"}},{"kind":"Field","name":{"kind":"Name","value":"handlerName"}},{"kind":"Field","name":{"kind":"Name","value":"cronTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"databaseEventTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"httpRouteTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"toolTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"workflowActionTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<FindManyLogicFunctionsQuery, FindManyLogicFunctionsQueryVariables>;
|
||||
export const FindOneLogicFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneLogicFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunctionIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findOneLogicFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LogicFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LogicFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"timeoutSeconds"}},{"kind":"Field","name":{"kind":"Name","value":"sourceHandlerPath"}},{"kind":"Field","name":{"kind":"Name","value":"handlerName"}},{"kind":"Field","name":{"kind":"Name","value":"cronTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"databaseEventTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"httpRouteTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"toolTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"workflowActionTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<FindOneLogicFunctionQuery, FindOneLogicFunctionQueryVariables>;
|
||||
export const GetLogicFunctionSourceCodeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetLogicFunctionSourceCode"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunctionIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getLogicFunctionSourceCode"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<GetLogicFunctionSourceCodeQuery, GetLogicFunctionSourceCodeQueryVariables>;
|
||||
export const InstallMarketplaceAppDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InstallMarketplaceApp"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"version"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"installMarketplaceApp"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"universalIdentifier"},"value":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}}},{"kind":"Argument","name":{"kind":"Name","value":"version"},"value":{"kind":"Variable","name":{"kind":"Name","value":"version"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<InstallMarketplaceAppMutation, InstallMarketplaceAppMutationVariables>;
|
||||
export const InstallApplicationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InstallApplication"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"installApplication"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"universalIdentifier"},"value":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<InstallApplicationMutation, InstallApplicationMutationVariables>;
|
||||
export const UpgradeApplicationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpgradeApplication"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appRegistrationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"targetVersion"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"upgradeApplication"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appRegistrationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appRegistrationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"targetVersion"},"value":{"kind":"Variable","name":{"kind":"Name","value":"targetVersion"}}}]}]}}]} as unknown as DocumentNode<UpgradeApplicationMutation, UpgradeApplicationMutationVariables>;
|
||||
export const FindManyMarketplaceAppsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyMarketplaceApps"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findManyMarketplaceApps"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"MarketplaceAppFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"MarketplaceAppFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"MarketplaceApp"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"isFeatured"}}]}}]} as unknown as DocumentNode<FindManyMarketplaceAppsQuery, FindManyMarketplaceAppsQueryVariables>;
|
||||
export const FindMarketplaceAppDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindMarketplaceAppDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findMarketplaceAppDetail"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"universalIdentifier"},"value":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"MarketplaceAppDetailFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"MarketplaceAppDetailFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"MarketplaceAppDetail"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sourceType"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"latestAvailableVersion"}},{"kind":"Field","name":{"kind":"Name","value":"isListed"}},{"kind":"Field","name":{"kind":"Name","value":"isFeatured"}},{"kind":"Field","name":{"kind":"Name","value":"manifest"}}]}}]} as unknown as DocumentNode<FindMarketplaceAppDetailQuery, FindMarketplaceAppDetailQueryVariables>;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const INSTALL_APPLICATION = gql`
|
||||
mutation InstallApplication($universalIdentifier: String!) {
|
||||
installApplication(universalIdentifier: $universalIdentifier) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const INSTALL_MARKETPLACE_APP = gql`
|
||||
mutation InstallMarketplaceApp(
|
||||
$universalIdentifier: String!
|
||||
$version: String
|
||||
) {
|
||||
installMarketplaceApp(
|
||||
universalIdentifier: $universalIdentifier
|
||||
version: $version
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -4,25 +4,23 @@ import { t } from '@lingui/core/macro';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
InstallMarketplaceAppDocument,
|
||||
type InstallMarketplaceAppMutation,
|
||||
InstallApplicationDocument,
|
||||
type InstallApplicationMutation,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useInstallMarketplaceApp = () => {
|
||||
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
const [installMarketplaceAppMutation] = useMutation(
|
||||
InstallMarketplaceAppDocument,
|
||||
);
|
||||
const [installApplicationMutation] = useMutation(InstallApplicationDocument);
|
||||
|
||||
const install = async (variables: {
|
||||
universalIdentifier: string;
|
||||
version?: string;
|
||||
}): Promise<InstallMarketplaceAppMutation | null> => {
|
||||
}): Promise<InstallApplicationMutation | null> => {
|
||||
setIsInstalling(true);
|
||||
|
||||
try {
|
||||
const result = await installMarketplaceAppMutation({ variables });
|
||||
const result = await installApplicationMutation({ variables });
|
||||
|
||||
if (isDefined(result.data)) {
|
||||
enqueueSuccessSnackBar({
|
||||
|
||||
+1
-1
@@ -104,7 +104,7 @@ export const SettingsAvailableApplicationDetails = () => {
|
||||
universalIdentifier: detail.universalIdentifier,
|
||||
});
|
||||
|
||||
const applicationId = data?.installMarketplaceApp?.id;
|
||||
const applicationId = data?.installApplication?.id;
|
||||
|
||||
if (isDefined(applicationId)) {
|
||||
navigateSettings(SettingsPath.ApplicationDetail, {
|
||||
|
||||
@@ -139,8 +139,8 @@ export class FileApi {
|
||||
}): Promise<ApiResponse<boolean>> {
|
||||
try {
|
||||
const mutation = `
|
||||
mutation InstallMarketplaceApp($universalIdentifier: String!) {
|
||||
installMarketplaceApp(universalIdentifier: $universalIdentifier) {
|
||||
mutation InstallApplication($universalIdentifier: String!) {
|
||||
installApplication(universalIdentifier: $universalIdentifier) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -169,7 +169,7 @@ export class FileApi {
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response.data.data.installMarketplaceApp,
|
||||
data: response.data.data.installApplication,
|
||||
};
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
|
||||
+1
@@ -101,6 +101,7 @@ export class DropConnectedAccountStandardObjectCommand extends ActiveOrSuspended
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
isSystemBuild: true,
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
...(connectedAccountObjectMetadata
|
||||
? {
|
||||
|
||||
+2
-21
@@ -4,14 +4,13 @@ import {
|
||||
UseInterceptors,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { Args, Mutation, Query } from '@nestjs/graphql';
|
||||
import { Args, Query } from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { ApplicationExceptionFilter } from 'src/engine/core-modules/application/application-exception-filter';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
|
||||
import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
|
||||
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
@@ -27,10 +26,7 @@ import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/wor
|
||||
@UseInterceptors(WorkspaceMigrationGraphqlApiExceptionInterceptor)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
export class ApplicationInstallResolver {
|
||||
constructor(
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly applicationInstallService: ApplicationInstallService,
|
||||
) {}
|
||||
constructor(private readonly applicationService: ApplicationService) {}
|
||||
|
||||
@Query(() => [ApplicationDTO])
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
|
||||
@@ -57,19 +53,4 @@ export class ApplicationInstallResolver {
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.APPLICATIONS))
|
||||
async installApplication(
|
||||
@Args('appRegistrationId') appRegistrationId: string,
|
||||
@Args('version', { type: () => String, nullable: true })
|
||||
version: string | undefined,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
): Promise<boolean> {
|
||||
return this.applicationInstallService.installApplication({
|
||||
appRegistrationId,
|
||||
version,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+25
-1
@@ -48,13 +48,37 @@ export class MarketplaceResolver {
|
||||
);
|
||||
}
|
||||
|
||||
@Mutation(() => ApplicationDTO)
|
||||
@Mutation(() => Boolean, {
|
||||
deprecationReason: 'Use installApplication instead',
|
||||
})
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.MARKETPLACE_APPS))
|
||||
async installMarketplaceApp(
|
||||
@Args('universalIdentifier') universalIdentifier: string,
|
||||
@Args('version', { type: () => String, nullable: true })
|
||||
version: string | undefined,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<boolean> {
|
||||
const registration =
|
||||
await this.marketplaceQueryService.findRegistrationByUniversalIdentifier(
|
||||
universalIdentifier,
|
||||
);
|
||||
|
||||
await this.applicationInstallService.installApplication({
|
||||
appRegistrationId: registration.id,
|
||||
version,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Mutation(() => ApplicationDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.MARKETPLACE_APPS))
|
||||
async installApplication(
|
||||
@Args('universalIdentifier') universalIdentifier: string,
|
||||
@Args('version', { type: () => String, nullable: true })
|
||||
version: string | undefined,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
) {
|
||||
const registration =
|
||||
await this.marketplaceQueryService.findRegistrationByUniversalIdentifier(
|
||||
|
||||
+2
@@ -6,6 +6,7 @@ import { ApplicationConnectionsController } from 'src/engine/core-modules/applic
|
||||
import { ApplicationConnectionsListService } from 'src/engine/core-modules/application/connection-provider/connections/services/application-connections-list.service';
|
||||
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { ConnectedAccountTokenEncryptionModule } from 'src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
import { RefreshTokensManagerModule } from 'src/modules/connected-account/refresh-tokens-manager/connected-account-refresh-tokens-manager.module';
|
||||
|
||||
@@ -22,6 +23,7 @@ import { RefreshTokensManagerModule } from 'src/modules/connected-account/refres
|
||||
TokenModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
RefreshTokensManagerModule,
|
||||
ConnectedAccountTokenEncryptionModule,
|
||||
],
|
||||
providers: [ApplicationConnectionsListService],
|
||||
controllers: [ApplicationConnectionsController],
|
||||
|
||||
+7
-2
@@ -9,6 +9,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { ConnectionProviderEntity } from 'src/engine/core-modules/application/connection-provider/connection-provider.entity';
|
||||
import { type AppConnectionDto } from 'src/engine/core-modules/application/connection-provider/connections/dtos/app-connection.dto';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { ConnectedAccountTokenEncryptionService } from 'src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.service';
|
||||
import { ConnectedAccountRefreshTokensService } from 'src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service';
|
||||
|
||||
type ListArgs = {
|
||||
@@ -38,6 +39,7 @@ export class ApplicationConnectionsListService {
|
||||
|
||||
constructor(
|
||||
private readonly refreshTokensService: ConnectedAccountRefreshTokensService,
|
||||
private readonly connectedAccountTokenEncryptionService: ConnectedAccountTokenEncryptionService,
|
||||
@InjectRepository(ConnectedAccountEntity)
|
||||
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
|
||||
@InjectRepository(ConnectionProviderEntity)
|
||||
@@ -207,7 +209,7 @@ export class ApplicationConnectionsListService {
|
||||
}
|
||||
|
||||
try {
|
||||
const tokens = await this.refreshTokensService.resolveTokens(
|
||||
const encryptedTokens = await this.refreshTokensService.resolveTokens(
|
||||
account,
|
||||
workspaceId,
|
||||
);
|
||||
@@ -219,7 +221,10 @@ export class ApplicationConnectionsListService {
|
||||
handle: account.handle,
|
||||
visibility: account.visibility as 'user' | 'workspace',
|
||||
userWorkspaceId: account.userWorkspaceId,
|
||||
accessToken: tokens.accessToken,
|
||||
accessToken: this.connectedAccountTokenEncryptionService.decrypt({
|
||||
ciphertext: encryptedTokens.accessToken,
|
||||
workspaceId,
|
||||
}),
|
||||
scopes: account.scopes ?? provider.oauthConfig?.scopes ?? [],
|
||||
authFailedAt: account.authFailedAt?.toISOString() ?? null,
|
||||
};
|
||||
|
||||
+54
-55
@@ -12,7 +12,6 @@ import {
|
||||
ConnectedAccountRefreshAccessTokenExceptionCode,
|
||||
} from 'src/engine/metadata-modules/connected-account/exceptions/connected-account-refresh-tokens.exception';
|
||||
import { ConnectedAccountTokenEncryptionService } from 'src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/google/services/google-api-refresh-tokens.service';
|
||||
import { MicrosoftAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/services/microsoft-api-refresh-tokens.service';
|
||||
|
||||
@@ -106,15 +105,6 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
refreshTokens: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: GlobalWorkspaceOrmManager,
|
||||
useValue: {
|
||||
executeInWorkspaceContext: jest
|
||||
.fn()
|
||||
|
||||
.mockImplementation((fn: () => any, _authContext?: any) => fn()),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(ConnectedAccountEntity),
|
||||
useValue: {
|
||||
@@ -149,7 +139,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
});
|
||||
|
||||
describe('resolveTokens', () => {
|
||||
it('should reuse the cached token, decrypt before returning to the caller, and skip the refresh call entirely', async () => {
|
||||
it('should reuse the cached encrypted tokens as-is when valid, skipping decrypt and the refresh call entirely', async () => {
|
||||
const connectedAccount = {
|
||||
id: mockConnectedAccountId,
|
||||
provider: ConnectedAccountProvider.MICROSOFT,
|
||||
@@ -164,28 +154,19 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
accessToken: mockAccessTokenPlaintext,
|
||||
refreshToken: mockRefreshTokenPlaintext,
|
||||
accessToken: mockEncryptedAccessToken,
|
||||
refreshToken: mockEncryptedRefreshToken,
|
||||
});
|
||||
expect(
|
||||
connectedAccountTokenEncryptionService.decrypt,
|
||||
).toHaveBeenCalledWith({
|
||||
ciphertext: mockEncryptedAccessToken,
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
expect(
|
||||
connectedAccountTokenEncryptionService.decrypt,
|
||||
).toHaveBeenCalledWith({
|
||||
ciphertext: mockEncryptedRefreshToken,
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
).not.toHaveBeenCalled();
|
||||
expect(
|
||||
microsoftAPIRefreshAccessTokenService.refreshTokens,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(connectedAccountRepository.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should decrypt the stored refresh token before sending to Microsoft, then re-encrypt the rotated tokens before persisting', async () => {
|
||||
it('should decrypt the stored refresh token before sending to Microsoft, persist the re-encrypted tokens, and return them encrypted', async () => {
|
||||
const connectedAccount = {
|
||||
id: mockConnectedAccountId,
|
||||
provider: ConnectedAccountProvider.MICROSOFT,
|
||||
@@ -194,35 +175,41 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
lastCredentialsRefreshedAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
|
||||
} as ConnectedAccountEntity;
|
||||
|
||||
const newTokens = {
|
||||
const newPlaintextTokens = {
|
||||
accessToken: mockNewAccessTokenPlaintext,
|
||||
refreshToken: mockRefreshTokenPlaintext,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(microsoftAPIRefreshAccessTokenService, 'refreshTokens')
|
||||
.mockResolvedValue(newTokens);
|
||||
.mockResolvedValue(newPlaintextTokens);
|
||||
|
||||
const result = await service.resolveTokens(
|
||||
connectedAccount,
|
||||
mockWorkspaceId,
|
||||
);
|
||||
|
||||
expect(result).toEqual(newTokens);
|
||||
const expectedEncryptedNewAccessToken = `${FAKE_CIPHER_PREFIX}CIPHER(${mockNewAccessTokenPlaintext})`;
|
||||
const expectedEncryptedNewRefreshToken = `${FAKE_CIPHER_PREFIX}CIPHER(${mockRefreshTokenPlaintext})`;
|
||||
|
||||
expect(result).toEqual({
|
||||
accessToken: expectedEncryptedNewAccessToken,
|
||||
refreshToken: expectedEncryptedNewRefreshToken,
|
||||
});
|
||||
expect(
|
||||
microsoftAPIRefreshAccessTokenService.refreshTokens,
|
||||
).toHaveBeenCalledWith(mockRefreshTokenPlaintext);
|
||||
expect(connectedAccountRepository.update).toHaveBeenCalledWith(
|
||||
{ id: mockConnectedAccountId, workspaceId: mockWorkspaceId },
|
||||
expect.objectContaining({
|
||||
accessToken: `${FAKE_CIPHER_PREFIX}CIPHER(${mockNewAccessTokenPlaintext})`,
|
||||
refreshToken: `${FAKE_CIPHER_PREFIX}CIPHER(${mockRefreshTokenPlaintext})`,
|
||||
accessToken: expectedEncryptedNewAccessToken,
|
||||
refreshToken: expectedEncryptedNewRefreshToken,
|
||||
lastCredentialsRefreshedAt: expect.any(Date),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should decrypt the stored refresh token before sending to Google, then re-encrypt the rotated tokens before persisting', async () => {
|
||||
it('should decrypt the stored refresh token before sending to Google, persist the re-encrypted tokens, and return them encrypted', async () => {
|
||||
const connectedAccount = {
|
||||
id: mockConnectedAccountId,
|
||||
provider: ConnectedAccountProvider.GOOGLE,
|
||||
@@ -231,29 +218,35 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
lastCredentialsRefreshedAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
|
||||
} as ConnectedAccountEntity;
|
||||
|
||||
const newTokens = {
|
||||
const newPlaintextTokens = {
|
||||
accessToken: mockNewAccessTokenPlaintext,
|
||||
refreshToken: mockRefreshTokenPlaintext,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(googleAPIRefreshAccessTokenService, 'refreshTokens')
|
||||
.mockResolvedValue(newTokens);
|
||||
.mockResolvedValue(newPlaintextTokens);
|
||||
|
||||
const result = await service.resolveTokens(
|
||||
connectedAccount,
|
||||
mockWorkspaceId,
|
||||
);
|
||||
|
||||
expect(result).toEqual(newTokens);
|
||||
const expectedEncryptedNewAccessToken = `${FAKE_CIPHER_PREFIX}CIPHER(${mockNewAccessTokenPlaintext})`;
|
||||
const expectedEncryptedNewRefreshToken = `${FAKE_CIPHER_PREFIX}CIPHER(${mockRefreshTokenPlaintext})`;
|
||||
|
||||
expect(result).toEqual({
|
||||
accessToken: expectedEncryptedNewAccessToken,
|
||||
refreshToken: expectedEncryptedNewRefreshToken,
|
||||
});
|
||||
expect(
|
||||
googleAPIRefreshAccessTokenService.refreshTokens,
|
||||
).toHaveBeenCalledWith(mockRefreshTokenPlaintext);
|
||||
expect(connectedAccountRepository.update).toHaveBeenCalledWith(
|
||||
{ id: mockConnectedAccountId, workspaceId: mockWorkspaceId },
|
||||
expect.objectContaining({
|
||||
accessToken: `${FAKE_CIPHER_PREFIX}CIPHER(${mockNewAccessTokenPlaintext})`,
|
||||
refreshToken: `${FAKE_CIPHER_PREFIX}CIPHER(${mockRefreshTokenPlaintext})`,
|
||||
accessToken: expectedEncryptedNewAccessToken,
|
||||
refreshToken: expectedEncryptedNewRefreshToken,
|
||||
lastCredentialsRefreshedAt: expect.any(Date),
|
||||
}),
|
||||
);
|
||||
@@ -268,35 +261,41 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
lastCredentialsRefreshedAt: null,
|
||||
} as ConnectedAccountEntity;
|
||||
|
||||
const newTokens = {
|
||||
const newPlaintextTokens = {
|
||||
accessToken: mockNewAccessTokenPlaintext,
|
||||
refreshToken: mockRefreshTokenPlaintext,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(microsoftAPIRefreshAccessTokenService, 'refreshTokens')
|
||||
.mockResolvedValue(newTokens);
|
||||
.mockResolvedValue(newPlaintextTokens);
|
||||
|
||||
const result = await service.resolveTokens(
|
||||
connectedAccount,
|
||||
mockWorkspaceId,
|
||||
);
|
||||
|
||||
expect(result).toEqual(newTokens);
|
||||
const expectedEncryptedNewAccessToken = `${FAKE_CIPHER_PREFIX}CIPHER(${mockNewAccessTokenPlaintext})`;
|
||||
const expectedEncryptedNewRefreshToken = `${FAKE_CIPHER_PREFIX}CIPHER(${mockRefreshTokenPlaintext})`;
|
||||
|
||||
expect(result).toEqual({
|
||||
accessToken: expectedEncryptedNewAccessToken,
|
||||
refreshToken: expectedEncryptedNewRefreshToken,
|
||||
});
|
||||
expect(
|
||||
microsoftAPIRefreshAccessTokenService.refreshTokens,
|
||||
).toHaveBeenCalledWith(mockRefreshTokenPlaintext);
|
||||
expect(connectedAccountRepository.update).toHaveBeenCalledWith(
|
||||
{ id: mockConnectedAccountId, workspaceId: mockWorkspaceId },
|
||||
expect.objectContaining({
|
||||
accessToken: `${FAKE_CIPHER_PREFIX}CIPHER(${mockNewAccessTokenPlaintext})`,
|
||||
refreshToken: `${FAKE_CIPHER_PREFIX}CIPHER(${mockRefreshTokenPlaintext})`,
|
||||
accessToken: expectedEncryptedNewAccessToken,
|
||||
refreshToken: expectedEncryptedNewRefreshToken,
|
||||
lastCredentialsRefreshedAt: expect.any(Date),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return decrypted access token and null refresh token when access token is valid but no refresh token exists', async () => {
|
||||
it('should return the encrypted access token and null refresh token when access token is valid but no refresh token exists', async () => {
|
||||
const connectedAccount = {
|
||||
id: mockConnectedAccountId,
|
||||
provider: ConnectedAccountProvider.APP,
|
||||
@@ -311,18 +310,12 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
accessToken: mockAccessTokenPlaintext,
|
||||
accessToken: mockEncryptedAccessToken,
|
||||
refreshToken: null,
|
||||
});
|
||||
expect(
|
||||
connectedAccountTokenEncryptionService.decrypt,
|
||||
).toHaveBeenCalledWith({
|
||||
ciphertext: mockEncryptedAccessToken,
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
expect(
|
||||
connectedAccountTokenEncryptionService.decrypt,
|
||||
).toHaveBeenCalledTimes(1);
|
||||
).not.toHaveBeenCalled();
|
||||
expect(connectedAccountRepository.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -474,7 +467,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
});
|
||||
|
||||
describe('resolveTokens - OIDC/SAML', () => {
|
||||
it('should decrypt and return existing tokens for OIDC without attempting a refresh', async () => {
|
||||
it('should return existing encrypted tokens for OIDC as-is without attempting a refresh', async () => {
|
||||
const connectedAccount = {
|
||||
id: mockConnectedAccountId,
|
||||
provider: ConnectedAccountProvider.OIDC,
|
||||
@@ -489,9 +482,12 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
accessToken: mockAccessTokenPlaintext,
|
||||
refreshToken: mockRefreshTokenPlaintext,
|
||||
accessToken: mockEncryptedAccessToken,
|
||||
refreshToken: mockEncryptedRefreshToken,
|
||||
});
|
||||
expect(
|
||||
connectedAccountTokenEncryptionService.decrypt,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(
|
||||
googleAPIRefreshAccessTokenService.refreshTokens,
|
||||
).not.toHaveBeenCalled();
|
||||
@@ -501,7 +497,7 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
expect(connectedAccountRepository.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should decrypt and return existing tokens for SAML without attempting a refresh', async () => {
|
||||
it('should return existing encrypted tokens for SAML as-is without attempting a refresh', async () => {
|
||||
const connectedAccount = {
|
||||
id: mockConnectedAccountId,
|
||||
provider: ConnectedAccountProvider.SAML,
|
||||
@@ -516,9 +512,12 @@ describe('ConnectedAccountRefreshTokensService', () => {
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
accessToken: mockAccessTokenPlaintext,
|
||||
refreshToken: mockRefreshTokenPlaintext,
|
||||
accessToken: mockEncryptedAccessToken,
|
||||
refreshToken: mockEncryptedRefreshToken,
|
||||
});
|
||||
expect(
|
||||
connectedAccountTokenEncryptionService.decrypt,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(
|
||||
googleAPIRefreshAccessTokenService.refreshTokens,
|
||||
).not.toHaveBeenCalled();
|
||||
|
||||
+21
-38
@@ -12,8 +12,6 @@ import {
|
||||
ConnectedAccountRefreshAccessTokenExceptionCode,
|
||||
} from 'src/engine/metadata-modules/connected-account/exceptions/connected-account-refresh-tokens.exception';
|
||||
import { ConnectedAccountTokenEncryptionService } from 'src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/google/services/google-api-refresh-tokens.service';
|
||||
import { MicrosoftAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/services/microsoft-api-refresh-tokens.service';
|
||||
|
||||
@@ -34,7 +32,6 @@ export class ConnectedAccountRefreshTokensService {
|
||||
private readonly googleAPIRefreshAccessTokenService: GoogleAPIRefreshAccessTokenService,
|
||||
private readonly microsoftAPIRefreshAccessTokenService: MicrosoftAPIRefreshAccessTokenService,
|
||||
private readonly appOAuthRefreshAccessTokenService: AppOAuthRefreshAccessTokenService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly connectedAccountTokenEncryptionService: ConnectedAccountTokenEncryptionService,
|
||||
@InjectRepository(ConnectedAccountEntity)
|
||||
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
|
||||
@@ -52,7 +49,7 @@ export class ConnectedAccountRefreshTokensService {
|
||||
`Reusing valid access token for connected account ${connectedAccount.id.slice(0, 7)} in workspace ${workspaceId.slice(0, 7)}`,
|
||||
);
|
||||
|
||||
return this.decryptExistingTokens(connectedAccount, workspaceId);
|
||||
return this.getExistingEncryptedTokens(connectedAccount, workspaceId);
|
||||
}
|
||||
|
||||
const encryptedRefreshToken = connectedAccount.refreshToken;
|
||||
@@ -75,16 +72,11 @@ export class ConnectedAccountRefreshTokensService {
|
||||
);
|
||||
}
|
||||
|
||||
private decryptExistingTokens(
|
||||
private getExistingEncryptedTokens(
|
||||
connectedAccount: ConnectedAccountEntity,
|
||||
workspaceId: string,
|
||||
): ConnectedAccountTokens {
|
||||
const {
|
||||
accessToken: encryptedAccessToken,
|
||||
refreshToken: encryptedRefreshToken,
|
||||
} = connectedAccount;
|
||||
|
||||
if (!isDefined(encryptedAccessToken)) {
|
||||
if (!isDefined(connectedAccount.accessToken)) {
|
||||
throw new ConnectedAccountRefreshAccessTokenException(
|
||||
`Access token is required for connected account ${connectedAccount.id} in workspace ${workspaceId}`,
|
||||
ConnectedAccountRefreshAccessTokenExceptionCode.ACCESS_TOKEN_NOT_FOUND,
|
||||
@@ -92,16 +84,8 @@ export class ConnectedAccountRefreshTokensService {
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: this.connectedAccountTokenEncryptionService.decrypt({
|
||||
ciphertext: encryptedAccessToken,
|
||||
workspaceId,
|
||||
}),
|
||||
refreshToken: isDefined(encryptedRefreshToken)
|
||||
? this.connectedAccountTokenEncryptionService.decrypt({
|
||||
ciphertext: encryptedRefreshToken,
|
||||
workspaceId,
|
||||
})
|
||||
: null,
|
||||
accessToken: connectedAccount.accessToken,
|
||||
refreshToken: connectedAccount.refreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -116,35 +100,34 @@ export class ConnectedAccountRefreshTokensService {
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const connectedAccountTokens = await this.refreshTokens(
|
||||
const plaintextTokens = await this.refreshTokens(
|
||||
connectedAccount,
|
||||
decryptedRefreshToken,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const {
|
||||
encryptedAccessToken: reEncryptedAccessToken,
|
||||
encryptedAccessToken,
|
||||
encryptedRefreshToken: reEncryptedRefreshToken,
|
||||
} = this.connectedAccountTokenEncryptionService.encryptTokenPair({
|
||||
accessToken: connectedAccountTokens.accessToken,
|
||||
refreshToken: connectedAccountTokens.refreshToken,
|
||||
accessToken: plaintextTokens.accessToken,
|
||||
refreshToken: plaintextTokens.refreshToken,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
await this.connectedAccountRepository.update(
|
||||
{ id: connectedAccount.id, workspaceId },
|
||||
{
|
||||
accessToken: encryptedAccessToken,
|
||||
refreshToken: reEncryptedRefreshToken,
|
||||
lastCredentialsRefreshedAt: new Date(),
|
||||
},
|
||||
);
|
||||
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
|
||||
await this.connectedAccountRepository.update(
|
||||
{ id: connectedAccount.id, workspaceId },
|
||||
{
|
||||
accessToken: reEncryptedAccessToken,
|
||||
refreshToken: reEncryptedRefreshToken,
|
||||
lastCredentialsRefreshedAt: new Date(),
|
||||
},
|
||||
);
|
||||
}, authContext);
|
||||
|
||||
return connectedAccountTokens;
|
||||
return {
|
||||
accessToken: encryptedAccessToken,
|
||||
refreshToken: reEncryptedRefreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
async isAccessTokenStillValid(
|
||||
|
||||
+13
-13
@@ -1,16 +1,4 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`Install application should fail when entity does not exist should fail with execution error when installing non-existent app registration 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "NOT_FOUND",
|
||||
"subCode": "APPLICATION_NOT_FOUND",
|
||||
"userFriendlyMessage": "Application not found.",
|
||||
},
|
||||
"message": "Application registration with id 20202020-0000-0000-0000-000000000000 not found",
|
||||
"name": "NotFoundError",
|
||||
}
|
||||
`;
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Install application should fail when entity does not exist should fail when a role has an invalid universalIdentifier 1`] = `
|
||||
{
|
||||
@@ -46,3 +34,15 @@ exports[`Install application should fail when entity does not exist should fail
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Install application should fail when entity does not exist should fail with execution error when installing non-existent app registration 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "NOT_FOUND",
|
||||
"subCode": "APPLICATION_REGISTRATION_NOT_FOUND",
|
||||
"userFriendlyMessage": "Application registration not found.",
|
||||
},
|
||||
"message": "No application registration found for identifier "20202020-0000-0000-0000-000000000000"",
|
||||
"name": "NotFoundError",
|
||||
}
|
||||
`;
|
||||
|
||||
+1
-3
@@ -141,13 +141,11 @@ describe('Install application should return structured validation errors', () =>
|
||||
expect(uploadResult.errors).toBeUndefined();
|
||||
expect(uploadResult.data?.uploadAppTarball.id).toBeDefined();
|
||||
|
||||
const registrationId = uploadResult.data!.uploadAppTarball.id;
|
||||
|
||||
createdApplicationUniversalIdentifiers.push(universalIdentifier);
|
||||
|
||||
const { errors } = await installApplication({
|
||||
input: {
|
||||
appRegistrationId: registrationId,
|
||||
universalIdentifier,
|
||||
},
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ describe('Install application should fail when entity does not exist', () => {
|
||||
const { errors } = await installApplication({
|
||||
expectToFail: true,
|
||||
input: {
|
||||
appRegistrationId: '20202020-0000-0000-0000-000000000000',
|
||||
universalIdentifier: '20202020-0000-0000-0000-000000000000',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+109
-3
@@ -1,6 +1,11 @@
|
||||
import crypto from 'crypto';
|
||||
import { promises as fs } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
import request from 'supertest';
|
||||
import * as tar from 'tar';
|
||||
import { cleanupApplicationAndAppRegistration } from 'test/integration/metadata/suites/application/utils/cleanup-application-and-app-registration.util';
|
||||
import { type DataSource } from 'typeorm';
|
||||
|
||||
const TEST_WORKSPACE_ID = '20202020-1c25-4d02-bf25-6aeccf7ea419';
|
||||
@@ -21,8 +26,8 @@ const MARKETPLACE_QUERY = `
|
||||
`;
|
||||
|
||||
const INSTALL_MUTATION = `
|
||||
mutation InstallMarketplaceApp($universalIdentifier: String!) {
|
||||
installMarketplaceApp(universalIdentifier: $universalIdentifier) {
|
||||
mutation InstallApplication($universalIdentifier: String!) {
|
||||
installApplication(universalIdentifier: $universalIdentifier) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -178,7 +183,7 @@ describe('Marketplace Catalog Sync (integration)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('installMarketplaceApp', () => {
|
||||
describe('installApplication', () => {
|
||||
it('should fail if registration does not exist', async () => {
|
||||
const res = await gqlRequest(INSTALL_MUTATION, {
|
||||
universalIdentifier: crypto.randomUUID(),
|
||||
@@ -189,5 +194,106 @@ describe('Marketplace Catalog Sync (integration)', () => {
|
||||
'No application registration found',
|
||||
);
|
||||
});
|
||||
|
||||
it('should install a tarball app and return the application id', async () => {
|
||||
const universalIdentifier = crypto.randomUUID();
|
||||
const roleId = crypto.randomUUID();
|
||||
|
||||
const manifest = JSON.stringify({
|
||||
application: {
|
||||
universalIdentifier,
|
||||
displayName: 'Install Test App',
|
||||
description: 'App for testing installApplication',
|
||||
icon: 'IconTestPipe',
|
||||
defaultRoleUniversalIdentifier: roleId,
|
||||
applicationVariables: {},
|
||||
packageJsonChecksum: null,
|
||||
yarnLockChecksum: null,
|
||||
},
|
||||
roles: [
|
||||
{
|
||||
universalIdentifier: roleId,
|
||||
label: 'Default Role',
|
||||
description: 'Default role',
|
||||
},
|
||||
],
|
||||
skills: [],
|
||||
agents: [],
|
||||
objects: [],
|
||||
fields: [],
|
||||
logicFunctions: [],
|
||||
frontComponents: [],
|
||||
publicAssets: [],
|
||||
views: [],
|
||||
navigationMenuItems: [],
|
||||
pageLayouts: [],
|
||||
pageLayoutTabs: [],
|
||||
commandMenuItems: [],
|
||||
});
|
||||
|
||||
const packageJson = JSON.stringify({
|
||||
name: 'test-install-app',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
const tempId = crypto.randomUUID();
|
||||
const sourceDir = join(tmpdir(), `test-tarball-src-${tempId}`);
|
||||
const tarballPath = join(tmpdir(), `test-tarball-${tempId}.tar.gz`);
|
||||
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
await fs.writeFile(join(sourceDir, 'manifest.json'), manifest);
|
||||
await fs.writeFile(join(sourceDir, 'package.json'), packageJson);
|
||||
|
||||
await tar.create(
|
||||
{ file: tarballPath, gzip: true, cwd: sourceDir },
|
||||
['manifest.json', 'package.json'],
|
||||
);
|
||||
|
||||
const tarballBuffer = await fs.readFile(tarballPath);
|
||||
|
||||
await fs.rm(sourceDir, { recursive: true, force: true });
|
||||
await fs.rm(tarballPath, { force: true });
|
||||
|
||||
const UPLOAD_MUTATION = `
|
||||
mutation UploadAppTarball($file: Upload!, $universalIdentifier: String) {
|
||||
uploadAppTarball(file: $file, universalIdentifier: $universalIdentifier) {
|
||||
id
|
||||
universalIdentifier
|
||||
name
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const uploadRes = await request(baseUrl)
|
||||
.post('/metadata')
|
||||
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
|
||||
.field(
|
||||
'operations',
|
||||
JSON.stringify({
|
||||
query: UPLOAD_MUTATION,
|
||||
variables: { file: null, universalIdentifier },
|
||||
}),
|
||||
)
|
||||
.field('map', JSON.stringify({ '0': ['variables.file'] }))
|
||||
.attach('0', tarballBuffer, 'app.tar.gz')
|
||||
.expect(200);
|
||||
|
||||
expect(uploadRes.body.errors).toBeUndefined();
|
||||
expect(uploadRes.body.data.uploadAppTarball.id).toBeDefined();
|
||||
|
||||
try {
|
||||
const installRes = await gqlRequest(INSTALL_MUTATION, {
|
||||
universalIdentifier,
|
||||
}).expect(200);
|
||||
|
||||
expect(installRes.body.errors).toBeUndefined();
|
||||
expect(installRes.body.data.installApplication).toBeDefined();
|
||||
expect(installRes.body.data.installApplication.id).toBeDefined();
|
||||
} finally {
|
||||
await cleanupApplicationAndAppRegistration({
|
||||
applicationUniversalIdentifier: universalIdentifier,
|
||||
});
|
||||
}
|
||||
}, 120000);
|
||||
});
|
||||
});
|
||||
|
||||
+10
-5
@@ -1,7 +1,7 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export type InstallApplicationFactoryInput = {
|
||||
appRegistrationId: string;
|
||||
universalIdentifier: string;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
@@ -11,15 +11,20 @@ export const installApplicationQueryFactory = ({
|
||||
input: InstallApplicationFactoryInput;
|
||||
}) => ({
|
||||
query: gql`
|
||||
mutation InstallApplication($appRegistrationId: String!, $version: String) {
|
||||
mutation InstallApplication(
|
||||
$universalIdentifier: String!
|
||||
$version: String
|
||||
) {
|
||||
installApplication(
|
||||
appRegistrationId: $appRegistrationId
|
||||
universalIdentifier: $universalIdentifier
|
||||
version: $version
|
||||
)
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
appRegistrationId: input.appRegistrationId,
|
||||
universalIdentifier: input.universalIdentifier,
|
||||
version: input.version,
|
||||
},
|
||||
});
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ export const installApplication = async ({
|
||||
expectToFail?: boolean;
|
||||
token?: string;
|
||||
}): CommonResponseBody<{
|
||||
installApplication: boolean;
|
||||
installApplication: { id: string };
|
||||
}> => {
|
||||
const graphqlOperation = installApplicationQueryFactory({ input });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user