From 2a77763c51751086471f5857c0346de75830bfa4 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Mon, 9 Dec 2024 17:46:51 +0000 Subject: [PATCH] feat(AppConfig): iterate how apps are managed - auth clients are always shared, cannot be disabled - custom connections are enabled by default, can be disabled - any existing connections can be reconnected regardless of its AppConfig or AppAuthClient states --- .../api/v1/admin/apps/create-config.ee.js | 5 +- .../v1/admin/apps/create-config.ee.test.js | 7 +- .../api/v1/admin/apps/update-config.ee.js | 5 +- .../v1/admin/apps/update-config.ee.test.js | 12 +- .../api/v1/apps/create-connection.test.js | 29 ++-- .../api/v1/apps/get-config.ee.test.js | 3 +- .../v1/connections/reset-connection.test.js | 1 - .../v1/connections/update-connection.test.js | 7 +- .../__snapshots__/app-config.test.js.snap | 16 +-- .../backend/src/models/app-auth-client.js | 17 +-- .../src/models/app-auth-client.test.js | 81 ----------- packages/backend/src/models/app-config.js | 33 ----- .../backend/src/models/app-config.test.js | 126 +----------------- packages/backend/src/models/connection.js | 26 +--- .../backend/src/models/connection.test.js | 105 +-------------- .../backend/src/serializers/app-config.js | 4 +- .../src/serializers/app-config.test.js | 4 +- packages/backend/src/serializers/auth.js | 2 + packages/backend/src/serializers/auth.test.js | 2 + .../backend/src/serializers/connection.js | 1 - .../src/serializers/connection.test.js | 1 - .../rest/api/v1/admin/apps/create-config.js | 3 +- .../rest/api/v1/apps/create-connection.js | 1 - .../test/mocks/rest/api/v1/apps/get-auth.js | 2 + .../test/mocks/rest/api/v1/apps/get-config.js | 4 +- .../mocks/rest/api/v1/apps/get-connections.js | 1 - .../api/v1/connections/reset-connection.js | 1 - .../api/v1/connections/update-connection.js | 1 - .../mocks/rest/api/v1/steps/get-connection.js | 1 - packages/backend/vitest.config.js | 8 +- packages/web/package.json | 1 + .../src/components/AddAppConnection/index.jsx | 3 +- .../index.jsx | 4 +- .../AdminApplicationSettings/index.jsx | 22 ++- .../AppAuthClientsDialog/index.ee.jsx | 12 +- .../AppConnectionContextMenu/index.jsx | 12 +- .../src/components/AppConnectionRow/index.jsx | 4 +- .../ChooseConnectionSubstep/index.jsx | 13 +- .../web/src/components/SplitButton/index.jsx | 11 +- .../web/src/hooks/useAuthenticateApp.ee.js | 33 ++++- packages/web/src/hooks/useAutomatischInfo.js | 2 +- packages/web/src/hooks/useCreateConnection.js | 4 +- packages/web/src/hooks/useLicense.js | 15 +++ packages/web/src/index.jsx | 3 + packages/web/src/locales/en.json | 4 +- packages/web/src/pages/Application/index.jsx | 63 ++++----- packages/web/src/propTypes/propTypes.js | 4 +- packages/web/yarn.lock | 36 ++++- 48 files changed, 192 insertions(+), 563 deletions(-) create mode 100644 packages/web/src/hooks/useLicense.js 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/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..ddee5c5e 100644 --- a/packages/backend/src/models/app-auth-client.test.js +++ b/packages/backend/src/models/app-auth-client.test.js @@ -164,63 +164,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 +175,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 +188,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 6dae7cfe..6763e9f8 100644 --- a/packages/backend/src/models/app-config.js +++ b/packages/backend/src/models/app-config.js @@ -39,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/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/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..9147ff50 100644 --- a/packages/web/src/components/AddAppConnection/index.jsx +++ b/packages/web/src/components/AddAppConnection/index.jsx @@ -18,6 +18,7 @@ import { generateExternalLink } from 'helpers/translationValues'; import { Form } from './style'; import useAppAuth from 'hooks/useAppAuth'; import { useQueryClient } from '@tanstack/react-query'; +import { useWhatChanged } from '@simbathesailor/use-what-changed'; function AddAppConnection(props) { const { application, connectionId, onClose } = props; @@ -64,7 +65,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) { - {({ 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/index.jsx b/packages/web/src/index.jsx index 8c03da02..916baf28 100644 --- a/packages/web/src/index.jsx +++ b/packages/web/src/index.jsx @@ -1,5 +1,6 @@ import { createRoot } from 'react-dom/client'; import { Settings } from 'luxon'; +import { setUseWhatChange } from '@simbathesailor/use-what-changed'; import ThemeProvider from 'components/ThemeProvider'; import IntlProvider from 'components/IntlProvider'; @@ -14,6 +15,8 @@ import reportWebVitals from './reportWebVitals'; // Sets the default locale to English for all luxon DateTime instances created afterwards. Settings.defaultLocale = 'en'; +setUseWhatChange(process.env.NODE_ENV === 'development'); + const container = document.getElementById('root'); const root = createRoot(container); diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index b121f5e2..d7786d9e 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", @@ -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/Application/index.jsx b/packages/web/src/pages/Application/index.jsx index 74794784..1afd7605 100644 --- a/packages/web/src/pages/Application/index.jsx +++ b/packages/web/src/pages/Application/index.jsx @@ -6,7 +6,6 @@ import { Navigate, Routes, useParams, - useSearchParams, useMatch, useNavigate, } from 'react-router-dom'; @@ -31,6 +30,7 @@ import AppIcon from 'components/AppIcon'; import Container from 'components/Container'; import PageTitle from 'components/PageTitle'; import useApp from 'hooks/useApp'; +import useAppAuthClients from 'hooks/useAppAuthClients'; import Can from 'components/Can'; import { AppPropType } from 'propTypes/propTypes'; @@ -61,47 +61,53 @@ export default function Application() { end: false, }); const flowsPathMatch = useMatch({ path: URLS.APP_FLOWS_PATTERN, end: false }); - const [searchParams] = useSearchParams(); const { appKey } = useParams(); const navigate = useNavigate(); + const { data: appAuthClients } = useAppAuthClients(appKey); const { data, loading } = useApp(appKey); const app = data?.data || {}; const { data: appConfig } = useAppConfig(appKey); - const connectionId = searchParams.get('connectionId') || undefined; const currentUserAbility = useCurrentUserAbility(); const goToApplicationPage = () => 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'), + }; - 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-custom-connection-button', + to: URLS.APP_ADD_CONNECTION(appKey, true), + disabled: !currentUserAbility.can('create', 'Connection'), + }; - 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 there is no app auth client. so we don't show the `addConnectionWithAuthClient` + if (appAuthClients?.data?.length === 0) { + return [addCustomConnection]; + } + + // means only auth clients are allowed for connection creation + if (appConfig?.data?.useOnlyPredefinedAuthClients === true) { + return [addConnectionWithAuthClient]; + } + + return [addCustomConnection, addConnectionWithAuthClient]; + }, [appKey, appConfig, appAuthClients, currentUserAbility, formatMessage]); if (loading) return null; @@ -154,12 +160,7 @@ export default function Application() { {(allowed) => ( disabled) + !allowed || appConfig?.data?.disabled === true } 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==