diff --git a/packages/backend/src/controllers/api/v1/admin/apps/create-config.ee.js b/packages/backend/src/controllers/api/v1/admin/apps/create-config.ee.js index edf0ff9a..5ae08ea4 100644 --- a/packages/backend/src/controllers/api/v1/admin/apps/create-config.ee.js +++ b/packages/backend/src/controllers/api/v1/admin/apps/create-config.ee.js @@ -10,12 +10,11 @@ export default async (request, response) => { }; const appConfigParams = (request) => { - const { customConnectionAllowed, shared, disabled } = request.body; + const { useOnlyPredefinedAuthClients, disabled } = request.body; return { key: request.params.appKey, - customConnectionAllowed, - shared, + useOnlyPredefinedAuthClients, disabled, }; }; diff --git a/packages/backend/src/controllers/api/v1/admin/apps/create-config.ee.test.js b/packages/backend/src/controllers/api/v1/admin/apps/create-config.ee.test.js index 9d59a699..3ee2bab4 100644 --- a/packages/backend/src/controllers/api/v1/admin/apps/create-config.ee.test.js +++ b/packages/backend/src/controllers/api/v1/admin/apps/create-config.ee.test.js @@ -23,8 +23,7 @@ describe('POST /api/v1/admin/apps/:appKey/config', () => { it('should return created app config', async () => { const appConfig = { - customConnectionAllowed: true, - shared: true, + useOnlyPredefinedAuthClients: false, disabled: false, }; @@ -38,14 +37,14 @@ describe('POST /api/v1/admin/apps/:appKey/config', () => { ...appConfig, key: 'gitlab', }); + expect(response.body).toMatchObject(expectedPayload); }); it('should return HTTP 422 for already existing app config', async () => { const appConfig = { key: 'gitlab', - customConnectionAllowed: true, - shared: true, + useOnlyPredefinedAuthClients: false, disabled: false, }; diff --git a/packages/backend/src/controllers/api/v1/admin/apps/update-config.ee.js b/packages/backend/src/controllers/api/v1/admin/apps/update-config.ee.js index 8475a264..c0d5160d 100644 --- a/packages/backend/src/controllers/api/v1/admin/apps/update-config.ee.js +++ b/packages/backend/src/controllers/api/v1/admin/apps/update-config.ee.js @@ -17,11 +17,10 @@ export default async (request, response) => { }; const appConfigParams = (request) => { - const { customConnectionAllowed, shared, disabled } = request.body; + const { useOnlyPredefinedAuthClients, disabled } = request.body; return { - customConnectionAllowed, - shared, + useOnlyPredefinedAuthClients, disabled, }; }; diff --git a/packages/backend/src/controllers/api/v1/admin/apps/update-config.ee.test.js b/packages/backend/src/controllers/api/v1/admin/apps/update-config.ee.test.js index 3b1fb8ab..5894424d 100644 --- a/packages/backend/src/controllers/api/v1/admin/apps/update-config.ee.test.js +++ b/packages/backend/src/controllers/api/v1/admin/apps/update-config.ee.test.js @@ -24,17 +24,15 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => { it('should return updated app config', async () => { const appConfig = { key: 'gitlab', - customConnectionAllowed: true, - shared: true, + useOnlyPredefinedAuthClients: true, disabled: false, }; await createAppConfig(appConfig); const newAppConfigValues = { - shared: false, disabled: true, - customConnectionAllowed: false, + useOnlyPredefinedAuthClients: false, }; const response = await request(app) @@ -53,9 +51,8 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => { it('should return not found response for unexisting app config', async () => { const appConfig = { - shared: false, disabled: true, - customConnectionAllowed: false, + useOnlyPredefinedAuthClients: false, }; await request(app) @@ -68,8 +65,7 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => { it('should return HTTP 422 for invalid app config data', async () => { const appConfig = { key: 'gitlab', - customConnectionAllowed: true, - shared: true, + useOnlyPredefinedAuthClients: true, disabled: false, }; diff --git a/packages/backend/src/controllers/api/v1/apps/create-connection.test.js b/packages/backend/src/controllers/api/v1/apps/create-connection.test.js index 4a12aa99..c73df6b6 100644 --- a/packages/backend/src/controllers/api/v1/apps/create-connection.test.js +++ b/packages/backend/src/controllers/api/v1/apps/create-connection.test.js @@ -155,7 +155,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => { await createAppConfig({ key: 'gitlab', disabled: false, - customConnectionAllowed: true, + useOnlyPredefinedAuthClients: false, }); }); @@ -218,7 +218,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => { await createAppConfig({ key: 'gitlab', disabled: false, - customConnectionAllowed: false, + useOnlyPredefinedAuthClients: true, }); }); @@ -266,14 +266,14 @@ describe('POST /api/v1/apps/:appKey/connections', () => { }); }); - describe('with auth clients enabled', async () => { + describe('with auth client enabled', async () => { let appAuthClient; beforeEach(async () => { await createAppConfig({ key: 'gitlab', disabled: false, - shared: true, + useOnlyPredefinedAuthClients: false, }); appAuthClient = await createAppAuthClient({ @@ -310,19 +310,6 @@ describe('POST /api/v1/apps/:appKey/connections', () => { expect(response.body).toStrictEqual(expectedPayload); }); - it('should return not authorized response for appAuthClientId and formattedData together', async () => { - const connectionData = { - appAuthClientId: appAuthClient.id, - formattedData: {}, - }; - - await request(app) - .post('/api/v1/apps/gitlab/connections') - .set('Authorization', token) - .send(connectionData) - .expect(403); - }); - it('should return not found response for invalid app key', async () => { await request(app) .post('/api/v1/apps/invalid-app-key/connections') @@ -349,18 +336,20 @@ describe('POST /api/v1/apps/:appKey/connections', () => { }); }); }); - describe('with auth clients disabled', async () => { + + describe('with auth client disabled', async () => { let appAuthClient; beforeEach(async () => { await createAppConfig({ key: 'gitlab', disabled: false, - shared: false, + useOnlyPredefinedAuthClients: false, }); appAuthClient = await createAppAuthClient({ appKey: 'gitlab', + active: false, }); }); @@ -373,7 +362,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => { .post('/api/v1/apps/gitlab/connections') .set('Authorization', token) .send(connectionData) - .expect(403); + .expect(404); }); it('should return not found response for invalid app key', async () => { diff --git a/packages/backend/src/controllers/api/v1/apps/get-config.ee.test.js b/packages/backend/src/controllers/api/v1/apps/get-config.ee.test.js index 75c70b25..505e492f 100644 --- a/packages/backend/src/controllers/api/v1/apps/get-config.ee.test.js +++ b/packages/backend/src/controllers/api/v1/apps/get-config.ee.test.js @@ -17,8 +17,7 @@ describe('GET /api/v1/apps/:appKey/config', () => { appConfig = await createAppConfig({ key: 'deepl', - customConnectionAllowed: true, - shared: true, + useOnlyPredefinedAuthClients: false, disabled: false, }); diff --git a/packages/backend/src/controllers/api/v1/connections/reset-connection.test.js b/packages/backend/src/controllers/api/v1/connections/reset-connection.test.js index ba4caaf9..2e94c5d6 100644 --- a/packages/backend/src/controllers/api/v1/connections/reset-connection.test.js +++ b/packages/backend/src/controllers/api/v1/connections/reset-connection.test.js @@ -47,7 +47,6 @@ describe('POST /api/v1/connections/:connectionId/reset', () => { const expectedPayload = resetConnectionMock({ ...refetchedCurrentUserConnection, - reconnectable: refetchedCurrentUserConnection.reconnectable, formattedData: { screenName: 'Connection name', }, diff --git a/packages/backend/src/controllers/api/v1/connections/update-connection.test.js b/packages/backend/src/controllers/api/v1/connections/update-connection.test.js index 988da4fa..5902e361 100644 --- a/packages/backend/src/controllers/api/v1/connections/update-connection.test.js +++ b/packages/backend/src/controllers/api/v1/connections/update-connection.test.js @@ -55,10 +55,9 @@ describe('PATCH /api/v1/connections/:connectionId', () => { const refetchedCurrentUserConnection = await currentUserConnection.$query(); - const expectedPayload = updateConnectionMock({ - ...refetchedCurrentUserConnection, - reconnectable: refetchedCurrentUserConnection.reconnectable, - }); + const expectedPayload = updateConnectionMock( + refetchedCurrentUserConnection + ); expect(response.body).toStrictEqual(expectedPayload); }); diff --git a/packages/backend/src/db/migrations/20241204103153_add_use_only_predefined_auth_clients_in_app_config.js b/packages/backend/src/db/migrations/20241204103153_add_use_only_predefined_auth_clients_in_app_config.js new file mode 100644 index 00000000..1865f05a --- /dev/null +++ b/packages/backend/src/db/migrations/20241204103153_add_use_only_predefined_auth_clients_in_app_config.js @@ -0,0 +1,11 @@ +export async function up(knex) { + return await knex.schema.alterTable('app_configs', (table) => { + table.boolean('use_only_predefined_auth_clients').defaultTo(false); + }); +} + +export async function down(knex) { + return await knex.schema.alterTable('app_configs', (table) => { + table.dropColumn('use_only_predefined_auth_clients'); + }); +} diff --git a/packages/backend/src/db/migrations/20241204103355_remove_obsolete_fields_in_app_config.js b/packages/backend/src/db/migrations/20241204103355_remove_obsolete_fields_in_app_config.js new file mode 100644 index 00000000..a99bc9e7 --- /dev/null +++ b/packages/backend/src/db/migrations/20241204103355_remove_obsolete_fields_in_app_config.js @@ -0,0 +1,15 @@ +export async function up(knex) { + return await knex.schema.alterTable('app_configs', (table) => { + table.dropColumn('shared'); + table.dropColumn('connection_allowed'); + table.dropColumn('custom_connection_allowed'); + }); +} + +export async function down(knex) { + return await knex.schema.alterTable('app_configs', (table) => { + table.boolean('shared').defaultTo(false); + table.boolean('connection_allowed').defaultTo(false); + table.boolean('custom_connection_allowed').defaultTo(false); + }); +} diff --git a/packages/backend/src/models/__snapshots__/app-config.test.js.snap b/packages/backend/src/models/__snapshots__/app-config.test.js.snap index aea9fa56..38ca2039 100644 --- a/packages/backend/src/models/__snapshots__/app-config.test.js.snap +++ b/packages/backend/src/models/__snapshots__/app-config.test.js.snap @@ -3,17 +3,9 @@ exports[`AppConfig model > jsonSchema should have correct validations 1`] = ` { "properties": { - "connectionAllowed": { - "default": false, - "type": "boolean", - }, "createdAt": { "type": "string", }, - "customConnectionAllowed": { - "default": false, - "type": "boolean", - }, "disabled": { "default": false, "type": "boolean", @@ -25,13 +17,13 @@ exports[`AppConfig model > jsonSchema should have correct validations 1`] = ` "key": { "type": "string", }, - "shared": { - "default": false, - "type": "boolean", - }, "updatedAt": { "type": "string", }, + "useOnlyPredefinedAuthClients": { + "default": false, + "type": "boolean", + }, }, "required": [ "key", diff --git a/packages/backend/src/models/app-auth-client.js b/packages/backend/src/models/app-auth-client.js index 90a9bda3..48800841 100644 --- a/packages/backend/src/models/app-auth-client.js +++ b/packages/backend/src/models/app-auth-client.js @@ -60,39 +60,26 @@ class AppAuthClient extends Base { return this.authDefaults ? true : false; } - async triggerAppConfigUpdate() { - const appConfig = await this.$relatedQuery('appConfig'); - - // This is a workaround to update connection allowed column for AppConfig - await appConfig?.$query().patch({ - key: appConfig.key, - shared: appConfig.shared, - disabled: appConfig.disabled, - }); - } - // TODO: Make another abstraction like beforeSave instead of using // beforeInsert and beforeUpdate separately for the same operation. async $beforeInsert(queryContext) { await super.$beforeInsert(queryContext); + this.encryptData(); } async $afterInsert(queryContext) { await super.$afterInsert(queryContext); - - await this.triggerAppConfigUpdate(); } async $beforeUpdate(opt, queryContext) { await super.$beforeUpdate(opt, queryContext); + this.encryptData(); } async $afterUpdate(opt, queryContext) { await super.$afterUpdate(opt, queryContext); - - await this.triggerAppConfigUpdate(); } async $afterFind() { diff --git a/packages/backend/src/models/app-auth-client.test.js b/packages/backend/src/models/app-auth-client.test.js index af1fefc2..bc4be9fc 100644 --- a/packages/backend/src/models/app-auth-client.test.js +++ b/packages/backend/src/models/app-auth-client.test.js @@ -7,7 +7,6 @@ import AppAuthClient from './app-auth-client.js'; import Base from './base.js'; import appConfig from '../config/app.js'; import { createAppAuthClient } from '../../test/factories/app-auth-client.js'; -import { createAppConfig } from '../../test/factories/app-config.js'; describe('AppAuthClient model', () => { it('tableName should return correct name', () => { @@ -164,63 +163,6 @@ describe('AppAuthClient model', () => { }); }); - describe('triggerAppConfigUpdate', () => { - it('should trigger an update in related app config', async () => { - await createAppConfig({ key: 'gitlab' }); - - const appAuthClient = await createAppAuthClient({ - appKey: 'gitlab', - }); - - const appConfigBeforeUpdateSpy = vi.spyOn( - AppConfig.prototype, - '$beforeUpdate' - ); - - await appAuthClient.triggerAppConfigUpdate(); - - expect(appConfigBeforeUpdateSpy).toHaveBeenCalledOnce(); - }); - - it('should update related AppConfig after creating an instance', async () => { - const appConfig = await createAppConfig({ - key: 'gitlab', - disabled: false, - shared: true, - }); - - await createAppAuthClient({ - appKey: 'gitlab', - active: true, - }); - - const refetchedAppConfig = await appConfig.$query(); - - expect(refetchedAppConfig.connectionAllowed).toBe(true); - }); - - it('should update related AppConfig after updating an instance', async () => { - const appConfig = await createAppConfig({ - key: 'gitlab', - disabled: false, - shared: true, - }); - - const appAuthClient = await createAppAuthClient({ - appKey: 'gitlab', - active: false, - }); - - let refetchedAppConfig = await appConfig.$query(); - expect(refetchedAppConfig.connectionAllowed).toBe(false); - - await appAuthClient.$query().patchAndFetch({ active: true }); - - refetchedAppConfig = await appConfig.$query(); - expect(refetchedAppConfig.connectionAllowed).toBe(true); - }); - }); - it('$beforeInsert should call AppAuthClient.encryptData', async () => { const appAuthClientBeforeInsertSpy = vi.spyOn( AppAuthClient.prototype, @@ -232,17 +174,6 @@ describe('AppAuthClient model', () => { expect(appAuthClientBeforeInsertSpy).toHaveBeenCalledOnce(); }); - it('$afterInsert should call AppAuthClient.triggerAppConfigUpdate', async () => { - const appAuthClientAfterInsertSpy = vi.spyOn( - AppAuthClient.prototype, - 'triggerAppConfigUpdate' - ); - - await createAppAuthClient(); - - expect(appAuthClientAfterInsertSpy).toHaveBeenCalledOnce(); - }); - it('$beforeUpdate should call AppAuthClient.encryptData', async () => { const appAuthClient = await createAppAuthClient(); @@ -256,19 +187,6 @@ describe('AppAuthClient model', () => { expect(appAuthClientBeforeUpdateSpy).toHaveBeenCalledOnce(); }); - it('$afterUpdate should call AppAuthClient.triggerAppConfigUpdate', async () => { - const appAuthClient = await createAppAuthClient(); - - const appAuthClientAfterUpdateSpy = vi.spyOn( - AppAuthClient.prototype, - 'triggerAppConfigUpdate' - ); - - await appAuthClient.$query().patchAndFetch({ name: 'sample' }); - - expect(appAuthClientAfterUpdateSpy).toHaveBeenCalledOnce(); - }); - it('$afterFind should call AppAuthClient.decryptData', async () => { const appAuthClient = await createAppAuthClient(); diff --git a/packages/backend/src/models/app-config.js b/packages/backend/src/models/app-config.js index 1a9176b9..6763e9f8 100644 --- a/packages/backend/src/models/app-config.js +++ b/packages/backend/src/models/app-config.js @@ -16,9 +16,7 @@ class AppConfig extends Base { properties: { id: { type: 'string', format: 'uuid' }, key: { type: 'string' }, - connectionAllowed: { type: 'boolean', default: false }, - customConnectionAllowed: { type: 'boolean', default: false }, - shared: { type: 'boolean', default: false }, + useOnlyPredefinedAuthClients: { type: 'boolean', default: false }, disabled: { type: 'boolean', default: false }, createdAt: { type: 'string' }, updatedAt: { type: 'string' }, @@ -41,39 +39,6 @@ class AppConfig extends Base { return await App.findOneByKey(this.key); } - - async computeAndAssignConnectionAllowedProperty() { - this.connectionAllowed = await this.computeConnectionAllowedProperty(); - } - - async computeConnectionAllowedProperty() { - const appAuthClients = await this.$relatedQuery('appAuthClients'); - - const hasSomeActiveAppAuthClients = - appAuthClients?.some((appAuthClient) => appAuthClient.active) || false; - - const conditions = [ - hasSomeActiveAppAuthClients, - this.shared, - !this.disabled, - ]; - - const connectionAllowed = conditions.every(Boolean); - - return connectionAllowed; - } - - async $beforeInsert(queryContext) { - await super.$beforeInsert(queryContext); - - await this.computeAndAssignConnectionAllowedProperty(); - } - - async $beforeUpdate(opt, queryContext) { - await super.$beforeUpdate(opt, queryContext); - - await this.computeAndAssignConnectionAllowedProperty(); - } } export default AppConfig; diff --git a/packages/backend/src/models/app-config.test.js b/packages/backend/src/models/app-config.test.js index 4945066c..2e6f05be 100644 --- a/packages/backend/src/models/app-config.test.js +++ b/packages/backend/src/models/app-config.test.js @@ -1,11 +1,9 @@ -import { vi, describe, it, expect } from 'vitest'; +import { describe, it, expect } from 'vitest'; import Base from './base.js'; import AppConfig from './app-config.js'; import App from './app.js'; import AppAuthClient from './app-auth-client.js'; -import { createAppConfig } from '../../test/factories/app-config.js'; -import { createAppAuthClient } from '../../test/factories/app-auth-client.js'; describe('AppConfig model', () => { it('tableName should return correct name', () => { @@ -55,126 +53,4 @@ describe('AppConfig model', () => { expect(app).toStrictEqual(expectedApp); }); }); - - describe('computeAndAssignConnectionAllowedProperty', () => { - it('should call computeConnectionAllowedProperty and assign the result', async () => { - const appConfig = await createAppConfig(); - - const computeConnectionAllowedPropertySpy = vi - .spyOn(appConfig, 'computeConnectionAllowedProperty') - .mockResolvedValue(true); - - await appConfig.computeAndAssignConnectionAllowedProperty(); - - expect(computeConnectionAllowedPropertySpy).toHaveBeenCalled(); - expect(appConfig.connectionAllowed).toBe(true); - }); - }); - - describe('computeConnectionAllowedProperty', () => { - it('should return true when app is enabled, shared and allows custom connection with an active app auth client', async () => { - await createAppAuthClient({ - appKey: 'deepl', - active: true, - }); - - await createAppAuthClient({ - appKey: 'deepl', - active: false, - }); - - const appConfig = await createAppConfig({ - disabled: false, - customConnectionAllowed: true, - shared: true, - key: 'deepl', - }); - - const connectionAllowed = - await appConfig.computeConnectionAllowedProperty(); - - expect(connectionAllowed).toBe(true); - }); - - it('should return false if there is no active app auth client', async () => { - await createAppAuthClient({ - appKey: 'deepl', - active: false, - }); - - const appConfig = await createAppConfig({ - disabled: false, - customConnectionAllowed: true, - shared: true, - key: 'deepl', - }); - - const connectionAllowed = - await appConfig.computeConnectionAllowedProperty(); - - expect(connectionAllowed).toBe(false); - }); - - it('should return false if there is no app auth clients', async () => { - const appConfig = await createAppConfig({ - disabled: false, - customConnectionAllowed: true, - shared: true, - key: 'deepl', - }); - - const connectionAllowed = - await appConfig.computeConnectionAllowedProperty(); - - expect(connectionAllowed).toBe(false); - }); - - it('should return false when app is disabled', async () => { - const appConfig = await createAppConfig({ - disabled: true, - customConnectionAllowed: true, - }); - - const connectionAllowed = - await appConfig.computeConnectionAllowedProperty(); - - expect(connectionAllowed).toBe(false); - }); - - it(`should return false when app doesn't allow custom connection`, async () => { - const appConfig = await createAppConfig({ - disabled: false, - customConnectionAllowed: false, - }); - - const connectionAllowed = - await appConfig.computeConnectionAllowedProperty(); - - expect(connectionAllowed).toBe(false); - }); - }); - - it('$beforeInsert should call computeAndAssignConnectionAllowedProperty', async () => { - const computeAndAssignConnectionAllowedPropertySpy = vi - .spyOn(AppConfig.prototype, 'computeAndAssignConnectionAllowedProperty') - .mockResolvedValue(true); - - await createAppConfig(); - - expect(computeAndAssignConnectionAllowedPropertySpy).toHaveBeenCalledOnce(); - }); - - it('$beforeUpdate should call computeAndAssignConnectionAllowedProperty', async () => { - const appConfig = await createAppConfig(); - - const computeAndAssignConnectionAllowedPropertySpy = vi - .spyOn(AppConfig.prototype, 'computeAndAssignConnectionAllowedProperty') - .mockResolvedValue(true); - - await appConfig.$query().patch({ - key: 'deepl', - }); - - expect(computeAndAssignConnectionAllowedPropertySpy).toHaveBeenCalledOnce(); - }); }); diff --git a/packages/backend/src/models/connection.js b/packages/backend/src/models/connection.js index 325e1e07..4a8d5351 100644 --- a/packages/backend/src/models/connection.js +++ b/packages/backend/src/models/connection.js @@ -33,10 +33,6 @@ class Connection extends Base { }, }; - static get virtualAttributes() { - return ['reconnectable']; - } - static relationMappings = () => ({ user: { relation: Base.BelongsToOneRelation, @@ -83,18 +79,6 @@ class Connection extends Base { }, }); - get reconnectable() { - if (this.appAuthClientId) { - return this.appAuthClient.active; - } - - if (this.appConfig) { - return !this.appConfig.disabled && this.appConfig.customConnectionAllowed; - } - - return true; - } - encryptData() { if (!this.eligibleForEncryption()) return; @@ -144,19 +128,13 @@ class Connection extends Base { ); } - if (!appConfig.customConnectionAllowed && this.formattedData) { + if (appConfig.useOnlyPredefinedAuthClients && this.formattedData) { throw new NotAuthorizedError( `New custom connections have been disabled for ${app.name}!` ); } - if (!appConfig.shared && this.appAuthClientId) { - throw new NotAuthorizedError( - 'The connection with the given app auth client is not allowed!' - ); - } - - if (appConfig.shared && !this.formattedData) { + if (!this.formattedData) { const authClient = await appConfig .$relatedQuery('appAuthClients') .findById(this.appAuthClientId) diff --git a/packages/backend/src/models/connection.test.js b/packages/backend/src/models/connection.test.js index 7c5057bb..329fdfe6 100644 --- a/packages/backend/src/models/connection.test.js +++ b/packages/backend/src/models/connection.test.js @@ -23,14 +23,6 @@ describe('Connection model', () => { expect(Connection.jsonSchema).toMatchSnapshot(); }); - it('virtualAttributes should return correct attributes', () => { - const virtualAttributes = Connection.virtualAttributes; - - const expectedAttributes = ['reconnectable']; - - expect(virtualAttributes).toStrictEqual(expectedAttributes); - }); - describe('relationMappings', () => { it('should return correct associations', () => { const relationMappings = Connection.relationMappings(); @@ -92,78 +84,6 @@ describe('Connection model', () => { }); }); - describe('reconnectable', () => { - it('should return active status of app auth client when created via app auth client', async () => { - const appAuthClient = await createAppAuthClient({ - active: true, - formattedAuthDefaults: { - clientId: 'sample-id', - }, - }); - - const connection = await createConnection({ - appAuthClientId: appAuthClient.id, - formattedData: { - token: 'sample-token', - }, - }); - - const connectionWithAppAuthClient = await connection - .$query() - .withGraphFetched({ - appAuthClient: true, - }); - - expect(connectionWithAppAuthClient.reconnectable).toBe(true); - }); - - it('should return true when app config is not disabled and allows custom connection', async () => { - const appConfig = await createAppConfig({ - key: 'gitlab', - disabled: false, - customConnectionAllowed: true, - }); - - const connection = await createConnection({ - key: appConfig.key, - formattedData: { - token: 'sample-token', - }, - }); - - const connectionWithAppAuthClient = await connection - .$query() - .withGraphFetched({ - appConfig: true, - }); - - expect(connectionWithAppAuthClient.reconnectable).toBe(true); - }); - - it('should return false when app config is disabled or does not allow custom connection', async () => { - const connection = await createConnection({ - key: 'gitlab', - formattedData: { - token: 'sample-token', - }, - }); - - await createAppConfig({ - key: 'gitlab', - disabled: true, - customConnectionAllowed: false, - }); - - const connectionWithAppAuthClient = await connection - .$query() - .withGraphFetched({ - appConfig: true, - }); - - expect(connectionWithAppAuthClient.reconnectable).toBe(false); - }); - }); - describe('encryptData', () => { it('should return undefined if eligibleForEncryption is not true', async () => { vi.spyOn(Connection.prototype, 'eligibleForEncryption').mockReturnValue( @@ -366,6 +286,7 @@ describe('Connection model', () => { ); }); + // TODO: update test case name it('should throw an error when app config does not allow custom connection with formatted data', async () => { vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({ name: 'gitlab', @@ -373,7 +294,7 @@ describe('Connection model', () => { vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({ disabled: false, - customConnectionAllowed: false, + useOnlyPredefinedAuthClients: true, }); const connection = new Connection(); @@ -386,32 +307,10 @@ describe('Connection model', () => { ); }); - it('should throw an error when app config is not shared with app auth client', async () => { - vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({ - name: 'gitlab', - }); - - vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({ - disabled: false, - shared: false, - }); - - const connection = new Connection(); - connection.appAuthClientId = 'sample-id'; - - await expect(() => - connection.checkEligibilityForCreation() - ).rejects.toThrow( - 'The connection with the given app auth client is not allowed!' - ); - }); - it('should apply app auth client auth defaults when creating with shared app auth client', async () => { await createAppConfig({ key: 'gitlab', disabled: false, - customConnectionAllowed: true, - shared: true, }); const appAuthClient = await createAppAuthClient({ diff --git a/packages/backend/src/models/user.test.js b/packages/backend/src/models/user.test.js index 4b4d0cd3..159af7ee 100644 --- a/packages/backend/src/models/user.test.js +++ b/packages/backend/src/models/user.test.js @@ -376,7 +376,10 @@ describe('User model', () => { const anotherUserConnection = await createConnection(); expect( - await userWithRoleAndPermissions.authorizedConnections + await userWithRoleAndPermissions.authorizedConnections.orderBy( + 'created_at', + 'asc' + ) ).toStrictEqual([userConnection, anotherUserConnection]); }); diff --git a/packages/backend/src/serializers/app-config.js b/packages/backend/src/serializers/app-config.js index d5f17ef2..82888815 100644 --- a/packages/backend/src/serializers/app-config.js +++ b/packages/backend/src/serializers/app-config.js @@ -1,10 +1,8 @@ const appConfigSerializer = (appConfig) => { return { key: appConfig.key, - customConnectionAllowed: appConfig.customConnectionAllowed, - shared: appConfig.shared, + useOnlyPredefinedAuthClients: appConfig.useOnlyPredefinedAuthClients, disabled: appConfig.disabled, - connectionAllowed: appConfig.connectionAllowed, createdAt: appConfig.createdAt.getTime(), updatedAt: appConfig.updatedAt.getTime(), }; diff --git a/packages/backend/src/serializers/app-config.test.js b/packages/backend/src/serializers/app-config.test.js index 61a46a1c..5ccdd026 100644 --- a/packages/backend/src/serializers/app-config.test.js +++ b/packages/backend/src/serializers/app-config.test.js @@ -12,10 +12,8 @@ describe('appConfig serializer', () => { it('should return app config data', async () => { const expectedPayload = { key: appConfig.key, - customConnectionAllowed: appConfig.customConnectionAllowed, - shared: appConfig.shared, + useOnlyPredefinedAuthClients: appConfig.useOnlyPredefinedAuthClients, disabled: appConfig.disabled, - connectionAllowed: appConfig.connectionAllowed, createdAt: appConfig.createdAt.getTime(), updatedAt: appConfig.updatedAt.getTime(), }; diff --git a/packages/backend/src/serializers/auth.js b/packages/backend/src/serializers/auth.js index c5d60a4e..da942e6f 100644 --- a/packages/backend/src/serializers/auth.js +++ b/packages/backend/src/serializers/auth.js @@ -2,7 +2,9 @@ const authSerializer = (auth) => { return { fields: auth.fields, authenticationSteps: auth.authenticationSteps, + sharedAuthenticationSteps: auth.sharedAuthenticationSteps, reconnectionSteps: auth.reconnectionSteps, + sharedReconnectionSteps: auth.sharedReconnectionSteps, }; }; diff --git a/packages/backend/src/serializers/auth.test.js b/packages/backend/src/serializers/auth.test.js index e9adb259..ef2d1bd6 100644 --- a/packages/backend/src/serializers/auth.test.js +++ b/packages/backend/src/serializers/auth.test.js @@ -10,6 +10,8 @@ describe('authSerializer', () => { fields: auth.fields, authenticationSteps: auth.authenticationSteps, reconnectionSteps: auth.reconnectionSteps, + sharedAuthenticationSteps: auth.sharedAuthenticationSteps, + sharedReconnectionSteps: auth.sharedReconnectionSteps, }; expect(authSerializer(auth)).toStrictEqual(expectedPayload); diff --git a/packages/backend/src/serializers/connection.js b/packages/backend/src/serializers/connection.js index e285f1e2..388a6b87 100644 --- a/packages/backend/src/serializers/connection.js +++ b/packages/backend/src/serializers/connection.js @@ -2,7 +2,6 @@ const connectionSerializer = (connection) => { return { id: connection.id, key: connection.key, - reconnectable: connection.reconnectable, appAuthClientId: connection.appAuthClientId, formattedData: { screenName: connection.formattedData.screenName, diff --git a/packages/backend/src/serializers/connection.test.js b/packages/backend/src/serializers/connection.test.js index c322af6b..3ea7b324 100644 --- a/packages/backend/src/serializers/connection.test.js +++ b/packages/backend/src/serializers/connection.test.js @@ -13,7 +13,6 @@ describe('connectionSerializer', () => { const expectedPayload = { id: connection.id, key: connection.key, - reconnectable: connection.reconnectable, appAuthClientId: connection.appAuthClientId, formattedData: { screenName: connection.formattedData.screenName, diff --git a/packages/backend/test/mocks/rest/api/v1/admin/apps/create-config.js b/packages/backend/test/mocks/rest/api/v1/admin/apps/create-config.js index 52e425ab..8fb199d7 100644 --- a/packages/backend/test/mocks/rest/api/v1/admin/apps/create-config.js +++ b/packages/backend/test/mocks/rest/api/v1/admin/apps/create-config.js @@ -2,8 +2,7 @@ const createAppConfigMock = (appConfig) => { return { data: { key: appConfig.key, - customConnectionAllowed: appConfig.customConnectionAllowed, - shared: appConfig.shared, + useOnlyPredefinedAuthClients: appConfig.useOnlyPredefinedAuthClients, disabled: appConfig.disabled, }, meta: { diff --git a/packages/backend/test/mocks/rest/api/v1/apps/create-connection.js b/packages/backend/test/mocks/rest/api/v1/apps/create-connection.js index 9e993a4c..2eb1fd7f 100644 --- a/packages/backend/test/mocks/rest/api/v1/apps/create-connection.js +++ b/packages/backend/test/mocks/rest/api/v1/apps/create-connection.js @@ -2,7 +2,6 @@ const createConnection = (connection) => { const connectionData = { id: connection.id, key: connection.key, - reconnectable: connection.reconnectable || true, appAuthClientId: connection.appAuthClientId, formattedData: connection.formattedData, verified: connection.verified || false, diff --git a/packages/backend/test/mocks/rest/api/v1/apps/get-auth.js b/packages/backend/test/mocks/rest/api/v1/apps/get-auth.js index 68ea18cd..d42b9724 100644 --- a/packages/backend/test/mocks/rest/api/v1/apps/get-auth.js +++ b/packages/backend/test/mocks/rest/api/v1/apps/get-auth.js @@ -4,6 +4,8 @@ const getAuthMock = (auth) => { fields: auth.fields, authenticationSteps: auth.authenticationSteps, reconnectionSteps: auth.reconnectionSteps, + sharedReconnectionSteps: auth.sharedReconnectionSteps, + sharedAuthenticationSteps: auth.sharedAuthenticationSteps, }, meta: { count: 1, diff --git a/packages/backend/test/mocks/rest/api/v1/apps/get-config.js b/packages/backend/test/mocks/rest/api/v1/apps/get-config.js index 3cb4ab11..97827f59 100644 --- a/packages/backend/test/mocks/rest/api/v1/apps/get-config.js +++ b/packages/backend/test/mocks/rest/api/v1/apps/get-config.js @@ -2,10 +2,8 @@ const getAppConfigMock = (appConfig) => { return { data: { key: appConfig.key, - customConnectionAllowed: appConfig.customConnectionAllowed, - shared: appConfig.shared, + useOnlyPredefinedAuthClients: appConfig.useOnlyPredefinedAuthClients, disabled: appConfig.disabled, - connectionAllowed: appConfig.connectionAllowed, createdAt: appConfig.createdAt.getTime(), updatedAt: appConfig.updatedAt.getTime(), }, diff --git a/packages/backend/test/mocks/rest/api/v1/apps/get-connections.js b/packages/backend/test/mocks/rest/api/v1/apps/get-connections.js index a6242e80..bd3bfa4c 100644 --- a/packages/backend/test/mocks/rest/api/v1/apps/get-connections.js +++ b/packages/backend/test/mocks/rest/api/v1/apps/get-connections.js @@ -3,7 +3,6 @@ const getConnectionsMock = (connections) => { data: connections.map((connection) => ({ id: connection.id, key: connection.key, - reconnectable: connection.reconnectable, verified: connection.verified, appAuthClientId: connection.appAuthClientId, formattedData: { diff --git a/packages/backend/test/mocks/rest/api/v1/connections/reset-connection.js b/packages/backend/test/mocks/rest/api/v1/connections/reset-connection.js index 7d95fa10..0d8131c8 100644 --- a/packages/backend/test/mocks/rest/api/v1/connections/reset-connection.js +++ b/packages/backend/test/mocks/rest/api/v1/connections/reset-connection.js @@ -3,7 +3,6 @@ const resetConnectionMock = (connection) => { id: connection.id, key: connection.key, verified: connection.verified, - reconnectable: connection.reconnectable, appAuthClientId: connection.appAuthClientId, formattedData: { screenName: connection.formattedData.screenName, diff --git a/packages/backend/test/mocks/rest/api/v1/connections/update-connection.js b/packages/backend/test/mocks/rest/api/v1/connections/update-connection.js index b059d27e..d46b9a0c 100644 --- a/packages/backend/test/mocks/rest/api/v1/connections/update-connection.js +++ b/packages/backend/test/mocks/rest/api/v1/connections/update-connection.js @@ -3,7 +3,6 @@ const updateConnectionMock = (connection) => { id: connection.id, key: connection.key, verified: connection.verified, - reconnectable: connection.reconnectable, appAuthClientId: connection.appAuthClientId, formattedData: { screenName: connection.formattedData.screenName, diff --git a/packages/backend/test/mocks/rest/api/v1/steps/get-connection.js b/packages/backend/test/mocks/rest/api/v1/steps/get-connection.js index 3f6c8abb..831a148a 100644 --- a/packages/backend/test/mocks/rest/api/v1/steps/get-connection.js +++ b/packages/backend/test/mocks/rest/api/v1/steps/get-connection.js @@ -3,7 +3,6 @@ const getConnectionMock = async (connection) => { id: connection.id, key: connection.key, verified: connection.verified, - reconnectable: connection.reconnectable, appAuthClientId: connection.appAuthClientId, formattedData: { screenName: connection.formattedData.screenName, diff --git a/packages/backend/vitest.config.js b/packages/backend/vitest.config.js index a62dfa53..645c7fbf 100644 --- a/packages/backend/vitest.config.js +++ b/packages/backend/vitest.config.js @@ -16,10 +16,10 @@ export default defineConfig({ include: ['**/src/models/**', '**/src/controllers/**'], thresholds: { autoUpdate: true, - statements: 93.41, - branches: 93.46, - functions: 95.95, - lines: 93.41, + statements: 95.16, + branches: 94.66, + functions: 97.65, + lines: 95.16, }, }, }, diff --git a/packages/e2e-tests/fixtures/admin/application-auth-clients-page.js b/packages/e2e-tests/fixtures/admin/application-auth-clients-page.js index bedddbf4..c1b852a1 100644 --- a/packages/e2e-tests/fixtures/admin/application-auth-clients-page.js +++ b/packages/e2e-tests/fixtures/admin/application-auth-clients-page.js @@ -9,12 +9,18 @@ export class AdminApplicationAuthClientsPage extends AuthenticatedPage { constructor(page) { super(page); - this.authClientsTab = this.page.getByText('AUTH CLIENTS'); + this.authClientsTab = this.page.getByTestId('auth-clients-tab'); this.saveButton = this.page.getByTestId('submitButton'); - this.successSnackbar = this.page.getByTestId('snackbar-save-admin-apps-settings-success'); + this.successSnackbar = this.page.getByTestId( + 'snackbar-save-admin-apps-settings-success' + ); this.createFirstAuthClientButton = this.page.getByTestId('no-results'); - this.createAuthClientButton = this.page.getByTestId('create-auth-client-button'); - this.submitAuthClientFormButton = this.page.getByTestId('submit-auth-client-form'); + this.createAuthClientButton = this.page.getByTestId( + 'create-auth-client-button' + ); + this.submitAuthClientFormButton = this.page.getByTestId( + 'submit-auth-client-form' + ); this.authClientEntry = this.page.getByTestId('auth-client'); } @@ -35,6 +41,8 @@ export class AdminApplicationAuthClientsPage extends AuthenticatedPage { } async authClientShouldBeVisible(authClientName) { - await expect(this.authClientEntry.filter({ hasText: authClientName })).toBeVisible(); + await expect( + this.authClientEntry.filter({ hasText: authClientName }) + ).toBeVisible(); } } diff --git a/packages/e2e-tests/fixtures/admin/application-settings-page.js b/packages/e2e-tests/fixtures/admin/application-settings-page.js index 57858ccb..2e756d28 100644 --- a/packages/e2e-tests/fixtures/admin/application-settings-page.js +++ b/packages/e2e-tests/fixtures/admin/application-settings-page.js @@ -8,56 +8,45 @@ export class AdminApplicationSettingsPage extends AuthenticatedPage { constructor(page) { super(page); - this.allowCustomConnectionsSwitch = this.page.locator( - '[name="customConnectionAllowed"]' + this.useOnlyPredefinedAuthClients = page.locator( + '[name="useOnlyPredefinedAuthClients"]' ); - this.allowSharedConnectionsSwitch = this.page.locator('[name="shared"]'); - this.disableConnectionsSwitch = this.page.locator('[name="disabled"]'); - this.saveButton = this.page.getByTestId('submit-button'); - this.successSnackbar = this.page.getByTestId( + this.disableConnectionsSwitch = page.locator('[name="disabled"]'); + this.saveButton = page.getByTestId('submit-button'); + this.successSnackbar = page.getByTestId( 'snackbar-save-admin-apps-settings-success' ); } - async allowCustomConnections() { - await expect(this.disableConnectionsSwitch).not.toBeChecked(); - await this.allowCustomConnectionsSwitch.check(); - await this.saveButton.click(); + async allowUseOnlyPredefinedAuthClients() { + await expect(this.useOnlyPredefinedAuthClients).not.toBeChecked(); + await this.useOnlyPredefinedAuthClients.check(); } - async allowSharedConnections() { - await expect(this.disableConnectionsSwitch).not.toBeChecked(); - await this.allowSharedConnectionsSwitch.check(); - await this.saveButton.click(); + async disallowUseOnlyPredefinedAuthClients() { + await expect(this.useOnlyPredefinedAuthClients).toBeChecked(); + await this.useOnlyPredefinedAuthClients.uncheck(); + await expect(this.useOnlyPredefinedAuthClients).not.toBeChecked(); } async disallowConnections() { await expect(this.disableConnectionsSwitch).not.toBeChecked(); await this.disableConnectionsSwitch.check(); - await this.saveButton.click(); - } - - async disallowCustomConnections() { - await expect(this.disableConnectionsSwitch).toBeChecked(); - await this.allowCustomConnectionsSwitch.uncheck(); - await this.saveButton.click(); - } - - async disallowSharedConnections() { - await expect(this.disableConnectionsSwitch).toBeChecked(); - await this.allowSharedConnectionsSwitch.uncheck(); - await this.saveButton.click(); } async allowConnections() { await expect(this.disableConnectionsSwitch).toBeChecked(); await this.disableConnectionsSwitch.uncheck(); + } + + async saveSettings() { await this.saveButton.click(); } async expectSuccessSnackbarToBeVisible() { - await expect(this.successSnackbar).toHaveCount(1); - await this.successSnackbar.click(); - await expect(this.successSnackbar).toHaveCount(0); + const snackbars = await this.successSnackbar.all(); + for (const snackbar of snackbars) { + await expect(snackbar).toBeVisible(); + } } } diff --git a/packages/e2e-tests/helpers/db-helpers.js b/packages/e2e-tests/helpers/db-helpers.js new file mode 100644 index 00000000..6ba0bb6f --- /dev/null +++ b/packages/e2e-tests/helpers/db-helpers.js @@ -0,0 +1,32 @@ +const { expect } = require('../fixtures/index'); +const { pgPool } = require('../fixtures/postgres-config'); + +export const insertAppConnection = async (appName) => { + const queryUser = { + text: 'SELECT * FROM users WHERE email = $1', + values: [process.env.LOGIN_EMAIL], + }; + + try { + const queryUserResult = await pgPool.query(queryUser); + expect(queryUserResult.rowCount).toEqual(1); + + const createConnection = { + text: 'INSERT INTO connections (key, data, user_id, verified, draft) VALUES ($1, $2, $3, $4, $5)', + values: [ + appName, + 'U2FsdGVkX1+cAtdHwLiuRL4DaK/T1aljeeKyPMmtWK0AmAIsKhYwQiuyQCYJO3mdZ31z73hqF2Y+yj2Kn2/IIpLRqCxB2sC0rCDCZyolzOZ290YcBXSzYRzRUxhoOcZEtwYDKsy8AHygKK/tkj9uv9k6wOe1LjipNik4VmRhKjEYizzjLrJpbeU1oY+qW0GBpPYomFTeNf+MejSSmsUYyYJ8+E/4GeEfaonvsTSwMT7AId98Lck6Vy4wrfgpm7sZZ8xU15/HqXZNc8UCo2iTdw45xj/Oov9+brX4WUASFPG8aYrK8dl/EdaOvr89P8uIofbSNZ25GjJvVF5ymarrPkTZ7djjJXchzpwBY+7GTJfs3funR/vIk0Hq95jgOFFP1liZyqTXSa49ojG3hzojRQ==', + queryUserResult.rows[0].id, + 'true', + 'false', + ], + }; + + const createConnectionResult = await pgPool.query(createConnection); + expect(createConnectionResult.rowCount).toBe(1); + expect(createConnectionResult.command).toBe('INSERT'); + } catch (err) { + console.error(err.message); + throw err; + } +}; diff --git a/packages/e2e-tests/tests/admin/applications.spec.js b/packages/e2e-tests/tests/admin/applications.spec.js index 2fad49b9..847adc41 100644 --- a/packages/e2e-tests/tests/admin/applications.spec.js +++ b/packages/e2e-tests/tests/admin/applications.spec.js @@ -1,20 +1,37 @@ const { test, expect } = require('../../fixtures/index'); const { pgPool } = require('../../fixtures/postgres-config'); +const { insertAppConnection } = require('../../helpers/db-helpers'); test.describe('Admin Applications', () => { test.beforeAll(async () => { const deleteAppAuthClients = { - text: 'DELETE FROM app_auth_clients WHERE app_key in ($1, $2, $3, $4, $5)', - values: ['carbone', 'spotify', 'deepl', 'mailchimp', 'reddit'] + text: 'DELETE FROM app_auth_clients WHERE app_key in ($1, $2, $3, $4, $5, $6)', + values: [ + 'carbone', + 'spotify', + 'clickup', + 'mailchimp', + 'reddit', + 'google-drive', + ], }; const deleteAppConfigs = { - text: 'DELETE FROM app_configs WHERE key in ($1, $2, $3, $4, $5)', - values: ['carbone', 'spotify', 'deepl', 'mailchimp', 'reddit'] + text: 'DELETE FROM app_configs WHERE key in ($1, $2, $3, $4, $5, $6)', + values: [ + 'carbone', + 'spotify', + 'clickup', + 'mailchimp', + 'reddit', + 'google-drive', + ], }; try { - const deleteAppAuthClientsResult = await pgPool.query(deleteAppAuthClients); + const deleteAppAuthClientsResult = await pgPool.query( + deleteAppAuthClients + ); expect(deleteAppAuthClientsResult.command).toBe('DELETE'); const deleteAppConfigsResult = await pgPool.query(deleteAppConfigs); expect(deleteAppConfigsResult.command).toBe('DELETE'); @@ -31,39 +48,59 @@ test.describe('Admin Applications', () => { test('Admin should be able to toggle Application settings', async ({ adminApplicationsPage, adminApplicationSettingsPage, - page + page, }) => { await adminApplicationsPage.openApplication('Carbone'); await expect(page.url()).toContain('/admin-settings/apps/carbone/settings'); - await adminApplicationSettingsPage.allowCustomConnections(); - await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); - await adminApplicationSettingsPage.allowSharedConnections(); + await adminApplicationSettingsPage.allowUseOnlyPredefinedAuthClients(); + await adminApplicationSettingsPage.saveSettings(); await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); await adminApplicationSettingsPage.disallowConnections(); + await adminApplicationSettingsPage.saveSettings(); await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); await page.reload(); - await adminApplicationSettingsPage.disallowCustomConnections(); - await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); - await adminApplicationSettingsPage.disallowSharedConnections(); + await adminApplicationSettingsPage.disallowUseOnlyPredefinedAuthClients(); + await adminApplicationSettingsPage.saveSettings(); await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); await adminApplicationSettingsPage.allowConnections(); + await adminApplicationSettingsPage.saveSettings(); await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); }); test('should allow only custom connections', async ({ adminApplicationsPage, adminApplicationSettingsPage, + adminApplicationAuthClientsPage, flowEditorPage, - page + page, }) => { - await adminApplicationsPage.openApplication('Spotify'); - await expect(page.url()).toContain('/admin-settings/apps/spotify/settings'); + await insertAppConnection('google-drive'); - await adminApplicationSettingsPage.allowCustomConnections(); - await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); + // TODO use openApplication method after fix + // await adminApplicationsPage.openApplication('Google-Drive'); + await adminApplicationsPage.searchInput.fill('Google-Drive'); + await adminApplicationsPage.appRow + .locator(page.getByText('Google Drive')) + .click(); + + await expect(page.url()).toContain( + '/admin-settings/apps/google-drive/settings' + ); + + await expect( + adminApplicationSettingsPage.useOnlyPredefinedAuthClients + ).not.toBeChecked(); + await expect( + adminApplicationSettingsPage.disableConnectionsSwitch + ).not.toBeChecked(); + + await adminApplicationAuthClientsPage.openAuthClientsTab(); + await expect( + adminApplicationAuthClientsPage.createFirstAuthClientButton + ).toHaveCount(1); await page.goto('/'); await page.getByTestId('create-flow-button').click(); @@ -72,42 +109,69 @@ test.describe('Admin Applications', () => { ); await expect(flowEditorPage.flowStep).toHaveCount(2); - const triggerStep = flowEditorPage.flowStep.last(); - await triggerStep.click(); - await flowEditorPage.chooseAppAndEvent("Spotify", "Create Playlist"); + await flowEditorPage.chooseAppAndEvent( + 'Google Drive', + 'New files in folder' + ); await flowEditorPage.connectionAutocomplete.click(); - const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' }); - const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' }); + const newConnectionOption = page + .getByRole('option') + .filter({ hasText: 'Add new connection' }); + const newSharedConnectionOption = page + .getByRole('option') + .filter({ hasText: 'Add new connection with auth client' }); + const existingConnection = page + .getByRole('option') + .filter({ hasText: 'Unnamed' }); + await expect(await existingConnection.count()).toBeGreaterThan(0); await expect(newConnectionOption).toBeEnabled(); await expect(newConnectionOption).toHaveCount(1); await expect(newSharedConnectionOption).toHaveCount(0); }); - test('should allow only shared connections', async ({ + test('should allow only predefined connections and existing custom', async ({ adminApplicationsPage, adminApplicationSettingsPage, adminApplicationAuthClientsPage, flowEditorPage, - page + page, }) => { - await adminApplicationsPage.openApplication('Reddit'); - await expect(page.url()).toContain('/admin-settings/apps/reddit/settings'); + await insertAppConnection('spotify'); - await adminApplicationSettingsPage.allowSharedConnections(); + await adminApplicationsPage.openApplication('Spotify'); + await expect(page.url()).toContain('/admin-settings/apps/spotify/settings'); + + await expect( + adminApplicationSettingsPage.useOnlyPredefinedAuthClients + ).not.toBeChecked(); + await expect( + adminApplicationSettingsPage.disableConnectionsSwitch + ).not.toBeChecked(); + + await adminApplicationSettingsPage.allowUseOnlyPredefinedAuthClients(); + await adminApplicationSettingsPage.saveSettings(); await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); await adminApplicationAuthClientsPage.openAuthClientsTab(); await adminApplicationAuthClientsPage.openFirstAuthClientCreateForm(); - const authClientForm = page.getByTestId("auth-client-form"); + const authClientForm = page.getByTestId('auth-client-form'); await authClientForm.locator(page.getByTestId('switch')).check(); - await authClientForm.locator(page.locator('[name="name"]')).fill('redditAuthClient'); - await authClientForm.locator(page.locator('[name="clientId"]')).fill('redditClientId'); - await authClientForm.locator(page.locator('[name="clientSecret"]')).fill('redditClientSecret'); + await authClientForm + .locator(page.locator('[name="name"]')) + .fill('spotifyAuthClient'); + await authClientForm + .locator(page.locator('[name="clientId"]')) + .fill('spotifyClientId'); + await authClientForm + .locator(page.locator('[name="clientSecret"]')) + .fill('spotifyClientSecret'); await adminApplicationAuthClientsPage.submitAuthClientForm(); - await adminApplicationAuthClientsPage.authClientShouldBeVisible('redditAuthClient'); + await adminApplicationAuthClientsPage.authClientShouldBeVisible( + 'spotifyAuthClient' + ); await page.goto('/'); await page.getByTestId('create-flow-button').click(); @@ -119,28 +183,61 @@ test.describe('Admin Applications', () => { const triggerStep = flowEditorPage.flowStep.last(); await triggerStep.click(); - await flowEditorPage.chooseAppAndEvent("Reddit", "Create link post"); + await flowEditorPage.chooseAppAndEvent('Spotify', 'Create playlist'); await flowEditorPage.connectionAutocomplete.click(); - const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' }); - const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' }); + const newConnectionOption = page + .getByRole('option') + .filter({ hasText: 'Add new connection' }); + const newSharedConnectionOption = page + .getByRole('option') + .filter({ hasText: 'Add connection with auth client' }); + const existingConnection = page + .getByRole('option') + .filter({ hasText: 'Unnamed' }); + await expect(await existingConnection.count()).toBeGreaterThan(0); await expect(newConnectionOption).toHaveCount(0); await expect(newSharedConnectionOption).toBeEnabled(); await expect(newSharedConnectionOption).toHaveCount(1); }); - test('should not allow any connections', async ({ + test('should allow all connections', async ({ adminApplicationsPage, adminApplicationSettingsPage, + adminApplicationAuthClientsPage, flowEditorPage, - page + page, }) => { - await adminApplicationsPage.openApplication('DeepL'); - await expect(page.url()).toContain('/admin-settings/apps/deepl/settings'); + await insertAppConnection('reddit'); - await adminApplicationSettingsPage.disallowConnections(); - await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); + await adminApplicationsPage.openApplication('Reddit'); + await expect(page.url()).toContain('/admin-settings/apps/reddit/settings'); + + await expect( + adminApplicationSettingsPage.useOnlyPredefinedAuthClients + ).not.toBeChecked(); + await expect( + adminApplicationSettingsPage.disableConnectionsSwitch + ).not.toBeChecked(); + + await adminApplicationAuthClientsPage.openAuthClientsTab(); + await adminApplicationAuthClientsPage.openFirstAuthClientCreateForm(); + const authClientForm = page.getByTestId('auth-client-form'); + await authClientForm.locator(page.getByTestId('switch')).check(); + await authClientForm + .locator(page.locator('[name="name"]')) + .fill('redditAuthClient'); + await authClientForm + .locator(page.locator('[name="clientId"]')) + .fill('redditClientId'); + await authClientForm + .locator(page.locator('[name="clientSecret"]')) + .fill('redditClientSecret'); + await adminApplicationAuthClientsPage.submitAuthClientForm(); + await adminApplicationAuthClientsPage.authClientShouldBeVisible( + 'redditAuthClient' + ); await page.goto('/'); await page.getByTestId('create-flow-button').click(); @@ -152,58 +249,126 @@ test.describe('Admin Applications', () => { const triggerStep = flowEditorPage.flowStep.last(); await triggerStep.click(); - await flowEditorPage.chooseAppAndEvent("DeepL", "Translate text"); + await flowEditorPage.chooseAppAndEvent('Reddit', 'Create link post'); await flowEditorPage.connectionAutocomplete.click(); - const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' }); - const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' }); - const noConnectionsOption = page.locator('.MuiAutocomplete-noOptions').filter({ hasText: 'No options' }); + const newConnectionOption = page + .getByRole('option') + .filter({ hasText: 'Add new connection' }); + const newSharedConnectionOption = page + .getByRole('option') + .filter({ hasText: 'Add connection with auth client' }); + const existingConnection = page + .getByRole('option') + .filter({ hasText: 'Unnamed' }); - await expect(noConnectionsOption).toHaveCount(1); + await expect(await existingConnection.count()).toBeGreaterThan(0); + await expect(newConnectionOption).toHaveCount(1); + await expect(newSharedConnectionOption).toBeEnabled(); + await expect(newSharedConnectionOption).toHaveCount(1); + }); + + test('should not allow new connections but existing custom', async ({ + adminApplicationsPage, + adminApplicationSettingsPage, + adminApplicationAuthClientsPage, + flowEditorPage, + page, + }) => { + await insertAppConnection('clickup'); + + await adminApplicationsPage.openApplication('ClickUp'); + await expect(page.url()).toContain('/admin-settings/apps/clickup/settings'); + + await adminApplicationSettingsPage.disallowConnections(); + await adminApplicationSettingsPage.saveSettings(); + await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); + + await adminApplicationAuthClientsPage.openAuthClientsTab(); + await adminApplicationAuthClientsPage.openFirstAuthClientCreateForm(); + + const authClientForm = page.getByTestId('auth-client-form'); + await authClientForm.locator(page.getByTestId('switch')).check(); + await authClientForm + .locator(page.locator('[name="name"]')) + .fill('clickupAuthClient'); + await authClientForm + .locator(page.locator('[name="clientId"]')) + .fill('clickupClientId'); + await authClientForm + .locator(page.locator('[name="clientSecret"]')) + .fill('clickupClientSecret'); + await adminApplicationAuthClientsPage.submitAuthClientForm(); + await adminApplicationAuthClientsPage.authClientShouldBeVisible( + 'clickupAuthClient' + ); + + await page.goto('/'); + await page.getByTestId('create-flow-button').click(); + await page.waitForURL( + /\/editor\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}/ + ); + + await expect(flowEditorPage.flowStep).toHaveCount(2); + const triggerStep = flowEditorPage.flowStep.last(); + await triggerStep.click(); + + await flowEditorPage.chooseAppAndEvent('ClickUp', 'Create folder'); + await flowEditorPage.connectionAutocomplete.click(); + + const newConnectionOption = page + .getByRole('option') + .filter({ hasText: 'Add new connection' }); + const newSharedConnectionOption = page + .getByRole('option') + .filter({ hasText: 'Add connection with auth client' }); + const existingConnection = page + .getByRole('option') + .filter({ hasText: 'Unnamed' }); + + await expect(await existingConnection.count()).toBeGreaterThan(0); await expect(newConnectionOption).toHaveCount(0); await expect(newSharedConnectionOption).toHaveCount(0); }); - test('should not allow new connections but only already created', async ({ + test('should not allow new connections but existing custom even if predefined auth clients are enabled', async ({ adminApplicationsPage, adminApplicationSettingsPage, + adminApplicationAuthClientsPage, flowEditorPage, - page + page, }) => { - const queryUser = { - text: 'SELECT * FROM users WHERE email = $1', - values: [process.env.LOGIN_EMAIL] - }; - - try { - const queryUserResult = await pgPool.query(queryUser); - expect(queryUserResult.rowCount).toEqual(1); - - const createMailchimpConnection = { - text: 'INSERT INTO connections (key, data, user_id, verified, draft) VALUES ($1, $2, $3, $4, $5)', - values: [ - 'mailchimp', - "U2FsdGVkX1+cAtdHwLiuRL4DaK/T1aljeeKyPMmtWK0AmAIsKhYwQiuyQCYJO3mdZ31z73hqF2Y+yj2Kn2/IIpLRqCxB2sC0rCDCZyolzOZ290YcBXSzYRzRUxhoOcZEtwYDKsy8AHygKK/tkj9uv9k6wOe1LjipNik4VmRhKjEYizzjLrJpbeU1oY+qW0GBpPYomFTeNf+MejSSmsUYyYJ8+E/4GeEfaonvsTSwMT7AId98Lck6Vy4wrfgpm7sZZ8xU15/HqXZNc8UCo2iTdw45xj/Oov9+brX4WUASFPG8aYrK8dl/EdaOvr89P8uIofbSNZ25GjJvVF5ymarrPkTZ7djjJXchzpwBY+7GTJfs3funR/vIk0Hq95jgOFFP1liZyqTXSa49ojG3hzojRQ==", - queryUserResult.rows[0].id, - 'true', - 'false' - ], - }; - - const createMailchimpConnectionResult = await pgPool.query(createMailchimpConnection); - expect(createMailchimpConnectionResult.rowCount).toBe(1); - expect(createMailchimpConnectionResult.command).toBe('INSERT'); - } catch (err) { - console.error(err.message); - throw err; - } + await insertAppConnection('mailchimp'); await adminApplicationsPage.openApplication('Mailchimp'); - await expect(page.url()).toContain('/admin-settings/apps/mailchimp/settings'); + await expect(page.url()).toContain( + '/admin-settings/apps/mailchimp/settings' + ); + await adminApplicationSettingsPage.allowUseOnlyPredefinedAuthClients(); await adminApplicationSettingsPage.disallowConnections(); + await adminApplicationSettingsPage.saveSettings(); await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); + await adminApplicationAuthClientsPage.openAuthClientsTab(); + await adminApplicationAuthClientsPage.openFirstAuthClientCreateForm(); + + const authClientForm = page.getByTestId('auth-client-form'); + await authClientForm.locator(page.getByTestId('switch')).check(); + await authClientForm + .locator(page.locator('[name="name"]')) + .fill('mailchimpAuthClient'); + await authClientForm + .locator(page.locator('[name="clientId"]')) + .fill('mailchimpClientId'); + await authClientForm + .locator(page.locator('[name="clientSecret"]')) + .fill('mailchimpClientSecret'); + await adminApplicationAuthClientsPage.submitAuthClientForm(); + await adminApplicationAuthClientsPage.authClientShouldBeVisible( + 'mailchimpAuthClient' + ); + await page.goto('/'); await page.getByTestId('create-flow-button').click(); await page.waitForURL( @@ -214,14 +379,22 @@ test.describe('Admin Applications', () => { const triggerStep = flowEditorPage.flowStep.last(); await triggerStep.click(); - await flowEditorPage.chooseAppAndEvent("Mailchimp", "Create campaign"); + await flowEditorPage.chooseAppAndEvent('Mailchimp', 'Create campaign'); await flowEditorPage.connectionAutocomplete.click(); await expect(page.getByRole('option').first()).toHaveText('Unnamed'); - const existingConnection = page.getByRole('option').filter({ hasText: 'Unnamed' }); - const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' }); - const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' }); - const noConnectionsOption = page.locator('.MuiAutocomplete-noOptions').filter({ hasText: 'No options' }); + const existingConnection = page + .getByRole('option') + .filter({ hasText: 'Unnamed' }); + const newConnectionOption = page + .getByRole('option') + .filter({ hasText: 'Add new connection' }); + const newSharedConnectionOption = page + .getByRole('option') + .filter({ hasText: 'Add new shared connection' }); + const noConnectionsOption = page + .locator('.MuiAutocomplete-noOptions') + .filter({ hasText: 'No options' }); await expect(await existingConnection.count()).toBeGreaterThan(0); await expect(noConnectionsOption).toHaveCount(0); diff --git a/packages/web/package.json b/packages/web/package.json index 501d1ccc..cf1eb72c 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -83,6 +83,7 @@ "access": "public" }, "devDependencies": { + "@simbathesailor/use-what-changed": "^2.0.0", "@tanstack/eslint-plugin-query": "^5.20.1", "@tanstack/react-query-devtools": "^5.24.1", "eslint-config-prettier": "^9.1.0", diff --git a/packages/web/src/components/AddAppConnection/index.jsx b/packages/web/src/components/AddAppConnection/index.jsx index dc14ad09..a074b035 100644 --- a/packages/web/src/components/AddAppConnection/index.jsx +++ b/packages/web/src/components/AddAppConnection/index.jsx @@ -64,7 +64,7 @@ function AddAppConnection(props) { asyncAuthenticate(); }, - [appAuthClientId, authenticate], + [appAuthClientId, authenticate, key, navigate], ); const handleClientClick = (appAuthClientId) => diff --git a/packages/web/src/components/AdminApplicationCreateAuthClient/index.jsx b/packages/web/src/components/AdminApplicationCreateAuthClient/index.jsx index ccda0d0e..4747e876 100644 --- a/packages/web/src/components/AdminApplicationCreateAuthClient/index.jsx +++ b/packages/web/src/components/AdminApplicationCreateAuthClient/index.jsx @@ -34,10 +34,10 @@ function AdminApplicationCreateAuthClient(props) { if (!appConfigKey) { const { data: appConfigData } = await createAppConfig({ - customConnectionAllowed: true, - shared: false, + useOnlyPredefinedAuthClients: false, disabled: false, }); + appConfigKey = appConfigData.key; } diff --git a/packages/web/src/components/AdminApplicationSettings/index.jsx b/packages/web/src/components/AdminApplicationSettings/index.jsx index 34ef8d0c..99b46a74 100644 --- a/packages/web/src/components/AdminApplicationSettings/index.jsx +++ b/packages/web/src/components/AdminApplicationSettings/index.jsx @@ -46,9 +46,8 @@ function AdminApplicationSettings(props) { const defaultValues = useMemo( () => ({ - customConnectionAllowed: - appConfig?.data?.customConnectionAllowed || false, - shared: appConfig?.data?.shared || false, + useOnlyPredefinedAuthClients: + appConfig?.data?.useOnlyPredefinedAuthClients || false, disabled: appConfig?.data?.disabled || false, }), [appConfig?.data], @@ -62,21 +61,17 @@ function AdminApplicationSettings(props) { - - + + + ; + if (!appAuthClients?.data.length) return ; return ( diff --git a/packages/web/src/components/AppConnectionContextMenu/index.jsx b/packages/web/src/components/AppConnectionContextMenu/index.jsx index fb94e4b3..f17fb860 100644 --- a/packages/web/src/components/AppConnectionContextMenu/index.jsx +++ b/packages/web/src/components/AppConnectionContextMenu/index.jsx @@ -11,14 +11,7 @@ import { useQueryClient } from '@tanstack/react-query'; import Can from 'components/Can'; function ContextMenu(props) { - const { - appKey, - connection, - onClose, - onMenuItemClick, - anchorEl, - disableReconnection, - } = props; + const { appKey, connection, onClose, onMenuItemClick, anchorEl } = props; const formatMessage = useFormatMessage(); const queryClient = useQueryClient(); @@ -73,7 +66,7 @@ function ContextMenu(props) { {(allowed) => ( ( function AppConnectionRow(props) { const formatMessage = useFormatMessage(); const enqueueSnackbar = useEnqueueSnackbar(); - const { id, key, formattedData, verified, createdAt, reconnectable } = - props.connection; + const { id, key, formattedData, verified, createdAt } = props.connection; const [verificationVisible, setVerificationVisible] = React.useState(false); const contextButtonRef = React.useRef(null); const [anchorEl, setAnchorEl] = React.useState(null); @@ -174,7 +173,6 @@ function AppConnectionRow(props) { optionGenerator(connection)) || []; + const addCustomConnection = { + label: formatMessage('chooseConnectionSubstep.addNewConnection'), + value: ADD_CONNECTION_VALUE, + }; + + const addConnectionWithAuthClient = { + label: formatMessage( + 'chooseConnectionSubstep.addConnectionWithAuthClient', + ), + value: ADD_SHARED_CONNECTION_VALUE, + }; + + // means there is no app config. defaulting to custom connections only + if (!appConfig?.data) { + return options.concat([addCustomConnection]); + } + + // app is disabled. + if (appConfig.data.disabled) return options; + + // means only auth clients are allowed for connection creation and there is app auth client if ( - !appConfig?.data || - (!appConfig.data?.disabled && appConfig.data?.customConnectionAllowed) + appConfig.data.useOnlyPredefinedAuthClients === true && + appAuthClients.data.length > 0 ) { - options.push({ - label: formatMessage('chooseConnectionSubstep.addNewConnection'), - value: ADD_CONNECTION_VALUE, - }); + return options.concat([addConnectionWithAuthClient]); } - if (appConfig?.data?.connectionAllowed) { - options.push({ - label: formatMessage('chooseConnectionSubstep.addNewSharedConnection'), - value: ADD_SHARED_CONNECTION_VALUE, - }); + // means there is no app auth client. so we don't show the `addConnectionWithAuthClient` + if ( + appConfig.data.useOnlyPredefinedAuthClients === true && + appAuthClients.data.length === 0 + ) { + return options; } - return options; - }, [data, formatMessage, appConfig?.data]); + if (appAuthClients.data.length === 0) { + return options.concat([addCustomConnection]); + } + + return options.concat([addCustomConnection, addConnectionWithAuthClient]); + }, [data, formatMessage, appConfig, appAuthClients]); const handleClientClick = async (appAuthClientId) => { try { @@ -162,10 +187,7 @@ function ChooseConnectionSubstep(props) { const handleChange = React.useCallback( async (event, selectedOption) => { if (typeof selectedOption === 'object') { - // TODO: try to simplify type casting below. - const typedSelectedOption = selectedOption; - const option = typedSelectedOption; - const connectionId = option?.value; + const connectionId = selectedOption?.value; if (connectionId === ADD_CONNECTION_VALUE) { setShowAddConnectionDialog(true); diff --git a/packages/web/src/components/SplitButton/index.jsx b/packages/web/src/components/SplitButton/index.jsx index a8fdcfde..82e8c733 100644 --- a/packages/web/src/components/SplitButton/index.jsx +++ b/packages/web/src/components/SplitButton/index.jsx @@ -67,17 +67,12 @@ export default function SplitButton(props) { }} open={open} anchorEl={anchorRef.current} + placement="bottom-end" transition disablePortal > - {({ TransitionProps, placement }) => ( - + {({ TransitionProps }) => ( + diff --git a/packages/web/src/hooks/useAuthenticateApp.ee.js b/packages/web/src/hooks/useAuthenticateApp.ee.js index b0b99b09..45c17bfb 100644 --- a/packages/web/src/hooks/useAuthenticateApp.ee.js +++ b/packages/web/src/hooks/useAuthenticateApp.ee.js @@ -13,6 +13,7 @@ import useCreateConnectionAuthUrl from './useCreateConnectionAuthUrl'; import useUpdateConnection from './useUpdateConnection'; import useResetConnection from './useResetConnection'; import useVerifyConnection from './useVerifyConnection'; +import { useWhatChanged } from '@simbathesailor/use-what-changed'; function getSteps(auth, hasConnection, useShared) { if (hasConnection) { @@ -37,11 +38,13 @@ export default function useAuthenticateApp(payload) { const { mutateAsync: createConnectionAuthUrl } = useCreateConnectionAuthUrl(); const { mutateAsync: updateConnection } = useUpdateConnection(); const { mutateAsync: resetConnection } = useResetConnection(); + const { mutateAsync: verifyConnection } = useVerifyConnection(); const [authenticationInProgress, setAuthenticationInProgress] = React.useState(false); const formatMessage = useFormatMessage(); - const steps = getSteps(auth?.data, !!connectionId, useShared); - const { mutateAsync: verifyConnection } = useVerifyConnection(); + const steps = React.useMemo(() => { + return getSteps(auth?.data, !!connectionId, useShared); + }, [auth, connectionId, useShared]); const authenticate = React.useMemo(() => { if (!steps?.length) return; @@ -57,7 +60,6 @@ export default function useAuthenticateApp(payload) { fields, }; let stepIndex = 0; - while (stepIndex < steps?.length) { const step = steps[stepIndex]; const variables = computeAuthStepVariables(step.arguments, response); @@ -105,10 +107,10 @@ export default function useAuthenticateApp(payload) { response[step.name] = stepResponse; } } catch (err) { - console.log(err); + console.error(err); setAuthenticationInProgress(false); - queryClient.invalidateQueries({ + await queryClient.invalidateQueries({ queryKey: ['apps', appKey, 'connections'], }); @@ -126,13 +128,14 @@ export default function useAuthenticateApp(payload) { return response; }; + // keep formatMessage out of it as it causes infinite loop. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ steps, appKey, appAuthClientId, connectionId, queryClient, - formatMessage, createConnection, createConnectionAuthUrl, updateConnection, @@ -140,6 +143,24 @@ export default function useAuthenticateApp(payload) { verifyConnection, ]); + useWhatChanged( + [ + steps, + appKey, + appAuthClientId, + connectionId, + queryClient, + createConnection, + createConnectionAuthUrl, + updateConnection, + resetConnection, + verifyConnection, + ], + 'steps, appKey, appAuthClientId, connectionId, queryClient, createConnection, createConnectionAuthUrl, updateConnection, resetConnection, verifyConnection', + '', + 'useAuthenticate', + ); + return { authenticate, inProgress: authenticationInProgress, diff --git a/packages/web/src/hooks/useAutomatischInfo.js b/packages/web/src/hooks/useAutomatischInfo.js index f7ee73b1..469f4c25 100644 --- a/packages/web/src/hooks/useAutomatischInfo.js +++ b/packages/web/src/hooks/useAutomatischInfo.js @@ -9,7 +9,7 @@ export default function useAutomatischInfo() { **/ staleTime: Infinity, queryKey: ['automatisch', 'info'], - queryFn: async (payload, signal) => { + queryFn: async ({ signal }) => { const { data } = await api.get('/v1/automatisch/info', { signal }); return data; diff --git a/packages/web/src/hooks/useCreateConnection.js b/packages/web/src/hooks/useCreateConnection.js index 6ba59f05..7615ab6d 100644 --- a/packages/web/src/hooks/useCreateConnection.js +++ b/packages/web/src/hooks/useCreateConnection.js @@ -3,7 +3,7 @@ import { useMutation } from '@tanstack/react-query'; import api from 'helpers/api'; export default function useCreateConnection(appKey) { - const query = useMutation({ + const mutation = useMutation({ mutationFn: async ({ appAuthClientId, formattedData }) => { const { data } = await api.post(`/v1/apps/${appKey}/connections`, { appAuthClientId, @@ -14,5 +14,5 @@ export default function useCreateConnection(appKey) { }, }); - return query; + return mutation; } diff --git a/packages/web/src/hooks/useLicense.js b/packages/web/src/hooks/useLicense.js new file mode 100644 index 00000000..deedf766 --- /dev/null +++ b/packages/web/src/hooks/useLicense.js @@ -0,0 +1,15 @@ +import { useQuery } from '@tanstack/react-query'; +import api from 'helpers/api'; + +export default function useLicense() { + const query = useQuery({ + queryKey: ['automatisch', 'license'], + queryFn: async ({ signal }) => { + const { data } = await api.get('/v1/automatisch/license', { signal }); + + return data; + }, + }); + + return query; +} diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index b121f5e2..40073d61 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -22,7 +22,7 @@ "app.connectionCount": "{count} connections", "app.flowCount": "{count} flows", "app.addConnection": "Add connection", - "app.addCustomConnection": "Add custom connection", + "app.addConnectionWithAuthClient": "Add connection with auth client", "app.reconnectConnection": "Reconnect connection", "app.createFlow": "Create flow", "app.settings": "Settings", @@ -74,7 +74,7 @@ "filterConditions.orContinueIf": "OR continue if…", "chooseConnectionSubstep.continue": "Continue", "chooseConnectionSubstep.addNewConnection": "Add new connection", - "chooseConnectionSubstep.addNewSharedConnection": "Add new shared connection", + "chooseConnectionSubstep.addConnectionWithAuthClient": "Add connection with auth client", "chooseConnectionSubstep.chooseConnection": "Choose connection", "flow.createdAt": "created {datetime}", "flow.updatedAt": "updated {datetime}", @@ -292,7 +292,7 @@ "adminApps.connections": "Connections", "adminApps.authClients": "Auth clients", "adminApps.settings": "Settings", - "adminAppsSettings.customConnectionAllowed": "Allow custom connection", + "adminAppsSettings.useOnlyPredefinedAuthClients": "Use only predefined auth clients", "adminAppsSettings.shared": "Shared", "adminAppsSettings.disabled": "Disabled", "adminAppsSettings.save": "Save", diff --git a/packages/web/src/pages/AdminApplication/index.jsx b/packages/web/src/pages/AdminApplication/index.jsx index 44e73085..85e15bba 100644 --- a/packages/web/src/pages/AdminApplication/index.jsx +++ b/packages/web/src/pages/AdminApplication/index.jsx @@ -87,18 +87,12 @@ export default function AdminApplication() { component={Link} /> - @@ -111,10 +105,6 @@ export default function AdminApplication() { path={`/auth-clients/*`} element={} /> - App connections} - /> navigate('connections'); const connectionOptions = React.useMemo(() => { - const shouldHaveCustomConnection = - appConfig?.data?.connectionAllowed && - appConfig?.data?.customConnectionAllowed; + const addCustomConnection = { + label: formatMessage('app.addConnection'), + key: 'addConnection', + 'data-test': 'add-connection-button', + to: URLS.APP_ADD_CONNECTION(appKey, false), + disabled: + !currentUserAbility.can('create', 'Connection') || + appConfig?.data?.useOnlyPredefinedAuthClients === true || + appConfig?.data?.disabled === true, + }; - const options = [ - { - label: formatMessage('app.addConnection'), - key: 'addConnection', - 'data-test': 'add-connection-button', - to: URLS.APP_ADD_CONNECTION(appKey, appConfig?.data?.connectionAllowed), - disabled: !currentUserAbility.can('create', 'Connection'), - }, - ]; + const addConnectionWithAuthClient = { + label: formatMessage('app.addConnectionWithAuthClient'), + key: 'addConnectionWithAuthClient', + 'data-test': 'add-connection-with-auth-client-button', + to: URLS.APP_ADD_CONNECTION(appKey, true), + disabled: + !currentUserAbility.can('create', 'Connection') || + appAuthClients?.data?.length === 0 || + appConfig?.data?.disabled === true, + }; - if (shouldHaveCustomConnection) { - options.push({ - label: formatMessage('app.addCustomConnection'), - key: 'addCustomConnection', - 'data-test': 'add-custom-connection-button', - to: URLS.APP_ADD_CONNECTION(appKey), - disabled: !currentUserAbility.can('create', 'Connection'), - }); + // means there is no app config. defaulting to custom connections only + if (!appConfig?.data) { + return [addCustomConnection]; } - return options; - }, [appKey, appConfig?.data, currentUserAbility, formatMessage]); + // means only auth clients are allowed for connection creation + if (appConfig?.data?.useOnlyPredefinedAuthClients === true) { + return [addConnectionWithAuthClient]; + } + + // means there is no app auth client. so we don't show the `addConnectionWithAuthClient` + if (appAuthClients?.data?.length === 0) { + return [addCustomConnection]; + } + + return [addCustomConnection, addConnectionWithAuthClient]; + }, [appKey, appConfig, appAuthClients, currentUserAbility, formatMessage]); if (loading) return null; @@ -153,14 +165,7 @@ export default function Application() { {(allowed) => ( disabled) - } + disabled={!allowed} options={connectionOptions} /> )} diff --git a/packages/web/src/propTypes/propTypes.js b/packages/web/src/propTypes/propTypes.js index e6660c75..c92f51a4 100644 --- a/packages/web/src/propTypes/propTypes.js +++ b/packages/web/src/propTypes/propTypes.js @@ -211,7 +211,6 @@ export const ConnectionPropType = PropTypes.shape({ flowCount: PropTypes.number, appData: AppPropType, createdAt: PropTypes.number, - reconnectable: PropTypes.bool, appAuthClientId: PropTypes.string, }); @@ -459,8 +458,7 @@ export const SamlAuthProviderRolePropType = PropTypes.shape({ export const AppConfigPropType = PropTypes.shape({ id: PropTypes.string, key: PropTypes.string, - customConnectionAllowed: PropTypes.bool, - connectionAllowed: PropTypes.bool, + useOnlyPredefinedAuthClients: PropTypes.bool, shared: PropTypes.bool, disabled: PropTypes.bool, }); diff --git a/packages/web/yarn.lock b/packages/web/yarn.lock index 253ca91e..8023fb30 100644 --- a/packages/web/yarn.lock +++ b/packages/web/yarn.lock @@ -2126,6 +2126,11 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz#427d5549943a9c6fce808e39ea64dbe60d4047f1" integrity sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA== +"@simbathesailor/use-what-changed@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@simbathesailor/use-what-changed/-/use-what-changed-2.0.0.tgz#7f82d78f92c8588b5fadd702065dde93bd781403" + integrity sha512-ulBNrPSvfho9UN6zS2fii3AsdEcp2fMaKeqUZZeCNPaZbB6aXyTUhpEN9atjMAbu/eyK3AY8L4SYJUG62Ekocw== + "@sinclair/typebox@^0.24.1": version "0.24.51" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f" @@ -9784,7 +9789,16 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -9888,7 +9902,14 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -10952,7 +10973,16 @@ workbox-window@6.6.1: "@types/trusted-types" "^2.0.2" workbox-core "6.6.1" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==