diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index c90d7397..54c37737 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -47,5 +47,5 @@ jobs: run: cp .env-example.test .env.test working-directory: packages/backend - name: Run tests - run: yarn test + run: yarn test:coverage working-directory: packages/backend diff --git a/packages/backend/package.json b/packages/backend/package.json index c6603c9f..2686d597 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -12,6 +12,7 @@ "pretest": "APP_ENV=test node ./test/setup/prepare-test-env.js", "test": "APP_ENV=test vitest run", "test:watch": "APP_ENV=test vitest watch", + "test:coverage": "yarn test --coverage", "lint": "eslint .", "db:create": "node ./bin/database/create.js", "db:seed:user": "node ./bin/database/seed-user.js", @@ -97,10 +98,11 @@ "url": "https://github.com/automatisch/automatisch/issues" }, "devDependencies": { + "@vitest/coverage-v8": "^2.1.5", "node-gyp": "^10.1.0", "nodemon": "^2.0.13", "supertest": "^6.3.3", - "vitest": "^1.1.3" + "vitest": "^2.1.5" }, "publishConfig": { "access": "public" 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/create-auth-client.ee.js b/packages/backend/src/controllers/api/v1/admin/apps/create-oauth-client.ee.js similarity index 66% rename from packages/backend/src/controllers/api/v1/admin/apps/create-auth-client.ee.js rename to packages/backend/src/controllers/api/v1/admin/apps/create-oauth-client.ee.js index 49cbfff2..ffba9257 100644 --- a/packages/backend/src/controllers/api/v1/admin/apps/create-auth-client.ee.js +++ b/packages/backend/src/controllers/api/v1/admin/apps/create-oauth-client.ee.js @@ -6,14 +6,14 @@ export default async (request, response) => { .findOne({ key: request.params.appKey }) .throwIfNotFound(); - const appAuthClient = await appConfig - .$relatedQuery('appAuthClients') - .insert(appAuthClientParams(request)); + const oauthClient = await appConfig + .$relatedQuery('oauthClients') + .insert(oauthClientParams(request)); - renderObject(response, appAuthClient, { status: 201 }); + renderObject(response, oauthClient, { status: 201 }); }; -const appAuthClientParams = (request) => { +const oauthClientParams = (request) => { const { active, appKey, name, formattedAuthDefaults } = request.body; return { diff --git a/packages/backend/src/controllers/api/v1/admin/apps/create-auth-client.ee.test.js b/packages/backend/src/controllers/api/v1/admin/apps/create-oauth-client.ee.test.js similarity index 81% rename from packages/backend/src/controllers/api/v1/admin/apps/create-auth-client.ee.test.js rename to packages/backend/src/controllers/api/v1/admin/apps/create-oauth-client.ee.test.js index ea658f88..4746a881 100644 --- a/packages/backend/src/controllers/api/v1/admin/apps/create-auth-client.ee.test.js +++ b/packages/backend/src/controllers/api/v1/admin/apps/create-oauth-client.ee.test.js @@ -5,11 +5,11 @@ import app from '../../../../../app.js'; import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js'; import { createUser } from '../../../../../../test/factories/user.js'; import { createRole } from '../../../../../../test/factories/role.js'; -import createAppAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/create-auth-client.js'; +import createOAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/create-oauth-client.js'; import { createAppConfig } from '../../../../../../test/factories/app-config.js'; import * as license from '../../../../../helpers/license.ee.js'; -describe('POST /api/v1/admin/apps/:appKey/auth-clients', () => { +describe('POST /api/v1/admin/apps/:appKey/oauth-clients', () => { let currentUser, adminRole, token; beforeEach(async () => { @@ -26,7 +26,7 @@ describe('POST /api/v1/admin/apps/:appKey/auth-clients', () => { key: 'gitlab', }); - const appAuthClient = { + const oauthClient = { active: true, appKey: 'gitlab', name: 'First auth client', @@ -39,17 +39,17 @@ describe('POST /api/v1/admin/apps/:appKey/auth-clients', () => { }; const response = await request(app) - .post('/api/v1/admin/apps/gitlab/auth-clients') + .post('/api/v1/admin/apps/gitlab/oauth-clients') .set('Authorization', token) - .send(appAuthClient) + .send(oauthClient) .expect(201); - const expectedPayload = createAppAuthClientMock(appAuthClient); + const expectedPayload = createOAuthClientMock(oauthClient); expect(response.body).toMatchObject(expectedPayload); }); it('should return not found response for not existing app config', async () => { - const appAuthClient = { + const oauthClient = { active: true, appKey: 'gitlab', name: 'First auth client', @@ -62,9 +62,9 @@ describe('POST /api/v1/admin/apps/:appKey/auth-clients', () => { }; await request(app) - .post('/api/v1/admin/apps/gitlab/auth-clients') + .post('/api/v1/admin/apps/gitlab/oauth-clients') .set('Authorization', token) - .send(appAuthClient) + .send(oauthClient) .expect(404); }); @@ -73,14 +73,14 @@ describe('POST /api/v1/admin/apps/:appKey/auth-clients', () => { key: 'gitlab', }); - const appAuthClient = { + const oauthClient = { appKey: 'gitlab', }; const response = await request(app) - .post('/api/v1/admin/apps/gitlab/auth-clients') + .post('/api/v1/admin/apps/gitlab/oauth-clients') .set('Authorization', token) - .send(appAuthClient) + .send(oauthClient) .expect(422); expect(response.body.meta.type).toStrictEqual('ModelValidation'); diff --git a/packages/backend/src/controllers/api/v1/admin/apps/get-auth-client.ee.js b/packages/backend/src/controllers/api/v1/admin/apps/get-auth-client.ee.js deleted file mode 100644 index c43ac23e..00000000 --- a/packages/backend/src/controllers/api/v1/admin/apps/get-auth-client.ee.js +++ /dev/null @@ -1,11 +0,0 @@ -import { renderObject } from '../../../../../helpers/renderer.js'; -import AppAuthClient from '../../../../../models/app-auth-client.js'; - -export default async (request, response) => { - const appAuthClient = await AppAuthClient.query() - .findById(request.params.appAuthClientId) - .where({ app_key: request.params.appKey }) - .throwIfNotFound(); - - renderObject(response, appAuthClient); -}; diff --git a/packages/backend/src/controllers/api/v1/admin/apps/get-oauth-client.ee.js b/packages/backend/src/controllers/api/v1/admin/apps/get-oauth-client.ee.js new file mode 100644 index 00000000..577461f2 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/apps/get-oauth-client.ee.js @@ -0,0 +1,11 @@ +import { renderObject } from '../../../../../helpers/renderer.js'; +import OAuthClient from '../../../../../models/oauth-client.js'; + +export default async (request, response) => { + const oauthClient = await OAuthClient.query() + .findById(request.params.oauthClientId) + .where({ app_key: request.params.appKey }) + .throwIfNotFound(); + + renderObject(response, oauthClient); +}; diff --git a/packages/backend/src/controllers/api/v1/admin/apps/get-auth-client.ee.test.js b/packages/backend/src/controllers/api/v1/admin/apps/get-oauth-client.ee.test.js similarity index 56% rename from packages/backend/src/controllers/api/v1/admin/apps/get-auth-client.ee.test.js rename to packages/backend/src/controllers/api/v1/admin/apps/get-oauth-client.ee.test.js index 2edb0ffe..5b30c289 100644 --- a/packages/backend/src/controllers/api/v1/admin/apps/get-auth-client.ee.test.js +++ b/packages/backend/src/controllers/api/v1/admin/apps/get-oauth-client.ee.test.js @@ -5,12 +5,12 @@ import app from '../../../../../app.js'; import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js'; import { createUser } from '../../../../../../test/factories/user.js'; import { createRole } from '../../../../../../test/factories/role.js'; -import getAppAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/get-auth-client.js'; -import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js'; +import getOAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/get-oauth-client.js'; +import { createOAuthClient } from '../../../../../../test/factories/oauth-client.js'; import * as license from '../../../../../helpers/license.ee.js'; -describe('GET /api/v1/admin/apps/:appKey/auth-clients/:appAuthClientId', () => { - let currentUser, adminRole, currentAppAuthClient, token; +describe('GET /api/v1/admin/apps/:appKey/oauth-clients/:oauthClientId', () => { + let currentUser, adminRole, currentOAuthClient, token; beforeEach(async () => { vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); @@ -18,29 +18,29 @@ describe('GET /api/v1/admin/apps/:appKey/auth-clients/:appAuthClientId', () => { adminRole = await createRole({ name: 'Admin' }); currentUser = await createUser({ roleId: adminRole.id }); - currentAppAuthClient = await createAppAuthClient({ + currentOAuthClient = await createOAuthClient({ appKey: 'deepl', }); token = await createAuthTokenByUserId(currentUser.id); }); - it('should return specified app auth client', async () => { + it('should return specified oauth client', async () => { const response = await request(app) - .get(`/api/v1/admin/apps/deepl/auth-clients/${currentAppAuthClient.id}`) + .get(`/api/v1/admin/apps/deepl/oauth-clients/${currentOAuthClient.id}`) .set('Authorization', token) .expect(200); - const expectedPayload = getAppAuthClientMock(currentAppAuthClient); + const expectedPayload = getOAuthClientMock(currentOAuthClient); expect(response.body).toStrictEqual(expectedPayload); }); - it('should return not found response for not existing app auth client ID', async () => { - const notExistingAppAuthClientUUID = Crypto.randomUUID(); + it('should return not found response for not existing oauth client ID', async () => { + const notExistingOAuthClientUUID = Crypto.randomUUID(); await request(app) .get( - `/api/v1/admin/apps/deepl/auth-clients/${notExistingAppAuthClientUUID}` + `/api/v1/admin/apps/deepl/oauth-clients/${notExistingOAuthClientUUID}` ) .set('Authorization', token) .expect(404); @@ -48,7 +48,7 @@ describe('GET /api/v1/admin/apps/:appKey/auth-clients/:appAuthClientId', () => { it('should return bad request response for invalid UUID', async () => { await request(app) - .get('/api/v1/admin/apps/deepl/auth-clients/invalidAppAuthClientUUID') + .get('/api/v1/admin/apps/deepl/oauth-clients/invalidOAuthClientUUID') .set('Authorization', token) .expect(400); }); diff --git a/packages/backend/src/controllers/api/v1/admin/apps/get-auth-clients.ee.js b/packages/backend/src/controllers/api/v1/admin/apps/get-oauth-clients.ee.js similarity index 54% rename from packages/backend/src/controllers/api/v1/admin/apps/get-auth-clients.ee.js rename to packages/backend/src/controllers/api/v1/admin/apps/get-oauth-clients.ee.js index 257e0dd7..230104a4 100644 --- a/packages/backend/src/controllers/api/v1/admin/apps/get-auth-clients.ee.js +++ b/packages/backend/src/controllers/api/v1/admin/apps/get-oauth-clients.ee.js @@ -1,10 +1,10 @@ import { renderObject } from '../../../../../helpers/renderer.js'; -import AppAuthClient from '../../../../../models/app-auth-client.js'; +import OAuthClient from '../../../../../models/oauth-client.js'; export default async (request, response) => { - const appAuthClients = await AppAuthClient.query() + const oauthClients = await OAuthClient.query() .where({ app_key: request.params.appKey }) .orderBy('created_at', 'desc'); - renderObject(response, appAuthClients); + renderObject(response, oauthClients); }; diff --git a/packages/backend/src/controllers/api/v1/admin/apps/get-auth-clients.ee.test.js b/packages/backend/src/controllers/api/v1/admin/apps/get-oauth-clients.ee.test.js similarity index 62% rename from packages/backend/src/controllers/api/v1/admin/apps/get-auth-clients.ee.test.js rename to packages/backend/src/controllers/api/v1/admin/apps/get-oauth-clients.ee.test.js index 7fbba6e0..69be2bbf 100644 --- a/packages/backend/src/controllers/api/v1/admin/apps/get-auth-clients.ee.test.js +++ b/packages/backend/src/controllers/api/v1/admin/apps/get-oauth-clients.ee.test.js @@ -4,11 +4,11 @@ import app from '../../../../../app.js'; import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js'; import { createUser } from '../../../../../../test/factories/user.js'; import { createRole } from '../../../../../../test/factories/role.js'; -import getAuthClientsMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/get-auth-clients.js'; -import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js'; +import getAdminOAuthClientsMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/get-oauth-clients.js'; +import { createOAuthClient } from '../../../../../../test/factories/oauth-client.js'; import * as license from '../../../../../helpers/license.ee.js'; -describe('GET /api/v1/admin/apps/:appKey/auth-clients', () => { +describe('GET /api/v1/admin/apps/:appKey/oauth-clients', () => { let currentUser, adminRole, token; beforeEach(async () => { @@ -20,23 +20,23 @@ describe('GET /api/v1/admin/apps/:appKey/auth-clients', () => { token = await createAuthTokenByUserId(currentUser.id); }); - it('should return specified app auth client info', async () => { - const appAuthClientOne = await createAppAuthClient({ + it('should return specified oauth client info', async () => { + const oauthClientOne = await createOAuthClient({ appKey: 'deepl', }); - const appAuthClientTwo = await createAppAuthClient({ + const oauthClientTwo = await createOAuthClient({ appKey: 'deepl', }); const response = await request(app) - .get('/api/v1/admin/apps/deepl/auth-clients') + .get('/api/v1/admin/apps/deepl/oauth-clients') .set('Authorization', token) .expect(200); - const expectedPayload = getAuthClientsMock([ - appAuthClientTwo, - appAuthClientOne, + const expectedPayload = getAdminOAuthClientsMock([ + oauthClientTwo, + oauthClientOne, ]); expect(response.body).toStrictEqual(expectedPayload); diff --git a/packages/backend/src/controllers/api/v1/admin/apps/update-auth-client.ee.js b/packages/backend/src/controllers/api/v1/admin/apps/update-auth-client.ee.js deleted file mode 100644 index a34e9a67..00000000 --- a/packages/backend/src/controllers/api/v1/admin/apps/update-auth-client.ee.js +++ /dev/null @@ -1,22 +0,0 @@ -import { renderObject } from '../../../../../helpers/renderer.js'; -import AppAuthClient from '../../../../../models/app-auth-client.js'; - -export default async (request, response) => { - const appAuthClient = await AppAuthClient.query() - .findById(request.params.appAuthClientId) - .throwIfNotFound(); - - await appAuthClient.$query().patchAndFetch(appAuthClientParams(request)); - - renderObject(response, appAuthClient); -}; - -const appAuthClientParams = (request) => { - const { active, name, formattedAuthDefaults } = request.body; - - return { - active, - name, - formattedAuthDefaults, - }; -}; 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/admin/apps/update-oauth-client.ee.js b/packages/backend/src/controllers/api/v1/admin/apps/update-oauth-client.ee.js new file mode 100644 index 00000000..7e9c3f7a --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/apps/update-oauth-client.ee.js @@ -0,0 +1,22 @@ +import { renderObject } from '../../../../../helpers/renderer.js'; +import OAuthClient from '../../../../../models/oauth-client.js'; + +export default async (request, response) => { + const oauthClient = await OAuthClient.query() + .findById(request.params.oauthClientId) + .throwIfNotFound(); + + await oauthClient.$query().patchAndFetch(oauthClientParams(request)); + + renderObject(response, oauthClient); +}; + +const oauthClientParams = (request) => { + const { active, name, formattedAuthDefaults } = request.body; + + return { + active, + name, + formattedAuthDefaults, + }; +}; diff --git a/packages/backend/src/controllers/api/v1/admin/apps/update-auth-client.ee.test.js b/packages/backend/src/controllers/api/v1/admin/apps/update-oauth-client.ee.test.js similarity index 65% rename from packages/backend/src/controllers/api/v1/admin/apps/update-auth-client.ee.test.js rename to packages/backend/src/controllers/api/v1/admin/apps/update-oauth-client.ee.test.js index f1a7bccd..9d28bb34 100644 --- a/packages/backend/src/controllers/api/v1/admin/apps/update-auth-client.ee.test.js +++ b/packages/backend/src/controllers/api/v1/admin/apps/update-oauth-client.ee.test.js @@ -6,12 +6,12 @@ import app from '../../../../../app.js'; import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js'; import { createUser } from '../../../../../../test/factories/user.js'; import { createRole } from '../../../../../../test/factories/role.js'; -import updateAppAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/update-auth-client.js'; +import updateOAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/update-oauth-client.js'; import { createAppConfig } from '../../../../../../test/factories/app-config.js'; -import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js'; +import { createOAuthClient } from '../../../../../../test/factories/oauth-client.js'; import * as license from '../../../../../helpers/license.ee.js'; -describe('PATCH /api/v1/admin/apps/:appKey/auth-clients', () => { +describe('PATCH /api/v1/admin/apps/:appKey/oauth-clients', () => { let currentUser, adminRole, token; beforeEach(async () => { @@ -27,8 +27,8 @@ describe('PATCH /api/v1/admin/apps/:appKey/auth-clients', () => { }); }); - it('should return updated entity for valid app auth client', async () => { - const appAuthClient = { + it('should return updated entity for valid oauth client', async () => { + const oauthClient = { active: true, appKey: 'gitlab', formattedAuthDefaults: { @@ -39,33 +39,33 @@ describe('PATCH /api/v1/admin/apps/:appKey/auth-clients', () => { }, }; - const existingAppAuthClient = await createAppAuthClient({ + const existingOAuthClient = await createOAuthClient({ appKey: 'gitlab', name: 'First auth client', }); const response = await request(app) .patch( - `/api/v1/admin/apps/gitlab/auth-clients/${existingAppAuthClient.id}` + `/api/v1/admin/apps/gitlab/oauth-clients/${existingOAuthClient.id}` ) .set('Authorization', token) - .send(appAuthClient) + .send(oauthClient) .expect(200); - const expectedPayload = updateAppAuthClientMock({ - ...existingAppAuthClient, - ...appAuthClient, + const expectedPayload = updateOAuthClientMock({ + ...existingOAuthClient, + ...oauthClient, }); expect(response.body).toMatchObject(expectedPayload); }); - it('should return not found response for not existing app auth client', async () => { - const notExistingAppAuthClientId = Crypto.randomUUID(); + it('should return not found response for not existing oauth client', async () => { + const notExistingOAuthClientId = Crypto.randomUUID(); await request(app) .patch( - `/api/v1/admin/apps/gitlab/auth-clients/${notExistingAppAuthClientId}` + `/api/v1/admin/apps/gitlab/oauth-clients/${notExistingOAuthClientId}` ) .set('Authorization', token) .expect(404); @@ -73,27 +73,27 @@ describe('PATCH /api/v1/admin/apps/:appKey/auth-clients', () => { it('should return bad request response for invalid UUID', async () => { await request(app) - .patch('/api/v1/admin/apps/gitlab/auth-clients/invalidAuthClientUUID') + .patch('/api/v1/admin/apps/gitlab/oauth-clients/invalidAuthClientUUID') .set('Authorization', token) .expect(400); }); it('should return HTTP 422 for invalid payload', async () => { - const appAuthClient = { + const oauthClient = { formattedAuthDefaults: 'invalid input', }; - const existingAppAuthClient = await createAppAuthClient({ + const existingOAuthClient = await createOAuthClient({ appKey: 'gitlab', name: 'First auth client', }); const response = await request(app) .patch( - `/api/v1/admin/apps/gitlab/auth-clients/${existingAppAuthClient.id}` + `/api/v1/admin/apps/gitlab/oauth-clients/${existingOAuthClient.id}` ) .set('Authorization', token) - .send(appAuthClient) + .send(oauthClient) .expect(422); expect(response.body.meta.type).toBe('ModelValidation'); diff --git a/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-role-mappings.ee.js b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-role-mappings.ee.js index 2f7a377f..9c7f5ed7 100644 --- a/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-role-mappings.ee.js +++ b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-role-mappings.ee.js @@ -7,7 +7,7 @@ export default async (request, response) => { .throwIfNotFound(); const roleMappings = await samlAuthProvider - .$relatedQuery('samlAuthProvidersRoleMappings') + .$relatedQuery('roleMappings') .orderBy('remote_role_name', 'asc'); renderObject(response, roleMappings); diff --git a/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js index c0e6afb8..9ca388c1 100644 --- a/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js +++ b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js @@ -8,15 +8,14 @@ export default async (request, response) => { .findById(samlAuthProviderId) .throwIfNotFound(); - const samlAuthProvidersRoleMappings = - await samlAuthProvider.updateRoleMappings( - samlAuthProvidersRoleMappingsParams(request) - ); + const roleMappings = await samlAuthProvider.updateRoleMappings( + roleMappingsParams(request) + ); - renderObject(response, samlAuthProvidersRoleMappings); + renderObject(response, roleMappings); }; -const samlAuthProvidersRoleMappingsParams = (request) => { +const roleMappingsParams = (request) => { const roleMappings = request.body; return roleMappings.map(({ roleId, remoteRoleName }) => ({ diff --git a/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/update-role-mappings.ee.test.js b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/update-role-mappings.ee.test.js index f06a3ba6..a3c12c0f 100644 --- a/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/update-role-mappings.ee.test.js +++ b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/update-role-mappings.ee.test.js @@ -6,7 +6,7 @@ import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by import { createRole } from '../../../../../../test/factories/role.js'; import { createUser } from '../../../../../../test/factories/user.js'; import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js'; -import { createSamlAuthProvidersRoleMapping } from '../../../../../../test/factories/saml-auth-providers-role-mapping.js'; +import { createRoleMapping } from '../../../../../../test/factories/role-mapping.js'; import createRoleMappingsMock from '../../../../../../test/mocks/rest/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js'; import * as license from '../../../../../helpers/license.ee.js'; @@ -21,12 +21,12 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi samlAuthProvider = await createSamlAuthProvider(); - await createSamlAuthProvidersRoleMapping({ + await createRoleMapping({ samlAuthProviderId: samlAuthProvider.id, remoteRoleName: 'Viewer', }); - await createSamlAuthProvidersRoleMapping({ + await createRoleMapping({ samlAuthProviderId: samlAuthProvider.id, remoteRoleName: 'Editor', }); @@ -64,7 +64,7 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi it('should delete role mappings when given empty role mappings', async () => { const existingRoleMappings = await samlAuthProvider.$relatedQuery( - 'samlAuthProvidersRoleMappings' + 'roleMappings' ); expect(existingRoleMappings.length).toBe(2); @@ -149,34 +149,4 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi .send(roleMappings) .expect(404); }); - - it('should not delete existing role mapping when error thrown', async () => { - const roleMappings = [ - { - roleId: userRole.id, - remoteRoleName: { - invalid: 'data', - }, - }, - ]; - - const roleMappingsBeforeRequest = await samlAuthProvider.$relatedQuery( - 'samlAuthProvidersRoleMappings' - ); - - await request(app) - .patch( - `/api/v1/admin/saml-auth-providers/${samlAuthProvider.id}/role-mappings` - ) - .set('Authorization', token) - .send(roleMappings) - .expect(422); - - const roleMappingsAfterRequest = await samlAuthProvider.$relatedQuery( - 'samlAuthProvidersRoleMappings' - ); - - expect(roleMappingsBeforeRequest).toStrictEqual(roleMappingsAfterRequest); - expect(roleMappingsAfterRequest.length).toBe(2); - }); }); diff --git a/packages/backend/src/controllers/api/v1/apps/create-connection.js b/packages/backend/src/controllers/api/v1/apps/create-connection.js index 40a081b9..35e3a34b 100644 --- a/packages/backend/src/controllers/api/v1/apps/create-connection.js +++ b/packages/backend/src/controllers/api/v1/apps/create-connection.js @@ -9,18 +9,18 @@ export default async (request, response) => { .$query() .withGraphFetched({ appConfig: true, - appAuthClient: true, + oauthClient: true, }); renderObject(response, connectionWithAppConfigAndAuthClient, { status: 201 }); }; const connectionParams = (request) => { - const { appAuthClientId, formattedData } = request.body; + const { oauthClientId, formattedData } = request.body; return { key: request.params.appKey, - appAuthClientId, + oauthClientId, formattedData, verified: 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..0465458f 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 @@ -3,7 +3,7 @@ import request from 'supertest'; import app from '../../../../app.js'; import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js'; import { createAppConfig } from '../../../../../test/factories/app-config.js'; -import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js'; +import { createOAuthClient } from '../../../../../test/factories/oauth-client.js'; import { createUser } from '../../../../../test/factories/user.js'; import { createPermission } from '../../../../../test/factories/permission.js'; import { createRole } from '../../../../../test/factories/role.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,17 +266,17 @@ describe('POST /api/v1/apps/:appKey/connections', () => { }); }); - describe('with auth clients enabled', async () => { - let appAuthClient; + describe('with auth client enabled', async () => { + let oauthClient; beforeEach(async () => { await createAppConfig({ key: 'gitlab', disabled: false, - shared: true, + useOnlyPredefinedAuthClients: false, }); - appAuthClient = await createAppAuthClient({ + oauthClient = await createOAuthClient({ appKey: 'gitlab', active: true, formattedAuthDefaults: { @@ -290,7 +290,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => { it('should return created connection', async () => { const connectionData = { - appAuthClientId: appAuthClient.id, + oauthClientId: oauthClient.id, }; const response = await request(app) @@ -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,31 +336,33 @@ describe('POST /api/v1/apps/:appKey/connections', () => { }); }); }); - describe('with auth clients disabled', async () => { - let appAuthClient; + + describe('with auth client disabled', async () => { + let oauthClient; beforeEach(async () => { await createAppConfig({ key: 'gitlab', disabled: false, - shared: false, + useOnlyPredefinedAuthClients: false, }); - appAuthClient = await createAppAuthClient({ + oauthClient = await createOAuthClient({ appKey: 'gitlab', + active: false, }); }); it('should return with not authorized response', async () => { const connectionData = { - appAuthClientId: appAuthClient.id, + oauthClientId: oauthClient.id, }; await request(app) .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-action-substeps.test.js b/packages/backend/src/controllers/api/v1/apps/get-action-substeps.test.js index bc28ae33..e3b6db03 100644 --- a/packages/backend/src/controllers/api/v1/apps/get-action-substeps.test.js +++ b/packages/backend/src/controllers/api/v1/apps/get-action-substeps.test.js @@ -15,7 +15,7 @@ describe('GET /api/v1/apps/:appKey/actions/:actionKey/substeps', () => { exampleApp = await App.findOneByKey('github'); }); - it('should return the app auth info', async () => { + it('should return the action substeps info', async () => { const actions = await App.findActionsByKey('github'); const exampleAction = actions.find( (action) => action.key === 'createIssue' diff --git a/packages/backend/src/controllers/api/v1/apps/get-auth-client.ee.js b/packages/backend/src/controllers/api/v1/apps/get-auth-client.ee.js deleted file mode 100644 index 5aceb529..00000000 --- a/packages/backend/src/controllers/api/v1/apps/get-auth-client.ee.js +++ /dev/null @@ -1,11 +0,0 @@ -import { renderObject } from '../../../../helpers/renderer.js'; -import AppAuthClient from '../../../../models/app-auth-client.js'; - -export default async (request, response) => { - const appAuthClient = await AppAuthClient.query() - .findById(request.params.appAuthClientId) - .where({ app_key: request.params.appKey, active: true }) - .throwIfNotFound(); - - renderObject(response, appAuthClient); -}; diff --git a/packages/backend/src/controllers/api/v1/apps/get-config.ee.js b/packages/backend/src/controllers/api/v1/apps/get-config.ee.js index d0837e35..229c20d1 100644 --- a/packages/backend/src/controllers/api/v1/apps/get-config.ee.js +++ b/packages/backend/src/controllers/api/v1/apps/get-config.ee.js @@ -4,7 +4,7 @@ import AppConfig from '../../../../models/app-config.js'; export default async (request, response) => { const appConfig = await AppConfig.query() .withGraphFetched({ - appAuthClients: true, + oauthClients: true, }) .findOne({ key: request.params.appKey, 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/apps/get-connections.js b/packages/backend/src/controllers/api/v1/apps/get-connections.js index 1f5a91ad..0f2fdfcb 100644 --- a/packages/backend/src/controllers/api/v1/apps/get-connections.js +++ b/packages/backend/src/controllers/api/v1/apps/get-connections.js @@ -9,7 +9,7 @@ export default async (request, response) => { .select('connections.*') .withGraphFetched({ appConfig: true, - appAuthClient: true, + oauthClient: true, }) .fullOuterJoinRelated('steps') .where({ diff --git a/packages/backend/src/controllers/api/v1/apps/get-connections.test.js b/packages/backend/src/controllers/api/v1/apps/get-connections.test.js index 8afb7eef..272f24cf 100644 --- a/packages/backend/src/controllers/api/v1/apps/get-connections.test.js +++ b/packages/backend/src/controllers/api/v1/apps/get-connections.test.js @@ -87,14 +87,14 @@ describe('GET /api/v1/apps/:appKey/connections', () => { it('should return not found response for invalid connection UUID', async () => { await createPermission({ - action: 'update', + action: 'read', subject: 'Connection', roleId: currentUserRole.id, conditions: ['isCreator'], }); await request(app) - .get('/api/v1/connections/invalid-connection-id/connections') + .get('/api/v1/apps/invalid-connection-id/connections') .set('Authorization', token) .expect(404); }); diff --git a/packages/backend/src/controllers/api/v1/apps/get-oauth-client.ee.js b/packages/backend/src/controllers/api/v1/apps/get-oauth-client.ee.js new file mode 100644 index 00000000..2577f27d --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-oauth-client.ee.js @@ -0,0 +1,11 @@ +import { renderObject } from '../../../../helpers/renderer.js'; +import OAuthClient from '../../../../models/oauth-client.js'; + +export default async (request, response) => { + const oauthClient = await OAuthClient.query() + .findById(request.params.oauthClientId) + .where({ app_key: request.params.appKey, active: true }) + .throwIfNotFound(); + + renderObject(response, oauthClient); +}; diff --git a/packages/backend/src/controllers/api/v1/apps/get-auth-client.ee.test.js b/packages/backend/src/controllers/api/v1/apps/get-oauth-client.ee.test.js similarity index 54% rename from packages/backend/src/controllers/api/v1/apps/get-auth-client.ee.test.js rename to packages/backend/src/controllers/api/v1/apps/get-oauth-client.ee.test.js index d5bea452..b39367f8 100644 --- a/packages/backend/src/controllers/api/v1/apps/get-auth-client.ee.test.js +++ b/packages/backend/src/controllers/api/v1/apps/get-oauth-client.ee.test.js @@ -4,46 +4,46 @@ import Crypto from 'crypto'; import app from '../../../../app.js'; import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js'; import { createUser } from '../../../../../test/factories/user.js'; -import getAppAuthClientMock from '../../../../../test/mocks/rest/api/v1/apps/get-auth-client.js'; -import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js'; +import getOAuthClientMock from '../../../../../test/mocks/rest/api/v1/apps/get-oauth-client.js'; +import { createOAuthClient } from '../../../../../test/factories/oauth-client.js'; import * as license from '../../../../helpers/license.ee.js'; -describe('GET /api/v1/apps/:appKey/auth-clients/:appAuthClientId', () => { - let currentUser, currentAppAuthClient, token; +describe('GET /api/v1/apps/:appKey/oauth-clients/:oauthClientId', () => { + let currentUser, currentOAuthClient, token; beforeEach(async () => { vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); currentUser = await createUser(); - currentAppAuthClient = await createAppAuthClient({ + currentOAuthClient = await createOAuthClient({ appKey: 'deepl', }); token = await createAuthTokenByUserId(currentUser.id); }); - it('should return specified app auth client', async () => { + it('should return specified oauth client', async () => { const response = await request(app) - .get(`/api/v1/apps/deepl/auth-clients/${currentAppAuthClient.id}`) + .get(`/api/v1/apps/deepl/oauth-clients/${currentOAuthClient.id}`) .set('Authorization', token) .expect(200); - const expectedPayload = getAppAuthClientMock(currentAppAuthClient); + const expectedPayload = getOAuthClientMock(currentOAuthClient); expect(response.body).toStrictEqual(expectedPayload); }); - it('should return not found response for not existing app auth client ID', async () => { - const notExistingAppAuthClientUUID = Crypto.randomUUID(); + it('should return not found response for not existing oauth client ID', async () => { + const notExistingOAuthClientUUID = Crypto.randomUUID(); await request(app) - .get(`/api/v1/apps/deepl/auth-clients/${notExistingAppAuthClientUUID}`) + .get(`/api/v1/apps/deepl/oauth-clients/${notExistingOAuthClientUUID}`) .set('Authorization', token) .expect(404); }); it('should return bad request response for invalid UUID', async () => { await request(app) - .get('/api/v1/apps/deepl/auth-clients/invalidAppAuthClientUUID') + .get('/api/v1/apps/deepl/oauth-clients/invalidOAuthClientUUID') .set('Authorization', token) .expect(400); }); diff --git a/packages/backend/src/controllers/api/v1/apps/get-auth-clients.ee.js b/packages/backend/src/controllers/api/v1/apps/get-oauth-clients.ee.js similarity index 56% rename from packages/backend/src/controllers/api/v1/apps/get-auth-clients.ee.js rename to packages/backend/src/controllers/api/v1/apps/get-oauth-clients.ee.js index 06eceec1..2a68737b 100644 --- a/packages/backend/src/controllers/api/v1/apps/get-auth-clients.ee.js +++ b/packages/backend/src/controllers/api/v1/apps/get-oauth-clients.ee.js @@ -1,10 +1,10 @@ import { renderObject } from '../../../../helpers/renderer.js'; -import AppAuthClient from '../../../../models/app-auth-client.js'; +import OAuthClient from '../../../../models/oauth-client.js'; export default async (request, response) => { - const appAuthClients = await AppAuthClient.query() + const oauthClients = await OAuthClient.query() .where({ app_key: request.params.appKey, active: true }) .orderBy('created_at', 'desc'); - renderObject(response, appAuthClients); + renderObject(response, oauthClients); }; diff --git a/packages/backend/src/controllers/api/v1/apps/get-auth-clients.ee.test.js b/packages/backend/src/controllers/api/v1/apps/get-oauth-clients.ee.test.js similarity index 59% rename from packages/backend/src/controllers/api/v1/apps/get-auth-clients.ee.test.js rename to packages/backend/src/controllers/api/v1/apps/get-oauth-clients.ee.test.js index d84bf167..4e4b8508 100644 --- a/packages/backend/src/controllers/api/v1/apps/get-auth-clients.ee.test.js +++ b/packages/backend/src/controllers/api/v1/apps/get-oauth-clients.ee.test.js @@ -3,11 +3,11 @@ import request from 'supertest'; import app from '../../../../app.js'; import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js'; import { createUser } from '../../../../../test/factories/user.js'; -import getAuthClientsMock from '../../../../../test/mocks/rest/api/v1/apps/get-auth-clients.js'; -import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js'; +import getOAuthClientsMock from '../../../../../test/mocks/rest/api/v1/apps/get-oauth-clients.js'; +import { createOAuthClient } from '../../../../../test/factories/oauth-client.js'; import * as license from '../../../../helpers/license.ee.js'; -describe('GET /api/v1/apps/:appKey/auth-clients', () => { +describe('GET /api/v1/apps/:appKey/oauth-clients', () => { let currentUser, token; beforeEach(async () => { @@ -18,23 +18,23 @@ describe('GET /api/v1/apps/:appKey/auth-clients', () => { token = await createAuthTokenByUserId(currentUser.id); }); - it('should return specified app auth client info', async () => { - const appAuthClientOne = await createAppAuthClient({ + it('should return specified oauth client info', async () => { + const oauthClientOne = await createOAuthClient({ appKey: 'deepl', }); - const appAuthClientTwo = await createAppAuthClient({ + const oauthClientTwo = await createOAuthClient({ appKey: 'deepl', }); const response = await request(app) - .get('/api/v1/apps/deepl/auth-clients') + .get('/api/v1/apps/deepl/oauth-clients') .set('Authorization', token) .expect(200); - const expectedPayload = getAuthClientsMock([ - appAuthClientTwo, - appAuthClientOne, + const expectedPayload = getOAuthClientsMock([ + oauthClientTwo, + oauthClientOne, ]); expect(response.body).toStrictEqual(expectedPayload); diff --git a/packages/backend/src/controllers/api/v1/apps/get-trigger-substeps.test.js b/packages/backend/src/controllers/api/v1/apps/get-trigger-substeps.test.js index 0748ee5a..e54b6de2 100644 --- a/packages/backend/src/controllers/api/v1/apps/get-trigger-substeps.test.js +++ b/packages/backend/src/controllers/api/v1/apps/get-trigger-substeps.test.js @@ -15,7 +15,7 @@ describe('GET /api/v1/apps/:appKey/triggers/:triggerKey/substeps', () => { exampleApp = await App.findOneByKey('github'); }); - it('should return the app auth info', async () => { + it('should return the trigger substeps info', async () => { const triggers = await App.findTriggersByKey('github'); const exampleTrigger = triggers.find( (trigger) => trigger.key === 'newIssues' 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.js b/packages/backend/src/controllers/api/v1/connections/update-connection.js index 5d84e797..979aa733 100644 --- a/packages/backend/src/controllers/api/v1/connections/update-connection.js +++ b/packages/backend/src/controllers/api/v1/connections/update-connection.js @@ -14,6 +14,6 @@ export default async (request, response) => { }; const connectionParams = (request) => { - const { formattedData, appAuthClientId } = request.body; - return { formattedData, appAuthClientId }; + const { formattedData, oauthClientId } = request.body; + return { formattedData, oauthClientId }; }; 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/controllers/api/v1/steps/create-dynamic-data.test.js b/packages/backend/src/controllers/api/v1/steps/create-dynamic-data.test.js index 64b1c254..af3f22e2 100644 --- a/packages/backend/src/controllers/api/v1/steps/create-dynamic-data.test.js +++ b/packages/backend/src/controllers/api/v1/steps/create-dynamic-data.test.js @@ -193,7 +193,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => { const notExistingStepUUID = Crypto.randomUUID(); await request(app) - .get(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`) + .post(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`) .set('Authorization', token) .expect(404); }); @@ -216,7 +216,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => { const step = await createStep({ appKey: null }); await request(app) - .get(`/api/v1/steps/${step.id}/dynamic-data`) + .post(`/api/v1/steps/${step.id}/dynamic-data`) .set('Authorization', token) .expect(404); }); diff --git a/packages/backend/src/controllers/api/v1/steps/create-dynamic-fields.test.js b/packages/backend/src/controllers/api/v1/steps/create-dynamic-fields.test.js index 3081351d..49d7f57f 100644 --- a/packages/backend/src/controllers/api/v1/steps/create-dynamic-fields.test.js +++ b/packages/backend/src/controllers/api/v1/steps/create-dynamic-fields.test.js @@ -118,7 +118,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => { const notExistingStepUUID = Crypto.randomUUID(); await request(app) - .get(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`) + .post(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`) .set('Authorization', token) .expect(404); }); @@ -138,10 +138,11 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => { conditions: [], }); - const step = await createStep({ appKey: null }); + const step = await createStep(); + await step.$query().patch({ appKey: null }); await request(app) - .get(`/api/v1/steps/${step.id}/dynamic-fields`) + .post(`/api/v1/steps/${step.id}/dynamic-fields`) .set('Authorization', token) .expect(404); }); diff --git a/packages/backend/src/controllers/api/v1/users/get-apps.js b/packages/backend/src/controllers/api/v1/users/get-apps.js index 801fbc71..94a4ddf6 100644 --- a/packages/backend/src/controllers/api/v1/users/get-apps.js +++ b/packages/backend/src/controllers/api/v1/users/get-apps.js @@ -3,5 +3,5 @@ import { renderObject } from '../../../../helpers/renderer.js'; export default async (request, response) => { const apps = await request.currentUser.getApps(request.query.name); - renderObject(response, apps, { serializer: 'App' }); + renderObject(response, apps, { serializer: 'UserApp' }); }; diff --git a/packages/backend/src/db/migrations/20241125141647_rename_saml_auth_providers_role_mappings_as_role_mappings.js b/packages/backend/src/db/migrations/20241125141647_rename_saml_auth_providers_role_mappings_as_role_mappings.js new file mode 100644 index 00000000..b8abfe85 --- /dev/null +++ b/packages/backend/src/db/migrations/20241125141647_rename_saml_auth_providers_role_mappings_as_role_mappings.js @@ -0,0 +1,52 @@ +export async function up(knex) { + await knex.schema.createTable('role_mappings', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table + .uuid('saml_auth_provider_id') + .references('id') + .inTable('saml_auth_providers'); + table.uuid('role_id').references('id').inTable('roles'); + table.string('remote_role_name').notNullable(); + + table.unique(['saml_auth_provider_id', 'remote_role_name']); + + table.timestamps(true, true); + }); + + const existingRoleMappings = await knex('saml_auth_providers_role_mappings'); + + if (existingRoleMappings.length) { + await knex('role_mappings').insert(existingRoleMappings); + } + + return await knex.schema.dropTable('saml_auth_providers_role_mappings'); +} + +export async function down(knex) { + await knex.schema.createTable( + 'saml_auth_providers_role_mappings', + (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table + .uuid('saml_auth_provider_id') + .references('id') + .inTable('saml_auth_providers'); + table.uuid('role_id').references('id').inTable('roles'); + table.string('remote_role_name').notNullable(); + + table.unique(['saml_auth_provider_id', 'remote_role_name']); + + table.timestamps(true, true); + } + ); + + const existingRoleMappings = await knex('role_mappings'); + + if (existingRoleMappings.length) { + await knex('saml_auth_providers_role_mappings').insert( + existingRoleMappings + ); + } + + return await knex.schema.dropTable('role_mappings'); +} 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/db/migrations/20241217170447_change_app_auth_clients_as_oauth_clients.js b/packages/backend/src/db/migrations/20241217170447_change_app_auth_clients_as_oauth_clients.js new file mode 100644 index 00000000..a26ad1f4 --- /dev/null +++ b/packages/backend/src/db/migrations/20241217170447_change_app_auth_clients_as_oauth_clients.js @@ -0,0 +1,31 @@ +export async function up(knex) { + await knex.schema.renameTable('app_auth_clients', 'oauth_clients'); + + await knex.schema.raw( + 'ALTER INDEX app_auth_clients_pkey RENAME TO oauth_clients_pkey' + ); + + await knex.schema.raw( + 'ALTER INDEX app_auth_clients_name_unique RENAME TO oauth_clients_name_unique' + ); + + return await knex.schema.alterTable('connections', (table) => { + table.renameColumn('app_auth_client_id', 'oauth_client_id'); + }); +} + +export async function down(knex) { + await knex.schema.renameTable('oauth_clients', 'app_auth_clients'); + + await knex.schema.raw( + 'ALTER INDEX oauth_clients_pkey RENAME TO app_auth_clients_pkey' + ); + + await knex.schema.raw( + 'ALTER INDEX oauth_clients_name_unique RENAME TO app_auth_clients_name_unique' + ); + + return await knex.schema.alterTable('connections', (table) => { + table.renameColumn('oauth_client_id', 'app_auth_client_id'); + }); +} diff --git a/packages/backend/src/helpers/add-authentication-steps.js b/packages/backend/src/helpers/add-authentication-steps.js index 5e7a462a..ee1bc85b 100644 --- a/packages/backend/src/helpers/add-authentication-steps.js +++ b/packages/backend/src/helpers/add-authentication-steps.js @@ -88,8 +88,8 @@ const sharedAuthenticationStepsWithAuthUrl = [ value: '{key}', }, { - name: 'appAuthClientId', - value: '{appAuthClientId}', + name: 'oauthClientId', + value: '{oauthClientId}', }, ], }, diff --git a/packages/backend/src/helpers/find-or-create-user-by-saml-identity.ee.js b/packages/backend/src/helpers/find-or-create-user-by-saml-identity.ee.js index daeb1206..2349b60d 100644 --- a/packages/backend/src/helpers/find-or-create-user-by-saml-identity.ee.js +++ b/packages/backend/src/helpers/find-or-create-user-by-saml-identity.ee.js @@ -30,7 +30,7 @@ const findOrCreateUserBySamlIdentity = async ( : [mappedUser.role]; const samlAuthProviderRoleMapping = await samlAuthProvider - .$relatedQuery('samlAuthProvidersRoleMappings') + .$relatedQuery('roleMappings') .whereIn('remote_role_name', mappedRoles) .limit(1) .first(); diff --git a/packages/backend/src/helpers/user-ability.test.js b/packages/backend/src/helpers/user-ability.test.js new file mode 100644 index 00000000..906a5fb3 --- /dev/null +++ b/packages/backend/src/helpers/user-ability.test.js @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest'; +import userAbility from './user-ability.js'; + +describe('userAbility', () => { + it('should return PureAbility instantiated with user permissions', () => { + const user = { + permissions: [ + { + subject: 'Flow', + action: 'read', + conditions: ['isCreator'], + }, + ], + role: { + name: 'User', + }, + }; + + const ability = userAbility(user); + + expect(ability.rules).toStrictEqual(user.permissions); + }); + + it('should return permission-less PureAbility for user with no role', () => { + const user = { + permissions: [ + { + subject: 'Flow', + action: 'read', + conditions: ['isCreator'], + }, + ], + role: null, + }; + const ability = userAbility(user); + + expect(ability.rules).toStrictEqual([]); + }); + + it('should return permission-less PureAbility for user with no permissions', () => { + const user = { permissions: null, role: { name: 'User' } }; + const ability = userAbility(user); + + expect(ability.rules).toStrictEqual([]); + }); +}); diff --git a/packages/backend/src/jobs/delete-user.ee.js b/packages/backend/src/jobs/delete-user.ee.js new file mode 100644 index 00000000..a6d58f33 --- /dev/null +++ b/packages/backend/src/jobs/delete-user.ee.js @@ -0,0 +1,37 @@ +import appConfig from '../config/app.js'; +import User from '../models/user.js'; +import ExecutionStep from '../models/execution-step.js'; + +export const deleteUserJob = async (job) => { + const { id } = job.data; + + const user = await User.query() + .withSoftDeleted() + .findById(id) + .throwIfNotFound(); + + const executionIds = ( + await user + .$relatedQuery('executions') + .withSoftDeleted() + .select('executions.id') + ).map((execution) => execution.id); + + await ExecutionStep.query() + .withSoftDeleted() + .whereIn('execution_id', executionIds) + .hardDelete(); + await user.$relatedQuery('executions').withSoftDeleted().hardDelete(); + await user.$relatedQuery('steps').withSoftDeleted().hardDelete(); + await user.$relatedQuery('flows').withSoftDeleted().hardDelete(); + await user.$relatedQuery('connections').withSoftDeleted().hardDelete(); + await user.$relatedQuery('identities').withSoftDeleted().hardDelete(); + + if (appConfig.isCloud) { + await user.$relatedQuery('subscriptions').withSoftDeleted().hardDelete(); + await user.$relatedQuery('usageData').withSoftDeleted().hardDelete(); + } + + await user.$relatedQuery('accessTokens').withSoftDeleted().hardDelete(); + await user.$query().withSoftDeleted().hardDelete(); +}; diff --git a/packages/backend/src/jobs/execute-action.js b/packages/backend/src/jobs/execute-action.js new file mode 100644 index 00000000..2d283c11 --- /dev/null +++ b/packages/backend/src/jobs/execute-action.js @@ -0,0 +1,46 @@ +import Step from '../models/step.js'; +import actionQueue from '../queues/action.js'; +import { processAction } from '../services/action.js'; +import { + REMOVE_AFTER_30_DAYS_OR_150_JOBS, + REMOVE_AFTER_7_DAYS_OR_50_JOBS, +} from '../helpers/remove-job-configuration.js'; +import delayAsMilliseconds from '../helpers/delay-as-milliseconds.js'; + +const DEFAULT_DELAY_DURATION = 0; + +export const executeActionJob = async (job) => { + const { stepId, flowId, executionId, computedParameters, executionStep } = + await processAction(job.data); + + if (executionStep.isFailed) return; + + const step = await Step.query().findById(stepId).throwIfNotFound(); + const nextStep = await step.getNextStep(); + + if (!nextStep) return; + + const jobName = `${executionId}-${nextStep.id}`; + + const jobPayload = { + flowId, + executionId, + stepId: nextStep.id, + }; + + const jobOptions = { + removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS, + removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS, + delay: DEFAULT_DELAY_DURATION, + }; + + if (step.appKey === 'delay') { + jobOptions.delay = delayAsMilliseconds(step.key, computedParameters); + } + + if (step.appKey === 'filter' && !executionStep.dataOut) { + return; + } + + await actionQueue.add(jobName, jobPayload, jobOptions); +}; diff --git a/packages/backend/src/jobs/execute-flow.js b/packages/backend/src/jobs/execute-flow.js new file mode 100644 index 00000000..ac6e0634 --- /dev/null +++ b/packages/backend/src/jobs/execute-flow.js @@ -0,0 +1,54 @@ +import triggerQueue from '../queues/trigger.js'; +import { processFlow } from '../services/flow.js'; +import Flow from '../models/flow.js'; +import { + REMOVE_AFTER_30_DAYS_OR_150_JOBS, + REMOVE_AFTER_7_DAYS_OR_50_JOBS, +} from '../helpers/remove-job-configuration.js'; + +export const executeFlowJob = async (job) => { + const { flowId } = job.data; + + const flow = await Flow.query().findById(flowId).throwIfNotFound(); + const user = await flow.$relatedQuery('user'); + const allowedToRunFlows = await user.isAllowedToRunFlows(); + + if (!allowedToRunFlows) { + return; + } + + const triggerStep = await flow.getTriggerStep(); + + const { data, error } = await processFlow({ flowId }); + + const reversedData = data.reverse(); + + const jobOptions = { + removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS, + removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS, + }; + + for (const triggerItem of reversedData) { + const jobName = `${triggerStep.id}-${triggerItem.meta.internalId}`; + + const jobPayload = { + flowId, + stepId: triggerStep.id, + triggerItem, + }; + + await triggerQueue.add(jobName, jobPayload, jobOptions); + } + + if (error) { + const jobName = `${triggerStep.id}-error`; + + const jobPayload = { + flowId, + stepId: triggerStep.id, + error, + }; + + await triggerQueue.add(jobName, jobPayload, jobOptions); + } +}; diff --git a/packages/backend/src/jobs/execute-trigger.js b/packages/backend/src/jobs/execute-trigger.js new file mode 100644 index 00000000..b81d6ff7 --- /dev/null +++ b/packages/backend/src/jobs/execute-trigger.js @@ -0,0 +1,32 @@ +import actionQueue from '../queues/action.js'; +import Step from '../models/step.js'; +import { processTrigger } from '../services/trigger.js'; +import { + REMOVE_AFTER_30_DAYS_OR_150_JOBS, + REMOVE_AFTER_7_DAYS_OR_50_JOBS, +} from '../helpers/remove-job-configuration.js'; + +export const executeTriggerJob = async (job) => { + const { flowId, executionId, stepId, executionStep } = await processTrigger( + job.data + ); + + if (executionStep.isFailed) return; + + const step = await Step.query().findById(stepId).throwIfNotFound(); + const nextStep = await step.getNextStep(); + const jobName = `${executionId}-${nextStep.id}`; + + const jobPayload = { + flowId, + executionId, + stepId: nextStep.id, + }; + + const jobOptions = { + removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS, + removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS, + }; + + await actionQueue.add(jobName, jobPayload, jobOptions); +}; diff --git a/packages/backend/src/jobs/remove-cancelled-subscriptions.ee.js b/packages/backend/src/jobs/remove-cancelled-subscriptions.ee.js new file mode 100644 index 00000000..b8d33619 --- /dev/null +++ b/packages/backend/src/jobs/remove-cancelled-subscriptions.ee.js @@ -0,0 +1,15 @@ +import { DateTime } from 'luxon'; +import Subscription from '../models/subscription.ee.js'; + +export const removeCancelledSubscriptionsJob = async () => { + await Subscription.query() + .delete() + .where({ + status: 'deleted', + }) + .andWhere( + 'cancellation_effective_date', + '<=', + DateTime.now().startOf('day').toISODate() + ); +}; diff --git a/packages/backend/src/jobs/send-email.js b/packages/backend/src/jobs/send-email.js new file mode 100644 index 00000000..ed818493 --- /dev/null +++ b/packages/backend/src/jobs/send-email.js @@ -0,0 +1,31 @@ +import logger from '../helpers/logger.js'; +import mailer from '../helpers/mailer.ee.js'; +import compileEmail from '../helpers/compile-email.ee.js'; +import appConfig from '../config/app.js'; + +export const sendEmailJob = async (job) => { + const { email, subject, template, params } = job.data; + + if (isCloudSandbox() && !isAutomatischEmail(email)) { + logger.info( + 'Only Automatisch emails are allowed for non-production environments!' + ); + + return; + } + + await mailer.sendMail({ + to: email, + from: appConfig.fromEmail, + subject: subject, + html: compileEmail(template, params), + }); +}; + +const isCloudSandbox = () => { + return appConfig.isCloud && !appConfig.isProd; +}; + +const isAutomatischEmail = (email) => { + return email.endsWith('@automatisch.io'); +}; 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/__snapshots__/connection.test.js.snap b/packages/backend/src/models/__snapshots__/connection.test.js.snap index 9fc77caf..405133b0 100644 --- a/packages/backend/src/models/__snapshots__/connection.test.js.snap +++ b/packages/backend/src/models/__snapshots__/connection.test.js.snap @@ -3,10 +3,6 @@ exports[`Connection model > jsonSchema should have correct validations 1`] = ` { "properties": { - "appAuthClientId": { - "format": "uuid", - "type": "string", - }, "createdAt": { "type": "string", }, @@ -31,6 +27,10 @@ exports[`Connection model > jsonSchema should have correct validations 1`] = ` "minLength": 1, "type": "string", }, + "oauthClientId": { + "format": "uuid", + "type": "string", + }, "updatedAt": { "type": "string", }, diff --git a/packages/backend/src/models/__snapshots__/app-auth-client.test.js.snap b/packages/backend/src/models/__snapshots__/oauth-client.test.js.snap similarity index 87% rename from packages/backend/src/models/__snapshots__/app-auth-client.test.js.snap rename to packages/backend/src/models/__snapshots__/oauth-client.test.js.snap index 87b5cc8c..04b38119 100644 --- a/packages/backend/src/models/__snapshots__/app-auth-client.test.js.snap +++ b/packages/backend/src/models/__snapshots__/oauth-client.test.js.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`AppAuthClient model > jsonSchema should have correct validations 1`] = ` +exports[`OAuthClient model > jsonSchema should have correct validations 1`] = ` { "properties": { "active": { diff --git a/packages/backend/src/models/__snapshots__/role-mapping.ee.test.js.snap b/packages/backend/src/models/__snapshots__/role-mapping.ee.test.js.snap new file mode 100644 index 00000000..fceaa4b0 --- /dev/null +++ b/packages/backend/src/models/__snapshots__/role-mapping.ee.test.js.snap @@ -0,0 +1,30 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`RoleMapping model > jsonSchema should have the correct schema 1`] = ` +{ + "properties": { + "id": { + "format": "uuid", + "type": "string", + }, + "remoteRoleName": { + "minLength": 1, + "type": "string", + }, + "roleId": { + "format": "uuid", + "type": "string", + }, + "samlAuthProviderId": { + "format": "uuid", + "type": "string", + }, + }, + "required": [ + "samlAuthProviderId", + "roleId", + "remoteRoleName", + ], + "type": "object", +} +`; diff --git a/packages/backend/src/models/__snapshots__/saml-auth-providers-role-mapping.ee.test.js.snap b/packages/backend/src/models/__snapshots__/saml-auth-providers-role-mapping.ee.test.js.snap index fca95d14..fceaa4b0 100644 --- a/packages/backend/src/models/__snapshots__/saml-auth-providers-role-mapping.ee.test.js.snap +++ b/packages/backend/src/models/__snapshots__/saml-auth-providers-role-mapping.ee.test.js.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`SamlAuthProvidersRoleMapping model > jsonSchema should have the correct schema 1`] = ` +exports[`RoleMapping model > jsonSchema should have the correct schema 1`] = ` { "properties": { "id": { diff --git a/packages/backend/src/models/app-auth-client.test.js b/packages/backend/src/models/app-auth-client.test.js deleted file mode 100644 index af1fefc2..00000000 --- a/packages/backend/src/models/app-auth-client.test.js +++ /dev/null @@ -1,284 +0,0 @@ -import { describe, it, expect, vi } from 'vitest'; -import AES from 'crypto-js/aes.js'; -import enc from 'crypto-js/enc-utf8.js'; - -import AppConfig from './app-config.js'; -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', () => { - expect(AppAuthClient.tableName).toBe('app_auth_clients'); - }); - - it('jsonSchema should have correct validations', () => { - expect(AppAuthClient.jsonSchema).toMatchSnapshot(); - }); - - it('relationMappings should return correct associations', () => { - const relationMappings = AppAuthClient.relationMappings(); - - const expectedRelations = { - appConfig: { - relation: Base.BelongsToOneRelation, - modelClass: AppConfig, - join: { - from: 'app_auth_clients.app_key', - to: 'app_configs.key', - }, - }, - }; - - expect(relationMappings).toStrictEqual(expectedRelations); - }); - - describe('encryptData', () => { - it('should return undefined if eligibleForEncryption is not true', async () => { - vi.spyOn( - AppAuthClient.prototype, - 'eligibleForEncryption' - ).mockReturnValue(false); - - const appAuthClient = new AppAuthClient(); - - expect(appAuthClient.encryptData()).toBeUndefined(); - }); - - it('should encrypt formattedAuthDefaults and set it to authDefaults', async () => { - vi.spyOn( - AppAuthClient.prototype, - 'eligibleForEncryption' - ).mockReturnValue(true); - - const formattedAuthDefaults = { - key: 'value', - }; - - const appAuthClient = new AppAuthClient(); - appAuthClient.formattedAuthDefaults = formattedAuthDefaults; - appAuthClient.encryptData(); - - const expectedDecryptedValue = JSON.parse( - AES.decrypt( - appAuthClient.authDefaults, - appConfig.encryptionKey - ).toString(enc) - ); - - expect(formattedAuthDefaults).toStrictEqual(expectedDecryptedValue); - expect(appAuthClient.authDefaults).not.toStrictEqual( - formattedAuthDefaults - ); - }); - - it('should encrypt formattedAuthDefaults and remove formattedAuthDefaults', async () => { - vi.spyOn( - AppAuthClient.prototype, - 'eligibleForEncryption' - ).mockReturnValue(true); - - const formattedAuthDefaults = { - key: 'value', - }; - - const appAuthClient = new AppAuthClient(); - appAuthClient.formattedAuthDefaults = formattedAuthDefaults; - appAuthClient.encryptData(); - - expect(appAuthClient.formattedAuthDefaults).not.toBeDefined(); - }); - }); - - describe('decryptData', () => { - it('should return undefined if eligibleForDecryption is not true', () => { - vi.spyOn( - AppAuthClient.prototype, - 'eligibleForDecryption' - ).mockReturnValue(false); - - const appAuthClient = new AppAuthClient(); - - expect(appAuthClient.decryptData()).toBeUndefined(); - }); - - it('should decrypt authDefaults and set it to formattedAuthDefaults', async () => { - vi.spyOn( - AppAuthClient.prototype, - 'eligibleForDecryption' - ).mockReturnValue(true); - - const formattedAuthDefaults = { - key: 'value', - }; - - const authDefaults = AES.encrypt( - JSON.stringify(formattedAuthDefaults), - appConfig.encryptionKey - ).toString(); - - const appAuthClient = new AppAuthClient(); - appAuthClient.authDefaults = authDefaults; - appAuthClient.decryptData(); - - expect(appAuthClient.formattedAuthDefaults).toStrictEqual( - formattedAuthDefaults - ); - expect(appAuthClient.authDefaults).not.toStrictEqual( - formattedAuthDefaults - ); - }); - }); - - describe('eligibleForEncryption', () => { - it('should return true when formattedAuthDefaults property exists', async () => { - const appAuthClient = await createAppAuthClient(); - - expect(appAuthClient.eligibleForEncryption()).toBe(true); - }); - - it("should return false when formattedAuthDefaults property doesn't exist", async () => { - const appAuthClient = await createAppAuthClient(); - - delete appAuthClient.formattedAuthDefaults; - - expect(appAuthClient.eligibleForEncryption()).toBe(false); - }); - }); - - describe('eligibleForDecryption', () => { - it('should return true when authDefaults property exists', async () => { - const appAuthClient = await createAppAuthClient(); - - expect(appAuthClient.eligibleForDecryption()).toBe(true); - }); - - it("should return false when authDefaults property doesn't exist", async () => { - const appAuthClient = await createAppAuthClient(); - - delete appAuthClient.authDefaults; - - expect(appAuthClient.eligibleForDecryption()).toBe(false); - }); - }); - - 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, - 'encryptData' - ); - - await createAppAuthClient(); - - 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(); - - const appAuthClientBeforeUpdateSpy = vi.spyOn( - AppAuthClient.prototype, - 'encryptData' - ); - - await appAuthClient.$query().patchAndFetch({ name: 'sample' }); - - 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(); - - const appAuthClientAfterFindSpy = vi.spyOn( - AppAuthClient.prototype, - 'decryptData' - ); - - await appAuthClient.$query(); - - expect(appAuthClientAfterFindSpy).toHaveBeenCalledOnce(); - }); -}); diff --git a/packages/backend/src/models/app-config.js b/packages/backend/src/models/app-config.js index 1a9176b9..fe7e2d44 100644 --- a/packages/backend/src/models/app-config.js +++ b/packages/backend/src/models/app-config.js @@ -1,5 +1,5 @@ import App from './app.js'; -import AppAuthClient from './app-auth-client.js'; +import OAuthClient from './oauth-client.js'; import Base from './base.js'; class AppConfig extends Base { @@ -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' }, @@ -26,12 +24,12 @@ class AppConfig extends Base { }; static relationMappings = () => ({ - appAuthClients: { + oauthClients: { relation: Base.HasManyRelation, - modelClass: AppAuthClient, + modelClass: OAuthClient, join: { from: 'app_configs.key', - to: 'app_auth_clients.app_key', + to: 'oauth_clients.app_key', }, }, }); @@ -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..a68b393f 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'; +import OAuthClient from './oauth-client.js'; describe('AppConfig model', () => { it('tableName should return correct name', () => { @@ -24,12 +22,12 @@ describe('AppConfig model', () => { const relationMappings = AppConfig.relationMappings(); const expectedRelations = { - appAuthClients: { + oauthClients: { relation: Base.HasManyRelation, - modelClass: AppAuthClient, + modelClass: OAuthClient, join: { from: 'app_configs.key', - to: 'app_auth_clients.app_key', + to: 'oauth_clients.app_key', }, }, }; @@ -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..5b4c7c66 100644 --- a/packages/backend/src/models/connection.js +++ b/packages/backend/src/models/connection.js @@ -2,7 +2,7 @@ import AES from 'crypto-js/aes.js'; import enc from 'crypto-js/enc-utf8.js'; import App from './app.js'; import AppConfig from './app-config.js'; -import AppAuthClient from './app-auth-client.js'; +import OAuthClient from './oauth-client.js'; import Base from './base.js'; import User from './user.js'; import Step from './step.js'; @@ -24,7 +24,7 @@ class Connection extends Base { data: { type: 'string' }, formattedData: { type: 'object' }, userId: { type: 'string', format: 'uuid' }, - appAuthClientId: { type: 'string', format: 'uuid' }, + oauthClientId: { type: 'string', format: 'uuid' }, verified: { type: 'boolean', default: false }, draft: { type: 'boolean' }, deletedAt: { type: 'string' }, @@ -33,10 +33,6 @@ class Connection extends Base { }, }; - static get virtualAttributes() { - return ['reconnectable']; - } - static relationMappings = () => ({ user: { relation: Base.BelongsToOneRelation, @@ -73,28 +69,16 @@ class Connection extends Base { to: 'app_configs.key', }, }, - appAuthClient: { + oauthClient: { relation: Base.BelongsToOneRelation, - modelClass: AppAuthClient, + modelClass: OAuthClient, join: { - from: 'connections.app_auth_client_id', - to: 'app_auth_clients.id', + from: 'connections.oauth_client_id', + to: 'oauth_clients.id', }, }, }); - 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,22 +128,16 @@ 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) + .$relatedQuery('oauthClients') + .findById(this.oauthClientId) .where({ active: true }) .throwIfNotFound(); @@ -237,13 +215,13 @@ class Connection extends Base { return updatedConnection; } - async updateFormattedData({ formattedData, appAuthClientId }) { - if (appAuthClientId) { - const appAuthClient = await AppAuthClient.query() - .findById(appAuthClientId) + async updateFormattedData({ formattedData, oauthClientId }) { + if (oauthClientId) { + const oauthClient = await OAuthClient.query() + .findById(oauthClientId) .throwIfNotFound(); - formattedData = appAuthClient.formattedAuthDefaults; + formattedData = oauthClient.formattedAuthDefaults; } return await this.$query().patchAndFetch({ diff --git a/packages/backend/src/models/connection.test.js b/packages/backend/src/models/connection.test.js index 7c5057bb..58410eef 100644 --- a/packages/backend/src/models/connection.test.js +++ b/packages/backend/src/models/connection.test.js @@ -2,7 +2,7 @@ import { describe, it, expect, vi } from 'vitest'; import AES from 'crypto-js/aes.js'; import enc from 'crypto-js/enc-utf8.js'; import appConfig from '../config/app.js'; -import AppAuthClient from './app-auth-client.js'; +import OAuthClient from './oauth-client.js'; import App from './app.js'; import AppConfig from './app-config.js'; import Base from './base.js'; @@ -12,7 +12,7 @@ import User from './user.js'; import Telemetry from '../helpers/telemetry/index.js'; import { createConnection } from '../../test/factories/connection.js'; import { createAppConfig } from '../../test/factories/app-config.js'; -import { createAppAuthClient } from '../../test/factories/app-auth-client.js'; +import { createOAuthClient } from '../../test/factories/oauth-client.js'; describe('Connection model', () => { it('tableName should return correct name', () => { @@ -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(); @@ -69,12 +61,12 @@ describe('Connection model', () => { to: 'app_configs.key', }, }, - appAuthClient: { + oauthClient: { relation: Base.BelongsToOneRelation, - modelClass: AppAuthClient, + modelClass: OAuthClient, join: { - from: 'connections.app_auth_client_id', - to: 'app_auth_clients.id', + from: 'connections.oauth_client_id', + to: 'oauth_clients.id', }, }, }; @@ -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,35 +307,13 @@ 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 () => { + it('should apply oauth client auth defaults when creating with shared oauth client', async () => { await createAppConfig({ key: 'gitlab', disabled: false, - customConnectionAllowed: true, - shared: true, }); - const appAuthClient = await createAppAuthClient({ + const oauthClient = await createOAuthClient({ appKey: 'gitlab', active: true, formattedAuthDefaults: { @@ -424,7 +323,7 @@ describe('Connection model', () => { const connection = await createConnection({ key: 'gitlab', - appAuthClientId: appAuthClient.id, + oauthClientId: oauthClient.id, formattedData: null, }); @@ -660,22 +559,22 @@ describe('Connection model', () => { }); describe('updateFormattedData', () => { - it('should extend connection data with app auth client auth defaults', async () => { - const appAuthClient = await createAppAuthClient({ + it('should extend connection data with oauth client auth defaults', async () => { + const oauthClient = await createOAuthClient({ formattedAuthDefaults: { clientId: 'sample-id', }, }); const connection = await createConnection({ - appAuthClientId: appAuthClient.id, + oauthClientId: oauthClient.id, formattedData: { token: 'sample-token', }, }); const updatedConnection = await connection.updateFormattedData({ - appAuthClientId: appAuthClient.id, + oauthClientId: oauthClient.id, }); expect(updatedConnection.formattedData).toStrictEqual({ diff --git a/packages/backend/src/models/app-auth-client.js b/packages/backend/src/models/oauth-client.js similarity index 78% rename from packages/backend/src/models/app-auth-client.js rename to packages/backend/src/models/oauth-client.js index 90a9bda3..d4c253a4 100644 --- a/packages/backend/src/models/app-auth-client.js +++ b/packages/backend/src/models/oauth-client.js @@ -4,8 +4,8 @@ import appConfig from '../config/app.js'; import Base from './base.js'; import AppConfig from './app-config.js'; -class AppAuthClient extends Base { - static tableName = 'app_auth_clients'; +class OAuthClient extends Base { + static tableName = 'oauth_clients'; static jsonSchema = { type: 'object', @@ -27,7 +27,7 @@ class AppAuthClient extends Base { relation: Base.BelongsToOneRelation, modelClass: AppConfig, join: { - from: 'app_auth_clients.app_key', + from: 'oauth_clients.app_key', to: 'app_configs.key', }, }, @@ -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() { @@ -100,4 +87,4 @@ class AppAuthClient extends Base { } } -export default AppAuthClient; +export default OAuthClient; diff --git a/packages/backend/src/models/oauth-client.test.js b/packages/backend/src/models/oauth-client.test.js new file mode 100644 index 00000000..e1d17154 --- /dev/null +++ b/packages/backend/src/models/oauth-client.test.js @@ -0,0 +1,192 @@ +import { describe, it, expect, vi } from 'vitest'; +import AES from 'crypto-js/aes.js'; +import enc from 'crypto-js/enc-utf8.js'; + +import AppConfig from './app-config.js'; +import OAuthClient from './oauth-client.js'; +import Base from './base.js'; +import appConfig from '../config/app.js'; +import { createOAuthClient } from '../../test/factories/oauth-client.js'; + +describe('OAuthClient model', () => { + it('tableName should return correct name', () => { + expect(OAuthClient.tableName).toBe('oauth_clients'); + }); + + it('jsonSchema should have correct validations', () => { + expect(OAuthClient.jsonSchema).toMatchSnapshot(); + }); + + it('relationMappings should return correct associations', () => { + const relationMappings = OAuthClient.relationMappings(); + + const expectedRelations = { + appConfig: { + relation: Base.BelongsToOneRelation, + modelClass: AppConfig, + join: { + from: 'oauth_clients.app_key', + to: 'app_configs.key', + }, + }, + }; + + expect(relationMappings).toStrictEqual(expectedRelations); + }); + + describe('encryptData', () => { + it('should return undefined if eligibleForEncryption is not true', async () => { + vi.spyOn(OAuthClient.prototype, 'eligibleForEncryption').mockReturnValue( + false + ); + + const oauthClient = new OAuthClient(); + + expect(oauthClient.encryptData()).toBeUndefined(); + }); + + it('should encrypt formattedAuthDefaults and set it to authDefaults', async () => { + vi.spyOn(OAuthClient.prototype, 'eligibleForEncryption').mockReturnValue( + true + ); + + const formattedAuthDefaults = { + key: 'value', + }; + + const oauthClient = new OAuthClient(); + oauthClient.formattedAuthDefaults = formattedAuthDefaults; + oauthClient.encryptData(); + + const expectedDecryptedValue = JSON.parse( + AES.decrypt(oauthClient.authDefaults, appConfig.encryptionKey).toString( + enc + ) + ); + + expect(formattedAuthDefaults).toStrictEqual(expectedDecryptedValue); + expect(oauthClient.authDefaults).not.toStrictEqual(formattedAuthDefaults); + }); + + it('should encrypt formattedAuthDefaults and remove formattedAuthDefaults', async () => { + vi.spyOn(OAuthClient.prototype, 'eligibleForEncryption').mockReturnValue( + true + ); + + const formattedAuthDefaults = { + key: 'value', + }; + + const oauthClient = new OAuthClient(); + oauthClient.formattedAuthDefaults = formattedAuthDefaults; + oauthClient.encryptData(); + + expect(oauthClient.formattedAuthDefaults).not.toBeDefined(); + }); + }); + + describe('decryptData', () => { + it('should return undefined if eligibleForDecryption is not true', () => { + vi.spyOn(OAuthClient.prototype, 'eligibleForDecryption').mockReturnValue( + false + ); + + const oauthClient = new OAuthClient(); + + expect(oauthClient.decryptData()).toBeUndefined(); + }); + + it('should decrypt authDefaults and set it to formattedAuthDefaults', async () => { + vi.spyOn(OAuthClient.prototype, 'eligibleForDecryption').mockReturnValue( + true + ); + + const formattedAuthDefaults = { + key: 'value', + }; + + const authDefaults = AES.encrypt( + JSON.stringify(formattedAuthDefaults), + appConfig.encryptionKey + ).toString(); + + const oauthClient = new OAuthClient(); + oauthClient.authDefaults = authDefaults; + oauthClient.decryptData(); + + expect(oauthClient.formattedAuthDefaults).toStrictEqual( + formattedAuthDefaults + ); + expect(oauthClient.authDefaults).not.toStrictEqual(formattedAuthDefaults); + }); + }); + + describe('eligibleForEncryption', () => { + it('should return true when formattedAuthDefaults property exists', async () => { + const oauthClient = await createOAuthClient(); + + expect(oauthClient.eligibleForEncryption()).toBe(true); + }); + + it("should return false when formattedAuthDefaults property doesn't exist", async () => { + const oauthClient = await createOAuthClient(); + + delete oauthClient.formattedAuthDefaults; + + expect(oauthClient.eligibleForEncryption()).toBe(false); + }); + }); + + describe('eligibleForDecryption', () => { + it('should return true when authDefaults property exists', async () => { + const oauthClient = await createOAuthClient(); + + expect(oauthClient.eligibleForDecryption()).toBe(true); + }); + + it("should return false when authDefaults property doesn't exist", async () => { + const oauthClient = await createOAuthClient(); + + delete oauthClient.authDefaults; + + expect(oauthClient.eligibleForDecryption()).toBe(false); + }); + }); + + it('$beforeInsert should call OAuthClient.encryptData', async () => { + const oauthClientBeforeInsertSpy = vi.spyOn( + OAuthClient.prototype, + 'encryptData' + ); + + await createOAuthClient(); + + expect(oauthClientBeforeInsertSpy).toHaveBeenCalledOnce(); + }); + + it('$beforeUpdate should call OAuthClient.encryptData', async () => { + const oauthClient = await createOAuthClient(); + + const oauthClientBeforeUpdateSpy = vi.spyOn( + OAuthClient.prototype, + 'encryptData' + ); + + await oauthClient.$query().patchAndFetch({ name: 'sample' }); + + expect(oauthClientBeforeUpdateSpy).toHaveBeenCalledOnce(); + }); + + it('$afterFind should call OAuthClient.decryptData', async () => { + const oauthClient = await createOAuthClient(); + + const oauthClientAfterFindSpy = vi.spyOn( + OAuthClient.prototype, + 'decryptData' + ); + + await oauthClient.$query(); + + expect(oauthClientAfterFindSpy).toHaveBeenCalledOnce(); + }); +}); diff --git a/packages/backend/src/models/saml-auth-providers-role-mapping.ee.js b/packages/backend/src/models/role-mapping.ee.js similarity index 74% rename from packages/backend/src/models/saml-auth-providers-role-mapping.ee.js rename to packages/backend/src/models/role-mapping.ee.js index 00e11bb0..0a5866e9 100644 --- a/packages/backend/src/models/saml-auth-providers-role-mapping.ee.js +++ b/packages/backend/src/models/role-mapping.ee.js @@ -1,8 +1,8 @@ import Base from './base.js'; import SamlAuthProvider from './saml-auth-provider.ee.js'; -class SamlAuthProvidersRoleMapping extends Base { - static tableName = 'saml_auth_providers_role_mappings'; +class RoleMapping extends Base { + static tableName = 'role_mappings'; static jsonSchema = { type: 'object', @@ -21,11 +21,11 @@ class SamlAuthProvidersRoleMapping extends Base { relation: Base.BelongsToOneRelation, modelClass: SamlAuthProvider, join: { - from: 'saml_auth_providers_role_mappings.saml_auth_provider_id', + from: 'role_mappings.saml_auth_provider_id', to: 'saml_auth_providers.id', }, }, }); } -export default SamlAuthProvidersRoleMapping; +export default RoleMapping; diff --git a/packages/backend/src/models/saml-auth-providers-role-mapping.ee.test.js b/packages/backend/src/models/role-mapping.ee.test.js similarity index 56% rename from packages/backend/src/models/saml-auth-providers-role-mapping.ee.test.js rename to packages/backend/src/models/role-mapping.ee.test.js index 2fe26e90..25400112 100644 --- a/packages/backend/src/models/saml-auth-providers-role-mapping.ee.test.js +++ b/packages/backend/src/models/role-mapping.ee.test.js @@ -1,28 +1,26 @@ import { describe, it, expect } from 'vitest'; -import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee'; +import RoleMapping from './role-mapping.ee'; import SamlAuthProvider from './saml-auth-provider.ee'; import Base from './base'; -describe('SamlAuthProvidersRoleMapping model', () => { +describe('RoleMapping model', () => { it('tableName should return correct name', () => { - expect(SamlAuthProvidersRoleMapping.tableName).toBe( - 'saml_auth_providers_role_mappings' - ); + expect(RoleMapping.tableName).toBe('role_mappings'); }); it('jsonSchema should have the correct schema', () => { - expect(SamlAuthProvidersRoleMapping.jsonSchema).toMatchSnapshot(); + expect(RoleMapping.jsonSchema).toMatchSnapshot(); }); it('relationMappings should return correct associations', () => { - const relationMappings = SamlAuthProvidersRoleMapping.relationMappings(); + const relationMappings = RoleMapping.relationMappings(); const expectedRelations = { samlAuthProvider: { relation: Base.BelongsToOneRelation, modelClass: SamlAuthProvider, join: { - from: 'saml_auth_providers_role_mappings.saml_auth_provider_id', + from: 'role_mappings.saml_auth_provider_id', to: 'saml_auth_providers.id', }, }, diff --git a/packages/backend/src/models/saml-auth-provider.ee.js b/packages/backend/src/models/saml-auth-provider.ee.js index 744da1a8..4eb588ec 100644 --- a/packages/backend/src/models/saml-auth-provider.ee.js +++ b/packages/backend/src/models/saml-auth-provider.ee.js @@ -5,7 +5,7 @@ import appConfig from '../config/app.js'; import axios from '../helpers/axios-with-proxy.js'; import Base from './base.js'; import Identity from './identity.ee.js'; -import SamlAuthProvidersRoleMapping from './saml-auth-providers-role-mapping.ee.js'; +import RoleMapping from './role-mapping.ee.js'; class SamlAuthProvider extends Base { static tableName = 'saml_auth_providers'; @@ -53,12 +53,12 @@ class SamlAuthProvider extends Base { to: 'saml_auth_providers.id', }, }, - samlAuthProvidersRoleMappings: { + roleMappings: { relation: Base.HasManyRelation, - modelClass: SamlAuthProvidersRoleMapping, + modelClass: RoleMapping, join: { from: 'saml_auth_providers.id', - to: 'saml_auth_providers_role_mappings.saml_auth_provider_id', + to: 'role_mappings.saml_auth_provider_id', }, }, }); @@ -133,27 +133,22 @@ class SamlAuthProvider extends Base { } async updateRoleMappings(roleMappings) { - return await SamlAuthProvider.transaction(async (trx) => { - await this.$relatedQuery('samlAuthProvidersRoleMappings', trx).delete(); + await this.$relatedQuery('roleMappings').delete(); - if (isEmpty(roleMappings)) { - return []; - } + if (isEmpty(roleMappings)) { + return []; + } - const samlAuthProvidersRoleMappingsData = roleMappings.map( - (samlAuthProvidersRoleMapping) => ({ - ...samlAuthProvidersRoleMapping, - samlAuthProviderId: this.id, - }) - ); + const roleMappingsData = roleMappings.map((roleMapping) => ({ + ...roleMapping, + samlAuthProviderId: this.id, + })); - const samlAuthProvidersRoleMappings = - await SamlAuthProvidersRoleMapping.query(trx).insertAndFetch( - samlAuthProvidersRoleMappingsData - ); + const newRoleMappings = await RoleMapping.query().insertAndFetch( + roleMappingsData + ); - return samlAuthProvidersRoleMappings; - }); + return newRoleMappings; } } diff --git a/packages/backend/src/models/saml-auth-provider.ee.test.js b/packages/backend/src/models/saml-auth-provider.ee.test.js index cc20db52..10a0abb0 100644 --- a/packages/backend/src/models/saml-auth-provider.ee.test.js +++ b/packages/backend/src/models/saml-auth-provider.ee.test.js @@ -1,9 +1,14 @@ -import { vi, describe, it, expect } from 'vitest'; +import { vi, beforeEach, describe, it, expect } from 'vitest'; +import { v4 as uuidv4 } from 'uuid'; import SamlAuthProvider from '../models/saml-auth-provider.ee'; -import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee'; +import RoleMapping from '../models/role-mapping.ee'; +import axios from '../helpers/axios-with-proxy.js'; import Identity from './identity.ee'; import Base from './base'; import appConfig from '../config/app'; +import { createSamlAuthProvider } from '../../test/factories/saml-auth-provider.ee.js'; +import { createRoleMapping } from '../../test/factories/role-mapping.js'; +import { createRole } from '../../test/factories/role.js'; describe('SamlAuthProvider model', () => { it('tableName should return correct name', () => { @@ -26,12 +31,12 @@ describe('SamlAuthProvider model', () => { to: 'saml_auth_providers.id', }, }, - samlAuthProvidersRoleMappings: { + roleMappings: { relation: Base.HasManyRelation, - modelClass: SamlAuthProvidersRoleMapping, + modelClass: RoleMapping, join: { from: 'saml_auth_providers.id', - to: 'saml_auth_providers_role_mappings.saml_auth_provider_id', + to: 'role_mappings.saml_auth_provider_id', }, }, }; @@ -81,4 +86,146 @@ describe('SamlAuthProvider model', () => { 'https://example.com/saml/logout' ); }); + + it('config should return the correct configuration object', () => { + const samlAuthProvider = new SamlAuthProvider(); + + samlAuthProvider.certificate = 'sample-certificate'; + samlAuthProvider.signatureAlgorithm = 'sha256'; + samlAuthProvider.entryPoint = 'https://example.com/saml'; + samlAuthProvider.issuer = 'sample-issuer'; + + vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue( + 'https://automatisch.io' + ); + + const expectedConfig = { + callbackUrl: 'https://automatisch.io/login/saml/sample-issuer/callback', + cert: 'sample-certificate', + entryPoint: 'https://example.com/saml', + issuer: 'sample-issuer', + signatureAlgorithm: 'sha256', + logoutUrl: 'https://example.com/saml', + }; + + expect(samlAuthProvider.config).toStrictEqual(expectedConfig); + }); + + it('generateLogoutRequestBody should return a correctly encoded SAML logout request', () => { + vi.mock('uuid', () => ({ + v4: vi.fn(), + })); + + const samlAuthProvider = new SamlAuthProvider(); + + samlAuthProvider.entryPoint = 'https://example.com/saml'; + samlAuthProvider.issuer = 'sample-issuer'; + + const mockUuid = '123e4567-e89b-12d3-a456-426614174000'; + uuidv4.mockReturnValue(mockUuid); + + const sessionId = 'test-session-id'; + + const logoutRequest = samlAuthProvider.generateLogoutRequestBody(sessionId); + + const expectedLogoutRequest = ` + + + sample-issuer + test-session-id + + `; + + const expectedEncodedRequest = Buffer.from(expectedLogoutRequest).toString( + 'base64' + ); + + expect(logoutRequest).toBe(expectedEncodedRequest); + }); + + it('terminateRemoteSession should send the correct POST request and return the response', async () => { + vi.mock('../helpers/axios-with-proxy.js', () => ({ + default: { + post: vi.fn(), + }, + })); + + const samlAuthProvider = new SamlAuthProvider(); + + samlAuthProvider.entryPoint = 'https://example.com/saml'; + samlAuthProvider.generateLogoutRequestBody = vi + .fn() + .mockReturnValue('mockEncodedLogoutRequest'); + + const sessionId = 'test-session-id'; + + const mockResponse = { data: 'Logout Successful' }; + axios.post.mockResolvedValue(mockResponse); + + const response = await samlAuthProvider.terminateRemoteSession(sessionId); + + expect(samlAuthProvider.generateLogoutRequestBody).toHaveBeenCalledWith( + sessionId + ); + + expect(axios.post).toHaveBeenCalledWith( + 'https://example.com/saml', + 'SAMLRequest=mockEncodedLogoutRequest', + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + } + ); + + expect(response).toBe(mockResponse); + }); + + describe('updateRoleMappings', () => { + let samlAuthProvider; + + beforeEach(async () => { + samlAuthProvider = await createSamlAuthProvider(); + }); + + it('should remove all existing role mappings', async () => { + await createRoleMapping({ + samlAuthProviderId: samlAuthProvider.id, + remoteRoleName: 'Admin', + }); + + await createRoleMapping({ + samlAuthProviderId: samlAuthProvider.id, + remoteRoleName: 'User', + }); + + await samlAuthProvider.updateRoleMappings([]); + + const roleMappings = await samlAuthProvider.$relatedQuery('roleMappings'); + expect(roleMappings).toStrictEqual([]); + }); + + it('should return the updated role mappings when new ones are provided', async () => { + const adminRole = await createRole({ name: 'Admin' }); + const userRole = await createRole({ name: 'User' }); + + const newRoleMappings = [ + { remoteRoleName: 'Admin', roleId: adminRole.id }, + { remoteRoleName: 'User', roleId: userRole.id }, + ]; + + const result = await samlAuthProvider.updateRoleMappings(newRoleMappings); + + const refetchedRoleMappings = await samlAuthProvider.$relatedQuery( + 'roleMappings' + ); + + expect(result).toStrictEqual(refetchedRoleMappings); + }); + }); }); diff --git a/packages/backend/src/models/user.js b/packages/backend/src/models/user.js index a2558dc1..99314e07 100644 --- a/packages/backend/src/models/user.js +++ b/packages/backend/src/models/user.js @@ -212,6 +212,10 @@ class User extends Base { return `${appConfig.webAppUrl}/accept-invitation?token=${this.invitationToken}`; } + get ability() { + return userAbility(this); + } + static async authenticate(email, password) { const user = await User.query().findOne({ email: email?.toLowerCase() || null, @@ -583,62 +587,6 @@ class User extends Base { return user; } - async $beforeInsert(queryContext) { - await super.$beforeInsert(queryContext); - - this.email = this.email.toLowerCase(); - await this.generateHash(); - - if (appConfig.isCloud) { - this.startTrialPeriod(); - } - } - - async $beforeUpdate(opt, queryContext) { - await super.$beforeUpdate(opt, queryContext); - - if (this.email) { - this.email = this.email.toLowerCase(); - } - - await this.generateHash(); - } - - async $afterInsert(queryContext) { - await super.$afterInsert(queryContext); - - if (appConfig.isCloud) { - await this.$relatedQuery('usageData').insert({ - userId: this.id, - consumedTaskCount: 0, - nextResetAt: DateTime.now().plus({ days: 30 }).toISODate(), - }); - } - } - - async $afterFind() { - if (await hasValidLicense()) return this; - - if (Array.isArray(this.permissions)) { - this.permissions = this.permissions.filter((permission) => { - const restrictedSubjects = [ - 'App', - 'Role', - 'SamlAuthProvider', - 'Config', - ]; - - return !restrictedSubjects.includes(permission.subject); - }); - } - - return this; - } - - get ability() { - return userAbility(this); - } - can(action, subject) { const can = this.ability.can(action, subject); @@ -654,12 +602,68 @@ class User extends Base { return conditionMap; } - cannot(action, subject) { - const cannot = this.ability.cannot(action, subject); + lowercaseEmail() { + if (this.email) { + this.email = this.email.toLowerCase(); + } + } - if (cannot) throw new NotAuthorizedError(); + async createUsageData() { + if (appConfig.isCloud) { + return await this.$relatedQuery('usageData').insertAndFetch({ + userId: this.id, + consumedTaskCount: 0, + nextResetAt: DateTime.now().plus({ days: 30 }).toISODate(), + }); + } + } - return cannot; + async omitEnterprisePermissionsWithoutValidLicense() { + if (await hasValidLicense()) { + return this; + } + + if (Array.isArray(this.permissions)) { + this.permissions = this.permissions.filter((permission) => { + const restrictedSubjects = [ + 'App', + 'Role', + 'SamlAuthProvider', + 'Config', + ]; + + return !restrictedSubjects.includes(permission.subject); + }); + } + } + + async $beforeInsert(queryContext) { + await super.$beforeInsert(queryContext); + + this.lowercaseEmail(); + await this.generateHash(); + + if (appConfig.isCloud) { + this.startTrialPeriod(); + } + } + + async $beforeUpdate(opt, queryContext) { + await super.$beforeUpdate(opt, queryContext); + + this.lowercaseEmail(); + + await this.generateHash(); + } + + async $afterInsert(queryContext) { + await super.$afterInsert(queryContext); + + await this.createUsageData(); + } + + async $afterFind() { + await this.omitEnterprisePermissionsWithoutValidLicense(); } } diff --git a/packages/backend/src/models/user.test.js b/packages/backend/src/models/user.test.js index 2112c4fd..159af7ee 100644 --- a/packages/backend/src/models/user.test.js +++ b/packages/backend/src/models/user.test.js @@ -1,6 +1,7 @@ import { describe, it, expect, vi } from 'vitest'; import { DateTime, Duration } from 'luxon'; import appConfig from '../config/app.js'; +import * as licenseModule from '../helpers/license.ee.js'; import Base from './base.js'; import AccessToken from './access-token.js'; import Config from './config.js'; @@ -20,6 +21,7 @@ import { REMOVE_AFTER_30_DAYS_OR_150_JOBS, REMOVE_AFTER_7_DAYS_OR_50_JOBS, } from '../helpers/remove-job-configuration.js'; +import * as userAbilityModule from '../helpers/user-ability.js'; import { createUser } from '../../test/factories/user.js'; import { createConnection } from '../../test/factories/connection.js'; import { createRole } from '../../test/factories/role.js'; @@ -205,64 +207,6 @@ describe('User model', () => { expect(virtualAttributes).toStrictEqual(expectedAttributes); }); - it('acceptInvitationUrl should return accept invitation page URL with invitation token', async () => { - const user = new User(); - user.invitationToken = 'invitation-token'; - - vi.spyOn(appConfig, 'webAppUrl', 'get').mockReturnValue( - 'https://automatisch.io' - ); - - expect(user.acceptInvitationUrl).toBe( - 'https://automatisch.io/accept-invitation?token=invitation-token' - ); - }); - - describe('authenticate', () => { - it('should create and return the token for correct email and password', async () => { - const user = await createUser({ - email: 'test-user@automatisch.io', - password: 'sample-password', - }); - - const token = await User.authenticate( - 'test-user@automatisch.io', - 'sample-password' - ); - - const persistedToken = await AccessToken.query().findOne({ - userId: user.id, - }); - - expect(token).toBe(persistedToken.token); - }); - - it('should return undefined for existing email and incorrect password', async () => { - await createUser({ - email: 'test-user@automatisch.io', - password: 'sample-password', - }); - - const token = await User.authenticate( - 'test-user@automatisch.io', - 'wrong-password' - ); - - expect(token).toBe(undefined); - }); - - it('should return undefined for non-existing email', async () => { - await createUser({ - email: 'test-user@automatisch.io', - password: 'sample-password', - }); - - const token = await User.authenticate('non-existing-user@automatisch.io'); - - expect(token).toBe(undefined); - }); - }); - describe('authorizedFlows', () => { it('should return user flows with isCreator condition', async () => { const userRole = await createRole({ name: 'User' }); @@ -432,7 +376,10 @@ describe('User model', () => { const anotherUserConnection = await createConnection(); expect( - await userWithRoleAndPermissions.authorizedConnections + await userWithRoleAndPermissions.authorizedConnections.orderBy( + 'created_at', + 'asc' + ) ).toStrictEqual([userConnection, anotherUserConnection]); }); @@ -505,6 +452,76 @@ describe('User model', () => { }); }); + it('acceptInvitationUrl should return accept invitation page URL with invitation token', async () => { + const user = new User(); + user.invitationToken = 'invitation-token'; + + vi.spyOn(appConfig, 'webAppUrl', 'get').mockReturnValue( + 'https://automatisch.io' + ); + + expect(user.acceptInvitationUrl).toBe( + 'https://automatisch.io/accept-invitation?token=invitation-token' + ); + }); + + it('ability should return userAbility for the user', () => { + const user = new User(); + user.fullName = 'Sample user'; + + const userAbilitySpy = vi + .spyOn(userAbilityModule, 'default') + .mockReturnValue('user-ability'); + + expect(user.ability).toStrictEqual('user-ability'); + expect(userAbilitySpy).toHaveBeenNthCalledWith(1, user); + }); + + describe('authenticate', () => { + it('should create and return the token for correct email and password', async () => { + const user = await createUser({ + email: 'test-user@automatisch.io', + password: 'sample-password', + }); + + const token = await User.authenticate( + 'test-user@automatisch.io', + 'sample-password' + ); + + const persistedToken = await AccessToken.query().findOne({ + userId: user.id, + }); + + expect(token).toBe(persistedToken.token); + }); + + it('should return undefined for existing email and incorrect password', async () => { + await createUser({ + email: 'test-user@automatisch.io', + password: 'sample-password', + }); + + const token = await User.authenticate( + 'test-user@automatisch.io', + 'wrong-password' + ); + + expect(token).toBe(undefined); + }); + + it('should return undefined for non-existing email', async () => { + await createUser({ + email: 'test-user@automatisch.io', + password: 'sample-password', + }); + + const token = await User.authenticate('non-existing-user@automatisch.io'); + + expect(token).toBe(undefined); + }); + }); + describe('login', () => { it('should return true when the given password matches with the user password', async () => { const user = await createUser({ password: 'sample-password' }); @@ -982,21 +999,9 @@ describe('User model', () => { const user = await createUser(); - const presentDate = DateTime.fromObject( - { year: 2024, month: 11, day: 17, hour: 11, minute: 30 }, - { zone: 'UTC+0' } - ); - - vi.setSystemTime(presentDate); - await user.startTrialPeriod(); - const futureDate = DateTime.fromObject( - { year: 2025, month: 1, day: 1 }, - { zone: 'UTC+0' } - ); - - vi.setSystemTime(futureDate); + vi.setSystemTime(DateTime.now().plus({ month: 1 })); const refetchedUser = await user.$query(); @@ -1104,7 +1109,9 @@ describe('User model', () => { const user = await createUser(); - expect(() => user.getPlanAndUsage()).rejects.toThrow('NotFoundError'); + await expect(() => user.getPlanAndUsage()).rejects.toThrow( + 'NotFoundError' + ); }); }); @@ -1175,7 +1182,7 @@ describe('User model', () => { }); it('should throw not found error when user role does not exist', async () => { - expect(() => + await expect(() => User.registerUser({ fullName: 'Sample user', email: 'user@automatisch.io', @@ -1184,4 +1191,342 @@ describe('User model', () => { ).rejects.toThrowError('NotFoundError'); }); }); + + describe('can', () => { + it('should return conditions for the given action and subject of the user', async () => { + const userRole = await createRole({ name: 'User' }); + + await createPermission({ + roleId: userRole.id, + subject: 'Flow', + action: 'read', + conditions: ['isCreator'], + }); + + await createPermission({ + roleId: userRole.id, + subject: 'Connection', + action: 'read', + conditions: [], + }); + + const user = await createUser({ roleId: userRole.id }); + + const userWithRoleAndPermissions = await user + .$query() + .withGraphFetched({ role: true, permissions: true }); + + expect(userWithRoleAndPermissions.can('read', 'Flow')).toStrictEqual({ + isCreator: true, + }); + + expect( + userWithRoleAndPermissions.can('read', 'Connection') + ).toStrictEqual({}); + }); + + it('should return not authorized error when the user is not permitted for the given action and subject', async () => { + const userRole = await createRole({ name: 'User' }); + const user = await createUser({ roleId: userRole.id }); + + const userWithRoleAndPermissions = await user + .$query() + .withGraphFetched({ role: true, permissions: true }); + + expect(() => userWithRoleAndPermissions.can('read', 'Flow')).toThrowError( + 'The user is not authorized!' + ); + }); + }); + + it('lowercaseEmail should lowercase the user email', () => { + const user = new User(); + user.email = 'USER@AUTOMATISCH.IO'; + + user.lowercaseEmail(); + + expect(user.email).toBe('user@automatisch.io'); + }); + + describe('createUsageData', () => { + it('should create usage data if Automatisch is a cloud installation', async () => { + vi.useFakeTimers(); + + vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true); + + const user = await createUser({ + fullName: 'Sample user', + email: 'user@automatisch.io', + }); + + vi.setSystemTime(DateTime.now().plus({ month: 1 })); + + const usageData = await user.createUsageData(); + const currentUsageData = await user.$relatedQuery('currentUsageData'); + + expect(usageData).toStrictEqual(currentUsageData); + + vi.useRealTimers(); + }); + + it('should not create usage data if Automatisch is not a cloud installation', async () => { + vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false); + + const user = await createUser({ + fullName: 'Sample user', + email: 'user@automatisch.io', + }); + + const usageData = await user.createUsageData(); + + expect(usageData).toBe(undefined); + }); + }); + + describe('omitEnterprisePermissionsWithoutValidLicense', () => { + it('should return user as-is with valid license', async () => { + const userRole = await createRole({ name: 'User' }); + const user = await createUser({ + fullName: 'Sample user', + email: 'user@automatisch.io', + roleId: userRole.id, + }); + + const readFlowPermission = await createPermission({ + roleId: userRole.id, + subject: 'Flow', + action: 'read', + conditions: [], + }); + + await createPermission({ + roleId: userRole.id, + subject: 'App', + action: 'read', + conditions: [], + }); + + await createPermission({ + roleId: userRole.id, + subject: 'Role', + action: 'read', + conditions: [], + }); + + await createPermission({ + roleId: userRole.id, + subject: 'Config', + action: 'read', + conditions: [], + }); + + await createPermission({ + roleId: userRole.id, + subject: 'SamlAuthProvider', + action: 'read', + conditions: [], + }); + + const userWithRoleAndPermissions = await user + .$query() + .withGraphFetched({ role: true, permissions: true }); + + expect(userWithRoleAndPermissions.permissions).toStrictEqual([ + readFlowPermission, + ]); + }); + + it('should omit enterprise permissions without valid license', async () => { + vi.spyOn(licenseModule, 'hasValidLicense').mockResolvedValue(false); + + const userRole = await createRole({ name: 'User' }); + const user = await createUser({ + fullName: 'Sample user', + email: 'user@automatisch.io', + roleId: userRole.id, + }); + + const readFlowPermission = await createPermission({ + roleId: userRole.id, + subject: 'Flow', + action: 'read', + conditions: [], + }); + + await createPermission({ + roleId: userRole.id, + subject: 'App', + action: 'read', + conditions: [], + }); + + await createPermission({ + roleId: userRole.id, + subject: 'Role', + action: 'read', + conditions: [], + }); + + await createPermission({ + roleId: userRole.id, + subject: 'Config', + action: 'read', + conditions: [], + }); + + await createPermission({ + roleId: userRole.id, + subject: 'SamlAuthProvider', + action: 'read', + conditions: [], + }); + + const userWithRoleAndPermissions = await user + .$query() + .withGraphFetched({ role: true, permissions: true }); + + expect(userWithRoleAndPermissions.permissions).toStrictEqual([ + readFlowPermission, + ]); + }); + }); + + describe('$beforeInsert', () => { + it('should call super.$beforeInsert', async () => { + const superBeforeInsertSpy = vi + .spyOn(User.prototype, '$beforeInsert') + .mockResolvedValue(); + + await createUser(); + + expect(superBeforeInsertSpy).toHaveBeenCalledOnce(); + }); + + it('should lowercase the user email', async () => { + const user = await createUser({ + fullName: 'Sample user', + email: 'USER@AUTOMATISCH.IO', + }); + + expect(user.email).toBe('user@automatisch.io'); + }); + + it('should generate password hash', async () => { + const user = await createUser({ + fullName: 'Sample user', + email: 'user@automatisch.io', + password: 'sample-password', + }); + + expect(user.password).not.toBe('sample-password'); + expect(await user.login('sample-password')).toBe(true); + }); + + it('should start trial period if Automatisch is a cloud installation', async () => { + vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true); + + const startTrialPeriodSpy = vi.spyOn(User.prototype, 'startTrialPeriod'); + + await createUser({ + fullName: 'Sample user', + email: 'user@automatisch.io', + }); + + expect(startTrialPeriodSpy).toHaveBeenCalledOnce(); + }); + + it('should not start trial period if Automatisch is not a cloud installation', async () => { + vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false); + + const startTrialPeriodSpy = vi.spyOn(User.prototype, 'startTrialPeriod'); + + await createUser({ + fullName: 'Sample user', + email: 'user@automatisch.io', + }); + + expect(startTrialPeriodSpy).not.toHaveBeenCalled(); + }); + }); + + describe('$beforeUpdate', () => { + it('should call super.$beforeUpdate', async () => { + const superBeforeUpdateSpy = vi + .spyOn(User.prototype, '$beforeUpdate') + .mockResolvedValue(); + + const user = await createUser({ + fullName: 'Sample user', + email: 'user@automatisch.io', + }); + + await user.$query().patch({ fullName: 'Updated user name' }); + + expect(superBeforeUpdateSpy).toHaveBeenCalledOnce(); + }); + + it('should lowercase the user email if given', async () => { + const user = await createUser({ + fullName: 'Sample user', + email: 'user@automatisch.io', + }); + + await user.$query().patchAndFetch({ email: 'NEW_EMAIL@AUTOMATISCH.IO' }); + + expect(user.email).toBe('new_email@automatisch.io'); + }); + + it('should generate password hash', async () => { + const user = await createUser({ + fullName: 'Sample user', + email: 'user@automatisch.io', + password: 'sample-password', + }); + + await user.$query().patchAndFetch({ password: 'new-password' }); + + expect(user.password).not.toBe('new-password'); + expect(await user.login('new-password')).toBe(true); + }); + }); + + describe('$afterInsert', () => { + it('should call super.$afterInsert', async () => { + const superAfterInsertSpy = vi.spyOn(User.prototype, '$afterInsert'); + + await createUser({ + fullName: 'Sample user', + email: 'user@automatisch.io', + }); + + expect(superAfterInsertSpy).toHaveBeenCalledOnce(); + }); + + it('should call createUsageData', async () => { + const createUsageDataSpy = vi.spyOn(User.prototype, 'createUsageData'); + + await createUser({ + fullName: 'Sample user', + email: 'user@automatisch.io', + }); + + expect(createUsageDataSpy).toHaveBeenCalledOnce(); + }); + }); + + it('$afterFind should invoke omitEnterprisePermissionsWithoutValidLicense method', async () => { + const omitEnterprisePermissionsWithoutValidLicenseSpy = vi.spyOn( + User.prototype, + 'omitEnterprisePermissionsWithoutValidLicense' + ); + + await createUser({ + fullName: 'Sample user', + email: 'user@automatisch.io', + }); + + expect( + omitEnterprisePermissionsWithoutValidLicenseSpy + ).toHaveBeenCalledOnce(); + }); }); diff --git a/packages/backend/src/queues/action.js b/packages/backend/src/queues/action.js index f1f4126d..dbb0226a 100644 --- a/packages/backend/src/queues/action.js +++ b/packages/backend/src/queues/action.js @@ -1,31 +1,4 @@ -import process from 'process'; -import { Queue } from 'bullmq'; -import redisConfig from '../config/redis.js'; -import logger from '../helpers/logger.js'; - -const CONNECTION_REFUSED = 'ECONNREFUSED'; - -const redisConnection = { - connection: redisConfig, -}; - -const actionQueue = new Queue('action', redisConnection); - -process.on('SIGTERM', async () => { - await actionQueue.close(); -}); - -actionQueue.on('error', (error) => { - if (error.code === CONNECTION_REFUSED) { - logger.error( - 'Make sure you have installed Redis and it is running.', - error - ); - - process.exit(); - } - - logger.error('Error happened in action queue!', error); -}); +import { generateQueue } from './queue.js'; +const actionQueue = generateQueue('action'); export default actionQueue; diff --git a/packages/backend/src/queues/delete-user.ee.js b/packages/backend/src/queues/delete-user.ee.js index b8437500..8e939523 100644 --- a/packages/backend/src/queues/delete-user.ee.js +++ b/packages/backend/src/queues/delete-user.ee.js @@ -1,31 +1,4 @@ -import process from 'process'; -import { Queue } from 'bullmq'; -import redisConfig from '../config/redis.js'; -import logger from '../helpers/logger.js'; - -const CONNECTION_REFUSED = 'ECONNREFUSED'; - -const redisConnection = { - connection: redisConfig, -}; - -const deleteUserQueue = new Queue('delete-user', redisConnection); - -process.on('SIGTERM', async () => { - await deleteUserQueue.close(); -}); - -deleteUserQueue.on('error', (error) => { - if (error.code === CONNECTION_REFUSED) { - logger.error( - 'Make sure you have installed Redis and it is running.', - error - ); - - process.exit(); - } - - logger.error('Error happened in delete user queue!', error); -}); +import { generateQueue } from './queue.js'; +const deleteUserQueue = generateQueue('delete-user'); export default deleteUserQueue; diff --git a/packages/backend/src/queues/email.js b/packages/backend/src/queues/email.js index db6eda0d..31e55bd5 100644 --- a/packages/backend/src/queues/email.js +++ b/packages/backend/src/queues/email.js @@ -1,31 +1,4 @@ -import process from 'process'; -import { Queue } from 'bullmq'; -import redisConfig from '../config/redis.js'; -import logger from '../helpers/logger.js'; - -const CONNECTION_REFUSED = 'ECONNREFUSED'; - -const redisConnection = { - connection: redisConfig, -}; - -const emailQueue = new Queue('email', redisConnection); - -process.on('SIGTERM', async () => { - await emailQueue.close(); -}); - -emailQueue.on('error', (error) => { - if (error.code === CONNECTION_REFUSED) { - logger.error( - 'Make sure you have installed Redis and it is running.', - error - ); - - process.exit(); - } - - logger.error('Error happened in email queue!', error); -}); +import { generateQueue } from './queue.js'; +const emailQueue = generateQueue('email'); export default emailQueue; diff --git a/packages/backend/src/queues/flow.js b/packages/backend/src/queues/flow.js index aa4ae713..b9d335fe 100644 --- a/packages/backend/src/queues/flow.js +++ b/packages/backend/src/queues/flow.js @@ -1,31 +1,4 @@ -import process from 'process'; -import { Queue } from 'bullmq'; -import redisConfig from '../config/redis.js'; -import logger from '../helpers/logger.js'; - -const CONNECTION_REFUSED = 'ECONNREFUSED'; - -const redisConnection = { - connection: redisConfig, -}; - -const flowQueue = new Queue('flow', redisConnection); - -process.on('SIGTERM', async () => { - await flowQueue.close(); -}); - -flowQueue.on('error', (error) => { - if (error.code === CONNECTION_REFUSED) { - logger.error( - 'Make sure you have installed Redis and it is running.', - error - ); - - process.exit(); - } - - logger.error('Error happened in flow queue!', error); -}); +import { generateQueue } from './queue.js'; +const flowQueue = generateQueue('flow'); export default flowQueue; diff --git a/packages/backend/src/queues/index.js b/packages/backend/src/queues/index.js new file mode 100644 index 00000000..fcdd4c89 --- /dev/null +++ b/packages/backend/src/queues/index.js @@ -0,0 +1,21 @@ +import appConfig from '../config/app.js'; +import actionQueue from './action.js'; +import emailQueue from './email.js'; +import flowQueue from './flow.js'; +import triggerQueue from './trigger.js'; +import deleteUserQueue from './delete-user.ee.js'; +import removeCancelledSubscriptionsQueue from './remove-cancelled-subscriptions.ee.js'; + +const queues = [ + actionQueue, + emailQueue, + flowQueue, + triggerQueue, + deleteUserQueue, +]; + +if (appConfig.isCloud) { + queues.push(removeCancelledSubscriptionsQueue); +} + +export default queues; diff --git a/packages/backend/src/queues/queue.js b/packages/backend/src/queues/queue.js new file mode 100644 index 00000000..f6a5263e --- /dev/null +++ b/packages/backend/src/queues/queue.js @@ -0,0 +1,44 @@ +import process from 'process'; +import { Queue } from 'bullmq'; +import redisConfig from '../config/redis.js'; +import logger from '../helpers/logger.js'; + +const CONNECTION_REFUSED = 'ECONNREFUSED'; + +const redisConnection = { + connection: redisConfig, +}; + +export const generateQueue = (queueName, options) => { + const queue = new Queue(queueName, redisConnection); + + queue.on('error', (error) => queueOnError(error, queueName)); + + if (options?.runDaily) addScheduler(queueName, queue); + + return queue; +}; + +const queueOnError = (error, queueName) => { + if (error.code === CONNECTION_REFUSED) { + const errorMessage = + 'Make sure you have installed Redis and it is running.'; + + logger.error(errorMessage, error); + + process.exit(); + } + + logger.error(`Error happened in ${queueName} queue!`, error); +}; + +const addScheduler = (queueName, queue) => { + const everydayAtOneOclock = '0 1 * * *'; + + queue.add(queueName, null, { + jobId: queueName, + repeat: { + pattern: everydayAtOneOclock, + }, + }); +}; diff --git a/packages/backend/src/queues/remove-cancelled-subscriptions.ee.js b/packages/backend/src/queues/remove-cancelled-subscriptions.ee.js index 1bdddebc..bb439722 100644 --- a/packages/backend/src/queues/remove-cancelled-subscriptions.ee.js +++ b/packages/backend/src/queues/remove-cancelled-subscriptions.ee.js @@ -1,44 +1,8 @@ -import process from 'process'; -import { Queue } from 'bullmq'; -import redisConfig from '../config/redis.js'; -import logger from '../helpers/logger.js'; +import { generateQueue } from './queue.js'; -const CONNECTION_REFUSED = 'ECONNREFUSED'; - -const redisConnection = { - connection: redisConfig, -}; - -const removeCancelledSubscriptionsQueue = new Queue( +const removeCancelledSubscriptionsQueue = generateQueue( 'remove-cancelled-subscriptions', - redisConnection + { runDaily: true } ); -process.on('SIGTERM', async () => { - await removeCancelledSubscriptionsQueue.close(); -}); - -removeCancelledSubscriptionsQueue.on('error', (error) => { - if (error.code === CONNECTION_REFUSED) { - logger.error( - 'Make sure you have installed Redis and it is running.', - error - ); - - process.exit(); - } - - logger.error( - 'Error happened in remove cancelled subscriptions queue!', - error - ); -}); - -removeCancelledSubscriptionsQueue.add('remove-cancelled-subscriptions', null, { - jobId: 'remove-cancelled-subscriptions', - repeat: { - pattern: '0 1 * * *', - }, -}); - export default removeCancelledSubscriptionsQueue; diff --git a/packages/backend/src/queues/trigger.js b/packages/backend/src/queues/trigger.js index 66a3d9ec..e2134e13 100644 --- a/packages/backend/src/queues/trigger.js +++ b/packages/backend/src/queues/trigger.js @@ -1,31 +1,4 @@ -import process from 'process'; -import { Queue } from 'bullmq'; -import redisConfig from '../config/redis.js'; -import logger from '../helpers/logger.js'; - -const CONNECTION_REFUSED = 'ECONNREFUSED'; - -const redisConnection = { - connection: redisConfig, -}; - -const triggerQueue = new Queue('trigger', redisConnection); - -process.on('SIGTERM', async () => { - await triggerQueue.close(); -}); - -triggerQueue.on('error', (error) => { - if (error.code === CONNECTION_REFUSED) { - logger.error( - 'Make sure you have installed Redis and it is running.', - error - ); - - process.exit(); - } - - logger.error('Error happened in trigger queue!', error); -}); +import { generateQueue } from './queue.js'; +const triggerQueue = generateQueue('trigger'); export default triggerQueue; diff --git a/packages/backend/src/routes/api/v1/admin/apps.ee.js b/packages/backend/src/routes/api/v1/admin/apps.ee.js index c476d2ff..6a0eb9a6 100644 --- a/packages/backend/src/routes/api/v1/admin/apps.ee.js +++ b/packages/backend/src/routes/api/v1/admin/apps.ee.js @@ -4,10 +4,10 @@ import { authorizeAdmin } from '../../../../helpers/authorization.js'; import { checkIsEnterprise } from '../../../../helpers/check-is-enterprise.js'; import createConfigAction from '../../../../controllers/api/v1/admin/apps/create-config.ee.js'; import updateConfigAction from '../../../../controllers/api/v1/admin/apps/update-config.ee.js'; -import getAuthClientsAction from '../../../../controllers/api/v1/admin/apps/get-auth-clients.ee.js'; -import getAuthClientAction from '../../../../controllers/api/v1/admin/apps/get-auth-client.ee.js'; -import createAuthClientAction from '../../../../controllers/api/v1/admin/apps/create-auth-client.ee.js'; -import updateAuthClientAction from '../../../../controllers/api/v1/admin/apps/update-auth-client.ee.js'; +import getOAuthClientsAction from '../../../../controllers/api/v1/admin/apps/get-oauth-clients.ee.js'; +import getOAuthClientAction from '../../../../controllers/api/v1/admin/apps/get-oauth-client.ee.js'; +import createOAuthClientAction from '../../../../controllers/api/v1/admin/apps/create-oauth-client.ee.js'; +import updateOAuthClientAction from '../../../../controllers/api/v1/admin/apps/update-oauth-client.ee.js'; const router = Router(); @@ -28,35 +28,35 @@ router.patch( ); router.get( - '/:appKey/auth-clients', + '/:appKey/oauth-clients', authenticateUser, authorizeAdmin, checkIsEnterprise, - getAuthClientsAction + getOAuthClientsAction ); router.post( - '/:appKey/auth-clients', + '/:appKey/oauth-clients', authenticateUser, authorizeAdmin, checkIsEnterprise, - createAuthClientAction + createOAuthClientAction ); router.get( - '/:appKey/auth-clients/:appAuthClientId', + '/:appKey/oauth-clients/:oauthClientId', authenticateUser, authorizeAdmin, checkIsEnterprise, - getAuthClientAction + getOAuthClientAction ); router.patch( - '/:appKey/auth-clients/:appAuthClientId', + '/:appKey/oauth-clients/:oauthClientId', authenticateUser, authorizeAdmin, checkIsEnterprise, - updateAuthClientAction + updateOAuthClientAction ); export default router; diff --git a/packages/backend/src/routes/api/v1/apps.js b/packages/backend/src/routes/api/v1/apps.js index 5bdc27f1..c92fc552 100644 --- a/packages/backend/src/routes/api/v1/apps.js +++ b/packages/backend/src/routes/api/v1/apps.js @@ -7,8 +7,8 @@ import getAppsAction from '../../../controllers/api/v1/apps/get-apps.js'; import getAuthAction from '../../../controllers/api/v1/apps/get-auth.js'; import getConnectionsAction from '../../../controllers/api/v1/apps/get-connections.js'; import getConfigAction from '../../../controllers/api/v1/apps/get-config.ee.js'; -import getAuthClientsAction from '../../../controllers/api/v1/apps/get-auth-clients.ee.js'; -import getAuthClientAction from '../../../controllers/api/v1/apps/get-auth-client.ee.js'; +import getOAuthClientsAction from '../../../controllers/api/v1/apps/get-oauth-clients.ee.js'; +import getOAuthClientAction from '../../../controllers/api/v1/apps/get-oauth-client.ee.js'; import getTriggersAction from '../../../controllers/api/v1/apps/get-triggers.js'; import getTriggerSubstepsAction from '../../../controllers/api/v1/apps/get-trigger-substeps.js'; import getActionsAction from '../../../controllers/api/v1/apps/get-actions.js'; @@ -44,17 +44,17 @@ router.get( ); router.get( - '/:appKey/auth-clients', + '/:appKey/oauth-clients', authenticateUser, checkIsEnterprise, - getAuthClientsAction + getOAuthClientsAction ); router.get( - '/:appKey/auth-clients/:appAuthClientId', + '/:appKey/oauth-clients/:oauthClientId', authenticateUser, checkIsEnterprise, - getAuthClientAction + getOAuthClientAction ); router.get('/:appKey/triggers', authenticateUser, getTriggersAction); diff --git a/packages/backend/src/serializers/app-auth-client.js b/packages/backend/src/serializers/app-auth-client.js deleted file mode 100644 index 88af3dab..00000000 --- a/packages/backend/src/serializers/app-auth-client.js +++ /dev/null @@ -1,10 +0,0 @@ -const appAuthClientSerializer = (appAuthClient) => { - return { - id: appAuthClient.id, - appConfigId: appAuthClient.appConfigId, - name: appAuthClient.name, - active: appAuthClient.active, - }; -}; - -export default appAuthClientSerializer; diff --git a/packages/backend/src/serializers/app-auth-client.test.js b/packages/backend/src/serializers/app-auth-client.test.js deleted file mode 100644 index d4ed178e..00000000 --- a/packages/backend/src/serializers/app-auth-client.test.js +++ /dev/null @@ -1,24 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { createAppAuthClient } from '../../test/factories/app-auth-client'; -import appAuthClientSerializer from './app-auth-client'; - -describe('appAuthClient serializer', () => { - let appAuthClient; - - beforeEach(async () => { - appAuthClient = await createAppAuthClient(); - }); - - it('should return app auth client data', async () => { - const expectedPayload = { - id: appAuthClient.id, - appConfigId: appAuthClient.appConfigId, - name: appAuthClient.name, - active: appAuthClient.active, - }; - - expect(appAuthClientSerializer(appAuthClient)).toStrictEqual( - expectedPayload - ); - }); -}); 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/app.js b/packages/backend/src/serializers/app.js index 57e0306d..2c8adb9f 100644 --- a/packages/backend/src/serializers/app.js +++ b/packages/backend/src/serializers/app.js @@ -6,6 +6,7 @@ const appSerializer = (app) => { primaryColor: app.primaryColor, authDocUrl: app.authDocUrl, supportsConnections: app.supportsConnections, + supportsOauthClients: app?.auth?.generateAuthUrl ? true : false, }; if (app.connectionCount) { diff --git a/packages/backend/src/serializers/app.test.js b/packages/backend/src/serializers/app.test.js index ec5716a9..513792e7 100644 --- a/packages/backend/src/serializers/app.test.js +++ b/packages/backend/src/serializers/app.test.js @@ -12,6 +12,7 @@ describe('appSerializer', () => { iconUrl: app.iconUrl, authDocUrl: app.authDocUrl, supportsConnections: app.supportsConnections, + supportsOauthClients: app.auth.generateAuthUrl ? true : false, primaryColor: app.primaryColor, }; 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..70224476 100644 --- a/packages/backend/src/serializers/connection.js +++ b/packages/backend/src/serializers/connection.js @@ -2,8 +2,7 @@ const connectionSerializer = (connection) => { return { id: connection.id, key: connection.key, - reconnectable: connection.reconnectable, - appAuthClientId: connection.appAuthClientId, + oauthClientId: connection.oauthClientId, 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..bb9db58a 100644 --- a/packages/backend/src/serializers/connection.test.js +++ b/packages/backend/src/serializers/connection.test.js @@ -13,8 +13,7 @@ describe('connectionSerializer', () => { const expectedPayload = { id: connection.id, key: connection.key, - reconnectable: connection.reconnectable, - appAuthClientId: connection.appAuthClientId, + oauthClientId: connection.oauthClientId, formattedData: { screenName: connection.formattedData.screenName, }, diff --git a/packages/backend/src/serializers/index.js b/packages/backend/src/serializers/index.js index 9a38e5e9..4525b5ae 100644 --- a/packages/backend/src/serializers/index.js +++ b/packages/backend/src/serializers/index.js @@ -4,12 +4,13 @@ import permissionSerializer from './permission.js'; import adminSamlAuthProviderSerializer from './admin-saml-auth-provider.ee.js'; import samlAuthProviderSerializer from './saml-auth-provider.ee.js'; import samlAuthProviderRoleMappingSerializer from './role-mapping.ee.js'; -import appAuthClientSerializer from './app-auth-client.js'; +import oauthClientSerializer from './oauth-client.js'; import appConfigSerializer from './app-config.js'; import flowSerializer from './flow.js'; import stepSerializer from './step.js'; import connectionSerializer from './connection.js'; import appSerializer from './app.js'; +import userAppSerializer from './user-app.js'; import authSerializer from './auth.js'; import triggerSerializer from './trigger.js'; import actionSerializer from './action.js'; @@ -26,13 +27,14 @@ const serializers = { Permission: permissionSerializer, AdminSamlAuthProvider: adminSamlAuthProviderSerializer, SamlAuthProvider: samlAuthProviderSerializer, - SamlAuthProvidersRoleMapping: samlAuthProviderRoleMappingSerializer, - AppAuthClient: appAuthClientSerializer, + RoleMapping: samlAuthProviderRoleMappingSerializer, + OAuthClient: oauthClientSerializer, AppConfig: appConfigSerializer, Flow: flowSerializer, Step: stepSerializer, Connection: connectionSerializer, App: appSerializer, + UserApp: userAppSerializer, Auth: authSerializer, Trigger: triggerSerializer, Action: actionSerializer, diff --git a/packages/backend/src/serializers/oauth-client.js b/packages/backend/src/serializers/oauth-client.js new file mode 100644 index 00000000..bacebafc --- /dev/null +++ b/packages/backend/src/serializers/oauth-client.js @@ -0,0 +1,10 @@ +const oauthClientSerializer = (oauthClient) => { + return { + id: oauthClient.id, + appConfigId: oauthClient.appConfigId, + name: oauthClient.name, + active: oauthClient.active, + }; +}; + +export default oauthClientSerializer; diff --git a/packages/backend/src/serializers/oauth-client.test.js b/packages/backend/src/serializers/oauth-client.test.js new file mode 100644 index 00000000..d5ab8d70 --- /dev/null +++ b/packages/backend/src/serializers/oauth-client.test.js @@ -0,0 +1,22 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { createOAuthClient } from '../../test/factories/oauth-client'; +import oauthClientSerializer from './oauth-client'; + +describe('oauthClient serializer', () => { + let oauthClient; + + beforeEach(async () => { + oauthClient = await createOAuthClient(); + }); + + it('should return oauth client data', async () => { + const expectedPayload = { + id: oauthClient.id, + appConfigId: oauthClient.appConfigId, + name: oauthClient.name, + active: oauthClient.active, + }; + + expect(oauthClientSerializer(oauthClient)).toStrictEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/serializers/user-app.js b/packages/backend/src/serializers/user-app.js new file mode 100644 index 00000000..0d16865b --- /dev/null +++ b/packages/backend/src/serializers/user-app.js @@ -0,0 +1,22 @@ +const userAppSerializer = (userApp) => { + let appData = { + key: userApp.key, + name: userApp.name, + iconUrl: userApp.iconUrl, + primaryColor: userApp.primaryColor, + authDocUrl: userApp.authDocUrl, + supportsConnections: userApp.supportsConnections, + }; + + if (userApp.connectionCount) { + appData.connectionCount = userApp.connectionCount; + } + + if (userApp.flowCount) { + appData.flowCount = userApp.flowCount; + } + + return appData; +}; + +export default userAppSerializer; diff --git a/packages/backend/src/worker.js b/packages/backend/src/worker.js index 988cb0cc..214e343d 100644 --- a/packages/backend/src/worker.js +++ b/packages/backend/src/worker.js @@ -1,20 +1,22 @@ import * as Sentry from './helpers/sentry.ee.js'; -import appConfig from './config/app.js'; +import process from 'node:process'; Sentry.init(); import './config/orm.js'; import './helpers/check-worker-readiness.js'; -import './workers/flow.js'; -import './workers/trigger.js'; -import './workers/action.js'; -import './workers/email.js'; -import './workers/delete-user.ee.js'; +import queues from './queues/index.js'; +import workers from './workers/index.js'; -if (appConfig.isCloud) { - import('./workers/remove-cancelled-subscriptions.ee.js'); - import('./queues/remove-cancelled-subscriptions.ee.js'); -} +process.on('SIGTERM', async () => { + for (const queue of queues) { + await queue.close(); + } + + for (const worker of workers) { + await worker.close(); + } +}); import telemetry from './helpers/telemetry/index.js'; diff --git a/packages/backend/src/workers/action.js b/packages/backend/src/workers/action.js index 9564d9a4..b0728059 100644 --- a/packages/backend/src/workers/action.js +++ b/packages/backend/src/workers/action.js @@ -1,79 +1,6 @@ -import { Worker } from 'bullmq'; -import process from 'node:process'; +import { generateWorker } from './worker.js'; +import { executeActionJob } from '../jobs/execute-action.js'; -import * as Sentry from '../helpers/sentry.ee.js'; -import redisConfig from '../config/redis.js'; -import logger from '../helpers/logger.js'; -import Step from '../models/step.js'; -import actionQueue from '../queues/action.js'; -import { processAction } from '../services/action.js'; -import { - REMOVE_AFTER_30_DAYS_OR_150_JOBS, - REMOVE_AFTER_7_DAYS_OR_50_JOBS, -} from '../helpers/remove-job-configuration.js'; -import delayAsMilliseconds from '../helpers/delay-as-milliseconds.js'; +const actionWorker = generateWorker('action', executeActionJob); -const DEFAULT_DELAY_DURATION = 0; - -export const worker = new Worker( - 'action', - async (job) => { - const { stepId, flowId, executionId, computedParameters, executionStep } = - await processAction(job.data); - - if (executionStep.isFailed) return; - - const step = await Step.query().findById(stepId).throwIfNotFound(); - const nextStep = await step.getNextStep(); - - if (!nextStep) return; - - const jobName = `${executionId}-${nextStep.id}`; - - const jobPayload = { - flowId, - executionId, - stepId: nextStep.id, - }; - - const jobOptions = { - removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS, - removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS, - delay: DEFAULT_DELAY_DURATION, - }; - - if (step.appKey === 'delay') { - jobOptions.delay = delayAsMilliseconds(step.key, computedParameters); - } - - if (step.appKey === 'filter' && !executionStep.dataOut) { - return; - } - - await actionQueue.add(jobName, jobPayload, jobOptions); - }, - { connection: redisConfig } -); - -worker.on('completed', (job) => { - logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`); -}); - -worker.on('failed', (job, err) => { - const errorMessage = ` - JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message} - \n ${err.stack} - `; - - logger.error(errorMessage); - - Sentry.captureException(err, { - extra: { - jobId: job.id, - }, - }); -}); - -process.on('SIGTERM', async () => { - await worker.close(); -}); +export default actionWorker; diff --git a/packages/backend/src/workers/delete-user.ee.js b/packages/backend/src/workers/delete-user.ee.js index 64a4bfa4..c47093b9 100644 --- a/packages/backend/src/workers/delete-user.ee.js +++ b/packages/backend/src/workers/delete-user.ee.js @@ -1,72 +1,6 @@ -import { Worker } from 'bullmq'; -import process from 'node:process'; +import { generateWorker } from './worker.js'; +import { deleteUserJob } from '../jobs/delete-user.ee.js'; -import * as Sentry from '../helpers/sentry.ee.js'; -import redisConfig from '../config/redis.js'; -import logger from '../helpers/logger.js'; -import appConfig from '../config/app.js'; -import User from '../models/user.js'; -import ExecutionStep from '../models/execution-step.js'; +const deleteUserWorker = generateWorker('delete-user', deleteUserJob); -export const worker = new Worker( - 'delete-user', - async (job) => { - const { id } = job.data; - - const user = await User.query() - .withSoftDeleted() - .findById(id) - .throwIfNotFound(); - - const executionIds = ( - await user - .$relatedQuery('executions') - .withSoftDeleted() - .select('executions.id') - ).map((execution) => execution.id); - - await ExecutionStep.query() - .withSoftDeleted() - .whereIn('execution_id', executionIds) - .hardDelete(); - await user.$relatedQuery('executions').withSoftDeleted().hardDelete(); - await user.$relatedQuery('steps').withSoftDeleted().hardDelete(); - await user.$relatedQuery('flows').withSoftDeleted().hardDelete(); - await user.$relatedQuery('connections').withSoftDeleted().hardDelete(); - await user.$relatedQuery('identities').withSoftDeleted().hardDelete(); - - if (appConfig.isCloud) { - await user.$relatedQuery('subscriptions').withSoftDeleted().hardDelete(); - await user.$relatedQuery('usageData').withSoftDeleted().hardDelete(); - } - - await user.$relatedQuery('accessTokens').withSoftDeleted().hardDelete(); - await user.$query().withSoftDeleted().hardDelete(); - }, - { connection: redisConfig } -); - -worker.on('completed', (job) => { - logger.info( - `JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has been deleted!` - ); -}); - -worker.on('failed', (job, err) => { - const errorMessage = ` - JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has failed to be deleted! ${err.message} - \n ${err.stack} - `; - - logger.error(errorMessage); - - Sentry.captureException(err, { - extra: { - jobId: job.id, - }, - }); -}); - -process.on('SIGTERM', async () => { - await worker.close(); -}); +export default deleteUserWorker; diff --git a/packages/backend/src/workers/email.js b/packages/backend/src/workers/email.js index 446d8309..4cd2c1bb 100644 --- a/packages/backend/src/workers/email.js +++ b/packages/backend/src/workers/email.js @@ -1,65 +1,6 @@ -import { Worker } from 'bullmq'; -import process from 'node:process'; +import { generateWorker } from './worker.js'; +import { sendEmailJob } from '../jobs/send-email.js'; -import * as Sentry from '../helpers/sentry.ee.js'; -import redisConfig from '../config/redis.js'; -import logger from '../helpers/logger.js'; -import mailer from '../helpers/mailer.ee.js'; -import compileEmail from '../helpers/compile-email.ee.js'; -import appConfig from '../config/app.js'; +const emailWorker = generateWorker('email', sendEmailJob); -const isCloudSandbox = () => { - return appConfig.isCloud && !appConfig.isProd; -}; - -const isAutomatischEmail = (email) => { - return email.endsWith('@automatisch.io'); -}; - -export const worker = new Worker( - 'email', - async (job) => { - const { email, subject, template, params } = job.data; - - if (isCloudSandbox() && !isAutomatischEmail(email)) { - logger.info( - 'Only Automatisch emails are allowed for non-production environments!' - ); - - return; - } - - await mailer.sendMail({ - to: email, - from: appConfig.fromEmail, - subject: subject, - html: compileEmail(template, params), - }); - }, - { connection: redisConfig } -); - -worker.on('completed', (job) => { - logger.info( - `JOB ID: ${job.id} - ${job.data.subject} email sent to ${job.data.email}!` - ); -}); - -worker.on('failed', (job, err) => { - const errorMessage = ` - JOB ID: ${job.id} - ${job.data.subject} email to ${job.data.email} has failed to send with ${err.message} - \n ${err.stack} - `; - - logger.error(errorMessage); - - Sentry.captureException(err, { - extra: { - jobId: job.id, - }, - }); -}); - -process.on('SIGTERM', async () => { - await worker.close(); -}); +export default emailWorker; diff --git a/packages/backend/src/workers/flow.js b/packages/backend/src/workers/flow.js index b1bcce78..7b04bef2 100644 --- a/packages/backend/src/workers/flow.js +++ b/packages/backend/src/workers/flow.js @@ -1,100 +1,6 @@ -import { Worker } from 'bullmq'; -import process from 'node:process'; +import { generateWorker } from './worker.js'; +import { executeFlowJob } from '../jobs/execute-flow.js'; -import * as Sentry from '../helpers/sentry.ee.js'; -import redisConfig from '../config/redis.js'; -import logger from '../helpers/logger.js'; -import flowQueue from '../queues/flow.js'; -import triggerQueue from '../queues/trigger.js'; -import { processFlow } from '../services/flow.js'; -import Flow from '../models/flow.js'; -import { - REMOVE_AFTER_30_DAYS_OR_150_JOBS, - REMOVE_AFTER_7_DAYS_OR_50_JOBS, -} from '../helpers/remove-job-configuration.js'; +const flowWorker = generateWorker('flow', executeFlowJob); -export const worker = new Worker( - 'flow', - async (job) => { - const { flowId } = job.data; - - const flow = await Flow.query().findById(flowId).throwIfNotFound(); - const user = await flow.$relatedQuery('user'); - const allowedToRunFlows = await user.isAllowedToRunFlows(); - - if (!allowedToRunFlows) { - return; - } - - const triggerStep = await flow.getTriggerStep(); - - const { data, error } = await processFlow({ flowId }); - - const reversedData = data.reverse(); - - const jobOptions = { - removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS, - removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS, - }; - - for (const triggerItem of reversedData) { - const jobName = `${triggerStep.id}-${triggerItem.meta.internalId}`; - - const jobPayload = { - flowId, - stepId: triggerStep.id, - triggerItem, - }; - - await triggerQueue.add(jobName, jobPayload, jobOptions); - } - - if (error) { - const jobName = `${triggerStep.id}-error`; - - const jobPayload = { - flowId, - stepId: triggerStep.id, - error, - }; - - await triggerQueue.add(jobName, jobPayload, jobOptions); - } - }, - { connection: redisConfig } -); - -worker.on('completed', (job) => { - logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`); -}); - -worker.on('failed', async (job, err) => { - const errorMessage = ` - JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message} - \n ${err.stack} - `; - - logger.error(errorMessage); - - const flow = await Flow.query().findById(job.data.flowId); - - if (!flow) { - await flowQueue.removeRepeatableByKey(job.repeatJobKey); - - const flowNotFoundErrorMessage = ` - JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has been deleted from Redis because flow was not found! - `; - - logger.error(flowNotFoundErrorMessage); - } - - Sentry.captureException(err, { - extra: { - jobId: job.id, - }, - }); -}); - -process.on('SIGTERM', async () => { - await worker.close(); -}); +export default flowWorker; diff --git a/packages/backend/src/workers/index.js b/packages/backend/src/workers/index.js new file mode 100644 index 00000000..81446f2a --- /dev/null +++ b/packages/backend/src/workers/index.js @@ -0,0 +1,21 @@ +import appConfig from '../config/app.js'; +import actionWorker from './action.js'; +import emailWorker from './email.js'; +import flowWorker from './flow.js'; +import triggerWorker from './trigger.js'; +import deleteUserWorker from './delete-user.ee.js'; +import removeCancelledSubscriptionsWorker from './remove-cancelled-subscriptions.ee.js'; + +const workers = [ + actionWorker, + emailWorker, + flowWorker, + triggerWorker, + deleteUserWorker, +]; + +if (appConfig.isCloud) { + workers.push(removeCancelledSubscriptionsWorker); +} + +export default workers; diff --git a/packages/backend/src/workers/remove-cancelled-subscriptions.ee.js b/packages/backend/src/workers/remove-cancelled-subscriptions.ee.js index 7b3952bf..83df0865 100644 --- a/packages/backend/src/workers/remove-cancelled-subscriptions.ee.js +++ b/packages/backend/src/workers/remove-cancelled-subscriptions.ee.js @@ -1,47 +1,9 @@ -import { Worker } from 'bullmq'; -import process from 'node:process'; -import { DateTime } from 'luxon'; -import * as Sentry from '../helpers/sentry.ee.js'; -import redisConfig from '../config/redis.js'; -import logger from '../helpers/logger.js'; -import Subscription from '../models/subscription.ee.js'; +import { generateWorker } from './worker.js'; +import { removeCancelledSubscriptionsJob } from '../jobs/remove-cancelled-subscriptions.ee.js'; -export const worker = new Worker( +const removeCancelledSubscriptionsWorker = generateWorker( 'remove-cancelled-subscriptions', - async () => { - await Subscription.query() - .delete() - .where({ - status: 'deleted', - }) - .andWhere( - 'cancellation_effective_date', - '<=', - DateTime.now().startOf('day').toISODate() - ); - }, - { connection: redisConfig } + removeCancelledSubscriptionsJob ); -worker.on('completed', (job) => { - logger.info( - `JOB ID: ${job.id} - The cancelled subscriptions have been removed!` - ); -}); - -worker.on('failed', (job, err) => { - const errorMessage = ` - JOB ID: ${job.id} - ERROR: The cancelled subscriptions can not be removed! ${err.message} - \n ${err.stack} - `; - logger.error(errorMessage); - Sentry.captureException(err, { - extra: { - jobId: job.id, - }, - }); -}); - -process.on('SIGTERM', async () => { - await worker.close(); -}); +export default removeCancelledSubscriptionsWorker; diff --git a/packages/backend/src/workers/trigger.js b/packages/backend/src/workers/trigger.js index 58749f75..e25915fc 100644 --- a/packages/backend/src/workers/trigger.js +++ b/packages/backend/src/workers/trigger.js @@ -1,65 +1,6 @@ -import { Worker } from 'bullmq'; -import process from 'node:process'; +import { generateWorker } from './worker.js'; +import { executeTriggerJob } from '../jobs/execute-trigger.js'; -import * as Sentry from '../helpers/sentry.ee.js'; -import redisConfig from '../config/redis.js'; -import logger from '../helpers/logger.js'; -import actionQueue from '../queues/action.js'; -import Step from '../models/step.js'; -import { processTrigger } from '../services/trigger.js'; -import { - REMOVE_AFTER_30_DAYS_OR_150_JOBS, - REMOVE_AFTER_7_DAYS_OR_50_JOBS, -} from '../helpers/remove-job-configuration.js'; +const triggerWorker = generateWorker('flow', executeTriggerJob); -export const worker = new Worker( - 'trigger', - async (job) => { - const { flowId, executionId, stepId, executionStep } = await processTrigger( - job.data - ); - - if (executionStep.isFailed) return; - - const step = await Step.query().findById(stepId).throwIfNotFound(); - const nextStep = await step.getNextStep(); - const jobName = `${executionId}-${nextStep.id}`; - - const jobPayload = { - flowId, - executionId, - stepId: nextStep.id, - }; - - const jobOptions = { - removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS, - removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS, - }; - - await actionQueue.add(jobName, jobPayload, jobOptions); - }, - { connection: redisConfig } -); - -worker.on('completed', (job) => { - logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`); -}); - -worker.on('failed', (job, err) => { - const errorMessage = ` - JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message} - \n ${err.stack} - `; - - logger.error(errorMessage); - - Sentry.captureException(err, { - extra: { - jobId: job.id, - }, - }); -}); - -process.on('SIGTERM', async () => { - await worker.close(); -}); +export default triggerWorker; diff --git a/packages/backend/src/workers/worker.js b/packages/backend/src/workers/worker.js new file mode 100644 index 00000000..5528a24a --- /dev/null +++ b/packages/backend/src/workers/worker.js @@ -0,0 +1,28 @@ +import { Worker } from 'bullmq'; + +import * as Sentry from '../helpers/sentry.ee.js'; +import redisConfig from '../config/redis.js'; +import logger from '../helpers/logger.js'; + +export const generateWorker = (workerName, job) => { + const worker = new Worker(workerName, job, { connection: redisConfig }); + + worker.on('completed', (job) => { + logger.info(`JOB ID: ${job.id} - has been successfully completed!`); + }); + + worker.on('failed', (job, err) => { + logger.error(` + JOB ID: ${job.id} - has failed to be completed! ${err.message} + \n ${err.stack} + `); + + Sentry.captureException(err, { + extra: { + jobId: job.id, + }, + }); + }); + + return worker; +}; diff --git a/packages/backend/test/factories/app-auth-client.js b/packages/backend/test/factories/oauth-client.js similarity index 67% rename from packages/backend/test/factories/app-auth-client.js rename to packages/backend/test/factories/oauth-client.js index 831d4c14..0b0f6b9b 100644 --- a/packages/backend/test/factories/app-auth-client.js +++ b/packages/backend/test/factories/oauth-client.js @@ -1,5 +1,5 @@ import { faker } from '@faker-js/faker'; -import AppAuthClient from '../../src/models/app-auth-client'; +import OAuthClient from '../../src/models/oauth-client'; const formattedAuthDefaults = { oAuthRedirectUrl: faker.internet.url(), @@ -8,14 +8,14 @@ const formattedAuthDefaults = { clientSecret: faker.string.uuid(), }; -export const createAppAuthClient = async (params = {}) => { +export const createOAuthClient = async (params = {}) => { params.name = params?.name || faker.person.fullName(); params.appKey = params?.appKey || 'deepl'; params.active = params?.active ?? true; params.formattedAuthDefaults = params?.formattedAuthDefaults || formattedAuthDefaults; - const appAuthClient = await AppAuthClient.query().insertAndFetch(params); + const oauthClient = await OAuthClient.query().insertAndFetch(params); - return appAuthClient; + return oauthClient; }; diff --git a/packages/backend/test/factories/role-mapping.js b/packages/backend/test/factories/role-mapping.js index e9d37fcc..19352dd0 100644 --- a/packages/backend/test/factories/role-mapping.js +++ b/packages/backend/test/factories/role-mapping.js @@ -1,16 +1,15 @@ +import { faker } from '@faker-js/faker'; import { createRole } from './role.js'; +import RoleMapping from '../../src/models/role-mapping.ee.js'; import { createSamlAuthProvider } from './saml-auth-provider.ee.js'; -import SamlAuthProviderRoleMapping from '../../src/models/saml-auth-providers-role-mapping.ee.js'; export const createRoleMapping = async (params = {}) => { - params.roleId = params?.roleId || (await createRole()).id; + params.roleId = params.roleId || (await createRole()).id; params.samlAuthProviderId = - params?.samlAuthProviderId || (await createSamlAuthProvider()).id; + params.samlAuthProviderId || (await createSamlAuthProvider()).id; + params.remoteRoleName = params.remoteRoleName || faker.person.jobType(); - params.remoteRoleName = params?.remoteRoleName || 'User'; + const roleMapping = await RoleMapping.query().insertAndFetch(params); - const samlAuthProviderRoleMapping = - await SamlAuthProviderRoleMapping.query().insertAndFetch(params); - - return samlAuthProviderRoleMapping; + return roleMapping; }; diff --git a/packages/backend/test/factories/saml-auth-providers-role-mapping.js b/packages/backend/test/factories/saml-auth-providers-role-mapping.js deleted file mode 100644 index 72b23c06..00000000 --- a/packages/backend/test/factories/saml-auth-providers-role-mapping.js +++ /dev/null @@ -1,16 +0,0 @@ -import { faker } from '@faker-js/faker'; -import { createRole } from './role.js'; -import SamlAuthProvidersRoleMapping from '../../src/models/saml-auth-providers-role-mapping.ee.js'; -import { createSamlAuthProvider } from './saml-auth-provider.ee.js'; - -export const createSamlAuthProvidersRoleMapping = async (params = {}) => { - params.roleId = params.roleId || (await createRole()).id; - params.samlAuthProviderId = - params.samlAuthProviderId || (await createSamlAuthProvider()).id; - params.remoteRoleName = params.remoteRoleName || faker.person.jobType(); - - const samlAuthProvider = - await SamlAuthProvidersRoleMapping.query().insertAndFetch(params); - - return samlAuthProvider; -}; diff --git a/packages/backend/test/mocks/rest/api/v1/admin/apps/create-auth-client.js b/packages/backend/test/mocks/rest/api/v1/admin/apps/create-auth-client.js deleted file mode 100644 index f91c8500..00000000 --- a/packages/backend/test/mocks/rest/api/v1/admin/apps/create-auth-client.js +++ /dev/null @@ -1,17 +0,0 @@ -const createAppAuthClientMock = (appAuthClient) => { - return { - data: { - name: appAuthClient.name, - active: appAuthClient.active, - }, - meta: { - count: 1, - currentPage: null, - isArray: false, - totalPages: null, - type: 'AppAuthClient', - }, - }; -}; - -export default createAppAuthClientMock; 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/admin/apps/create-oauth-client.js b/packages/backend/test/mocks/rest/api/v1/admin/apps/create-oauth-client.js new file mode 100644 index 00000000..10e4e9b7 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/admin/apps/create-oauth-client.js @@ -0,0 +1,17 @@ +const createOAuthClientMock = (oauthClient) => { + return { + data: { + name: oauthClient.name, + active: oauthClient.active, + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'OAuthClient', + }, + }; +}; + +export default createOAuthClientMock; diff --git a/packages/backend/test/mocks/rest/api/v1/admin/apps/get-auth-client.js b/packages/backend/test/mocks/rest/api/v1/admin/apps/get-auth-client.js deleted file mode 100644 index 4d437eca..00000000 --- a/packages/backend/test/mocks/rest/api/v1/admin/apps/get-auth-client.js +++ /dev/null @@ -1,18 +0,0 @@ -const getAppAuthClientMock = (appAuthClient) => { - return { - data: { - name: appAuthClient.name, - id: appAuthClient.id, - active: appAuthClient.active, - }, - meta: { - count: 1, - currentPage: null, - isArray: false, - totalPages: null, - type: 'AppAuthClient', - }, - }; -}; - -export default getAppAuthClientMock; diff --git a/packages/backend/test/mocks/rest/api/v1/admin/apps/get-auth-clients.js b/packages/backend/test/mocks/rest/api/v1/admin/apps/get-auth-clients.js deleted file mode 100644 index dd0cc5ee..00000000 --- a/packages/backend/test/mocks/rest/api/v1/admin/apps/get-auth-clients.js +++ /dev/null @@ -1,18 +0,0 @@ -const getAdminAppAuthClientsMock = (appAuthClients) => { - return { - data: appAuthClients.map((appAuthClient) => ({ - name: appAuthClient.name, - id: appAuthClient.id, - active: appAuthClient.active, - })), - meta: { - count: appAuthClients.length, - currentPage: null, - isArray: true, - totalPages: null, - type: 'AppAuthClient', - }, - }; -}; - -export default getAdminAppAuthClientsMock; diff --git a/packages/backend/test/mocks/rest/api/v1/admin/apps/get-oauth-client.js b/packages/backend/test/mocks/rest/api/v1/admin/apps/get-oauth-client.js new file mode 100644 index 00000000..1431b968 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/admin/apps/get-oauth-client.js @@ -0,0 +1,18 @@ +const getOAuthClientMock = (oauthClient) => { + return { + data: { + name: oauthClient.name, + id: oauthClient.id, + active: oauthClient.active, + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'OAuthClient', + }, + }; +}; + +export default getOAuthClientMock; diff --git a/packages/backend/test/mocks/rest/api/v1/admin/apps/get-oauth-clients.js b/packages/backend/test/mocks/rest/api/v1/admin/apps/get-oauth-clients.js new file mode 100644 index 00000000..c0bd5d54 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/admin/apps/get-oauth-clients.js @@ -0,0 +1,18 @@ +const getAdminOAuthClientsMock = (oauthClients) => { + return { + data: oauthClients.map((oauthClient) => ({ + name: oauthClient.name, + id: oauthClient.id, + active: oauthClient.active, + })), + meta: { + count: oauthClients.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'OAuthClient', + }, + }; +}; + +export default getAdminOAuthClientsMock; diff --git a/packages/backend/test/mocks/rest/api/v1/admin/apps/update-auth-client.js b/packages/backend/test/mocks/rest/api/v1/admin/apps/update-auth-client.js deleted file mode 100644 index 9d4dea24..00000000 --- a/packages/backend/test/mocks/rest/api/v1/admin/apps/update-auth-client.js +++ /dev/null @@ -1,18 +0,0 @@ -const updateAppAuthClientMock = (appAuthClient) => { - return { - data: { - id: appAuthClient.id, - name: appAuthClient.name, - active: appAuthClient.active, - }, - meta: { - count: 1, - currentPage: null, - isArray: false, - totalPages: null, - type: 'AppAuthClient', - }, - }; -}; - -export default updateAppAuthClientMock; diff --git a/packages/backend/test/mocks/rest/api/v1/admin/apps/update-oauth-client.js b/packages/backend/test/mocks/rest/api/v1/admin/apps/update-oauth-client.js new file mode 100644 index 00000000..bdb5294d --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/admin/apps/update-oauth-client.js @@ -0,0 +1,18 @@ +const updateOAuthClientMock = (oauthClient) => { + return { + data: { + id: oauthClient.id, + name: oauthClient.name, + active: oauthClient.active, + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'OAuthClient', + }, + }; +}; + +export default updateOAuthClientMock; diff --git a/packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/get-role-mappings.ee.js b/packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/get-role-mappings.ee.js index 66fdf08a..dcd8304e 100644 --- a/packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/get-role-mappings.ee.js +++ b/packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/get-role-mappings.ee.js @@ -15,7 +15,7 @@ const getRoleMappingsMock = async (roleMappings) => { currentPage: null, isArray: true, totalPages: null, - type: 'SamlAuthProvidersRoleMapping', + type: 'RoleMapping', }, }; }; diff --git a/packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js b/packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js index 2ff8ca16..e921150f 100644 --- a/packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js +++ b/packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js @@ -15,7 +15,7 @@ const createRoleMappingsMock = async (roleMappings) => { currentPage: null, isArray: true, totalPages: null, - type: 'SamlAuthProvidersRoleMapping', + type: 'RoleMapping', }, }; }; 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..ccbeba23 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,8 +2,7 @@ const createConnection = (connection) => { const connectionData = { id: connection.id, key: connection.key, - reconnectable: connection.reconnectable || true, - appAuthClientId: connection.appAuthClientId, + oauthClientId: connection.oauthClientId, formattedData: connection.formattedData, verified: connection.verified || false, createdAt: connection.createdAt.getTime(), diff --git a/packages/backend/test/mocks/rest/api/v1/apps/get-app.js b/packages/backend/test/mocks/rest/api/v1/apps/get-app.js index e5b96c38..25061912 100644 --- a/packages/backend/test/mocks/rest/api/v1/apps/get-app.js +++ b/packages/backend/test/mocks/rest/api/v1/apps/get-app.js @@ -7,6 +7,7 @@ const getAppMock = (app) => { name: app.name, primaryColor: app.primaryColor, supportsConnections: app.supportsConnections, + supportsOauthClients: app.auth.generateAuthUrl ? true : false, }, meta: { count: 1, diff --git a/packages/backend/test/mocks/rest/api/v1/apps/get-apps.js b/packages/backend/test/mocks/rest/api/v1/apps/get-apps.js index a097d1f2..e1892d42 100644 --- a/packages/backend/test/mocks/rest/api/v1/apps/get-apps.js +++ b/packages/backend/test/mocks/rest/api/v1/apps/get-apps.js @@ -6,6 +6,7 @@ const getAppsMock = (apps) => { name: app.name, primaryColor: app.primaryColor, supportsConnections: app.supportsConnections, + supportsOauthClients: app?.auth?.generateAuthUrl ? true : false, })); return { diff --git a/packages/backend/test/mocks/rest/api/v1/apps/get-auth-client.js b/packages/backend/test/mocks/rest/api/v1/apps/get-auth-client.js deleted file mode 100644 index 4d437eca..00000000 --- a/packages/backend/test/mocks/rest/api/v1/apps/get-auth-client.js +++ /dev/null @@ -1,18 +0,0 @@ -const getAppAuthClientMock = (appAuthClient) => { - return { - data: { - name: appAuthClient.name, - id: appAuthClient.id, - active: appAuthClient.active, - }, - meta: { - count: 1, - currentPage: null, - isArray: false, - totalPages: null, - type: 'AppAuthClient', - }, - }; -}; - -export default getAppAuthClientMock; diff --git a/packages/backend/test/mocks/rest/api/v1/apps/get-auth-clients.js b/packages/backend/test/mocks/rest/api/v1/apps/get-auth-clients.js deleted file mode 100644 index 0a697dec..00000000 --- a/packages/backend/test/mocks/rest/api/v1/apps/get-auth-clients.js +++ /dev/null @@ -1,18 +0,0 @@ -const getAppAuthClientsMock = (appAuthClients) => { - return { - data: appAuthClients.map((appAuthClient) => ({ - name: appAuthClient.name, - id: appAuthClient.id, - active: appAuthClient.active, - })), - meta: { - count: appAuthClients.length, - currentPage: null, - isArray: true, - totalPages: null, - type: 'AppAuthClient', - }, - }; -}; - -export default getAppAuthClientsMock; 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..d7b9f0e9 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,9 +3,8 @@ const getConnectionsMock = (connections) => { data: connections.map((connection) => ({ id: connection.id, key: connection.key, - reconnectable: connection.reconnectable, verified: connection.verified, - appAuthClientId: connection.appAuthClientId, + oauthClientId: connection.oauthClientId, formattedData: { screenName: connection.formattedData.screenName, }, diff --git a/packages/backend/test/mocks/rest/api/v1/apps/get-oauth-client.js b/packages/backend/test/mocks/rest/api/v1/apps/get-oauth-client.js new file mode 100644 index 00000000..1431b968 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/apps/get-oauth-client.js @@ -0,0 +1,18 @@ +const getOAuthClientMock = (oauthClient) => { + return { + data: { + name: oauthClient.name, + id: oauthClient.id, + active: oauthClient.active, + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'OAuthClient', + }, + }; +}; + +export default getOAuthClientMock; diff --git a/packages/backend/test/mocks/rest/api/v1/apps/get-oauth-clients.js b/packages/backend/test/mocks/rest/api/v1/apps/get-oauth-clients.js new file mode 100644 index 00000000..549544b0 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/apps/get-oauth-clients.js @@ -0,0 +1,18 @@ +const getOAuthClientsMock = (oauthClients) => { + return { + data: oauthClients.map((oauthClient) => ({ + name: oauthClient.name, + id: oauthClient.id, + active: oauthClient.active, + })), + meta: { + count: oauthClients.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'OAuthClient', + }, + }; +}; + +export default getOAuthClientsMock; 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..f618a641 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,8 +3,7 @@ const resetConnectionMock = (connection) => { id: connection.id, key: connection.key, verified: connection.verified, - reconnectable: connection.reconnectable, - appAuthClientId: connection.appAuthClientId, + oauthClientId: connection.oauthClientId, 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..306f7726 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,8 +3,7 @@ const updateConnectionMock = (connection) => { id: connection.id, key: connection.key, verified: connection.verified, - reconnectable: connection.reconnectable, - appAuthClientId: connection.appAuthClientId, + oauthClientId: connection.oauthClientId, 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..18731302 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,8 +3,7 @@ const getConnectionMock = async (connection) => { id: connection.id, key: connection.key, verified: connection.verified, - reconnectable: connection.reconnectable, - appAuthClientId: connection.appAuthClientId, + oauthClientId: connection.oauthClientId, formattedData: { screenName: connection.formattedData.screenName, }, diff --git a/packages/backend/vitest.config.js b/packages/backend/vitest.config.js index 5f4ed961..e75ea51d 100644 --- a/packages/backend/vitest.config.js +++ b/packages/backend/vitest.config.js @@ -2,8 +2,29 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { + root: './', environment: 'node', setupFiles: ['./test/setup/global-hooks.js'], globals: true, + reporters: process.env.GITHUB_ACTIONS ? ['dot', 'github-actions'] : ['dot'], + coverage: { + reportOnFailure: true, + provider: 'v8', + reportsDirectory: './coverage', + reporter: ['text', 'lcov'], + all: true, + include: ['**/src/models/**', '**/src/controllers/**'], + exclude: [ + '**/src/controllers/webhooks/**', + '**/src/controllers/paddle/**', + ], + thresholds: { + autoUpdate: true, + statements: 99.44, + branches: 97.78, + functions: 99.1, + lines: 99.44, + }, + }, }, }); diff --git a/packages/backend/yarn.lock b/packages/backend/yarn.lock index c4838d96..bf899f12 100644 --- a/packages/backend/yarn.lock +++ b/packages/backend/yarn.lock @@ -2,6 +2,31 @@ # yarn lockfile v1 +"@ampproject/remapping@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + +"@babel/parser@^7.25.4": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.2.tgz#fd7b6f487cfea09889557ef5d4eeb9ff9a5abd11" + integrity sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ== + dependencies: + "@babel/types" "^7.26.0" + "@babel/runtime@^7.15.4": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" @@ -9,6 +34,19 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/types@^7.25.4", "@babel/types@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.0.tgz#deabd08d6b753bc8e0f198f8709fb575e31774ff" + integrity sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + "@bull-board/api@3.11.1": version "3.11.1" resolved "https://registry.yarnpkg.com/@bull-board/api/-/api-3.11.1.tgz#98b2c9556f643718bb5bde4a1306e6706af8192e" @@ -242,18 +280,43 @@ wrap-ansi "^8.1.0" wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" -"@jest/schemas@^29.6.3": - version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" - integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== - dependencies: - "@sinclair/typebox" "^0.27.8" +"@istanbuljs/schema@^0.1.2": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@jridgewell/sourcemap-codec@^1.5.0": +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== +"@jridgewell/trace-mapping@^0.3.23", "@jridgewell/trace-mapping@^0.3.24": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@mapbox/node-pre-gyp@^1.0.11": version "1.0.11" resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa" @@ -574,11 +637,6 @@ dependencies: "@sentry/types" "7.120.0" -"@sinclair/typebox@^0.27.8": - version "0.27.8" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" - integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== - "@types/body-parser@*": version "1.19.5" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" @@ -770,49 +828,82 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@vitest/expect@1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-1.6.0.tgz#0b3ba0914f738508464983f4d811bc122b51fb30" - integrity sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ== +"@vitest/coverage-v8@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-2.1.5.tgz#74ef3bf6737f9897a54af22f820d90e85883ff83" + integrity sha512-/RoopB7XGW7UEkUndRXF87A9CwkoZAJW01pj8/3pgmDVsjMH2IKy6H1A38po9tmUlwhSyYs0az82rbKd9Yaynw== dependencies: - "@vitest/spy" "1.6.0" - "@vitest/utils" "1.6.0" - chai "^4.3.10" + "@ampproject/remapping" "^2.3.0" + "@bcoe/v8-coverage" "^0.2.3" + debug "^4.3.7" + istanbul-lib-coverage "^3.2.2" + istanbul-lib-report "^3.0.1" + istanbul-lib-source-maps "^5.0.6" + istanbul-reports "^3.1.7" + magic-string "^0.30.12" + magicast "^0.3.5" + std-env "^3.8.0" + test-exclude "^7.0.1" + tinyrainbow "^1.2.0" -"@vitest/runner@1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-1.6.0.tgz#a6de49a96cb33b0e3ba0d9064a3e8d6ce2f08825" - integrity sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg== +"@vitest/expect@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.5.tgz#5a6afa6314cae7a61847927bb5bc038212ca7381" + integrity sha512-nZSBTW1XIdpZvEJyoP/Sy8fUg0b8od7ZpGDkTUcfJ7wz/VoZAFzFfLyxVxGFhUjJzhYqSbIpfMtl/+k/dpWa3Q== dependencies: - "@vitest/utils" "1.6.0" - p-limit "^5.0.0" - pathe "^1.1.1" + "@vitest/spy" "2.1.5" + "@vitest/utils" "2.1.5" + chai "^5.1.2" + tinyrainbow "^1.2.0" -"@vitest/snapshot@1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-1.6.0.tgz#deb7e4498a5299c1198136f56e6e0f692e6af470" - integrity sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ== +"@vitest/mocker@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.5.tgz#54ee50648bc0bb606dfc58e13edfacb8b9208324" + integrity sha512-XYW6l3UuBmitWqSUXTNXcVBUCRytDogBsWuNXQijc00dtnU/9OqpXWp4OJroVrad/gLIomAq9aW8yWDBtMthhQ== dependencies: - magic-string "^0.30.5" - pathe "^1.1.1" - pretty-format "^29.7.0" - -"@vitest/spy@1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-1.6.0.tgz#362cbd42ccdb03f1613798fde99799649516906d" - integrity sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw== - dependencies: - tinyspy "^2.2.0" - -"@vitest/utils@1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-1.6.0.tgz#5c5675ca7d6f546a7b4337de9ae882e6c57896a1" - integrity sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw== - dependencies: - diff-sequences "^29.6.3" + "@vitest/spy" "2.1.5" estree-walker "^3.0.3" - loupe "^2.3.7" - pretty-format "^29.7.0" + magic-string "^0.30.12" + +"@vitest/pretty-format@2.1.5", "@vitest/pretty-format@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.5.tgz#bc79b8826d4a63dc04f2a75d2944694039fa50aa" + integrity sha512-4ZOwtk2bqG5Y6xRGHcveZVr+6txkH7M2e+nPFd6guSoN638v/1XQ0K06eOpi0ptVU/2tW/pIU4IoPotY/GZ9fw== + dependencies: + tinyrainbow "^1.2.0" + +"@vitest/runner@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.5.tgz#4d5e2ba2dfc0af74e4b0f9f3f8be020559b26ea9" + integrity sha512-pKHKy3uaUdh7X6p1pxOkgkVAFW7r2I818vHDthYLvUyjRfkKOU6P45PztOch4DZarWQne+VOaIMwA/erSSpB9g== + dependencies: + "@vitest/utils" "2.1.5" + pathe "^1.1.2" + +"@vitest/snapshot@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.5.tgz#a09a8712547452a84e08b3ec97b270d9cc156b4f" + integrity sha512-zmYw47mhfdfnYbuhkQvkkzYroXUumrwWDGlMjpdUr4jBd3HZiV2w7CQHj+z7AAS4VOtWxI4Zt4bWt4/sKcoIjg== + dependencies: + "@vitest/pretty-format" "2.1.5" + magic-string "^0.30.12" + pathe "^1.1.2" + +"@vitest/spy@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.5.tgz#f790d1394a5030644217ce73562e92465e83147e" + integrity sha512-aWZF3P0r3w6DiYTVskOYuhBc7EMc3jvn1TkBg8ttylFFRqNN2XGD7V5a4aQdk6QiUzZQ4klNBSpCLJgWNdIiNw== + dependencies: + tinyspy "^3.0.2" + +"@vitest/utils@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.5.tgz#0e19ce677c870830a1573d33ee86b0d6109e9546" + integrity sha512-yfj6Yrp0Vesw2cwJbP+cl04OC+IHFsuQsrsJBL9pyGeQXE56v1UAOQco+SR55Vf1nQzfV0QJg1Qum7AaWUwwYg== + dependencies: + "@vitest/pretty-format" "2.1.5" + loupe "^3.1.2" + tinyrainbow "^1.2.0" "@xmldom/xmldom@^0.8.5", "@xmldom/xmldom@^0.8.6", "@xmldom/xmldom@^0.8.8": version "0.8.10" @@ -847,14 +938,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn-walk@^8.3.2: - version "8.3.4" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" - integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== - dependencies: - acorn "^8.11.0" - -acorn@^8.11.0, acorn@^8.14.0, acorn@^8.9.0: +acorn@^8.9.0: version "8.14.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== @@ -925,11 +1009,6 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" - integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== - ansi-styles@^6.1.0: version "6.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" @@ -976,10 +1055,10 @@ asap@^2.0.0: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== -assertion-error@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" - integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== +assertion-error@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" + integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== async@^3.2.3: version "3.2.6" @@ -1211,18 +1290,16 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -chai@^4.3.10: - version "4.5.0" - resolved "https://registry.yarnpkg.com/chai/-/chai-4.5.0.tgz#707e49923afdd9b13a8b0b47d33d732d13812fd8" - integrity sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw== +chai@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/chai/-/chai-5.1.2.tgz#3afbc340b994ae3610ca519a6c70ace77ad4378d" + integrity sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw== dependencies: - assertion-error "^1.1.0" - check-error "^1.0.3" - deep-eql "^4.1.3" - get-func-name "^2.0.2" - loupe "^2.3.6" - pathval "^1.1.1" - type-detect "^4.1.0" + assertion-error "^2.0.1" + check-error "^2.1.1" + deep-eql "^5.0.1" + loupe "^3.1.0" + pathval "^2.0.0" chalk@^4.0.0, chalk@^4.0.2: version "4.1.2" @@ -1237,12 +1314,10 @@ charenc@0.0.2: resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== -check-error@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" - integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== - dependencies: - get-func-name "^2.0.2" +check-error@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc" + integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw== chokidar@^3.5.2: version "3.6.0" @@ -1379,11 +1454,6 @@ concat-stream@^1.5.2: readable-stream "^2.2.2" typedarray "^0.0.6" -confbox@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06" - integrity sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w== - console-control-strings@^1.0.0, console-control-strings@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" @@ -1441,7 +1511,7 @@ cron-parser@^4.2.1, cron-parser@^4.6.0: dependencies: luxon "^3.2.1" -cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.0, cross-spawn@^7.0.2: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -1488,7 +1558,7 @@ debug@2.6.9, debug@~2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: +debug@4, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.7: version "4.3.7" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== @@ -1516,12 +1586,10 @@ decompress-response@^6.0.0: dependencies: mimic-response "^3.1.0" -deep-eql@^4.1.3: - version "4.1.4" - resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.4.tgz#d0d3912865911bb8fac5afb4e3acfa6a28dc72b7" - integrity sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg== - dependencies: - type-detect "^4.0.0" +deep-eql@^5.0.1: + version "5.0.2" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341" + integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q== deep-extend@^0.6.0: version "0.6.0" @@ -1590,11 +1658,6 @@ dezalgo@^1.0.4: asap "^2.0.0" wrappy "1" -diff-sequences@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" - integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== - doctrine@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" @@ -1722,6 +1785,11 @@ es-errors@^1.3.0: resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== +es-module-lexer@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" + integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== + esbuild@^0.21.3: version "0.21.5" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" @@ -1885,26 +1953,16 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== -execa@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" - integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== - dependencies: - cross-spawn "^7.0.3" - get-stream "^8.0.1" - human-signals "^5.0.0" - is-stream "^3.0.0" - merge-stream "^2.0.0" - npm-run-path "^5.1.0" - onetime "^6.0.0" - signal-exit "^4.1.0" - strip-final-newline "^3.0.0" - expand-template@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== +expect-type@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.1.0.tgz#a146e414250d13dfc49eafcfd1344a4060fa4c75" + integrity sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA== + exponential-backoff@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" @@ -2209,11 +2267,6 @@ gauge@^3.0.0: strip-ansi "^6.0.1" wide-align "^1.1.2" -get-func-name@^2.0.1, get-func-name@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" - integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== - get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" @@ -2235,11 +2288,6 @@ get-port@^5.1.1: resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== -get-stream@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" - integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== - getopts@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.3.0.tgz#71e5593284807e03e2427449d4f6712a268666f4" @@ -2264,7 +2312,7 @@ glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" -glob@^10.2.2, glob@^10.3.10: +glob@^10.2.2, glob@^10.3.10, glob@^10.4.1: version "10.4.5" resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== @@ -2384,6 +2432,11 @@ hexoid@^1.0.0: resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + http-cache-semantics@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" @@ -2445,11 +2498,6 @@ https-proxy-agent@^7.0.1: agent-base "^7.0.2" debug "4" -human-signals@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" - integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== - iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -2624,11 +2672,6 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== -is-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" - integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== - isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -2651,6 +2694,37 @@ isolated-vm@^5.0.1: dependencies: prebuild-install "^7.1.1" +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== + +istanbul-lib-report@^3.0.0, istanbul-lib-report@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" + integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^4.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^5.0.6: + version "5.0.6" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz#acaef948df7747c8eb5fbf1265cb980f6353a441" + integrity sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A== + dependencies: + "@jridgewell/trace-mapping" "^0.3.23" + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + +istanbul-reports@^3.1.7: + version "3.1.7" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b" + integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + jackspeak@^3.1.2: version "3.4.3" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" @@ -2675,11 +2749,6 @@ join-component@^1.1.0: resolved "https://registry.yarnpkg.com/join-component/-/join-component-1.1.0.tgz#b8417b750661a392bee2c2537c68b2a9d4977cd5" integrity sha512-bF7vcQxbODoGK1imE2P9GS9aw4zD0Sd+Hni68IMZLj7zRnquH7dXUmMw9hDI5S/Jzt7q+IyTXN0rSg2GI0IKhQ== -js-tokens@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-9.0.0.tgz#0f893996d6f3ed46df7f0a3b12a03f5fd84223c1" - integrity sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ== - js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -2797,14 +2866,6 @@ lie@3.1.1: dependencies: immediate "~3.0.5" -local-pkg@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.5.1.tgz#69658638d2a95287534d4c2fff757980100dbb6d" - integrity sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ== - dependencies: - mlly "^1.7.3" - pkg-types "^1.2.1" - localforage@^1.8.1: version "1.10.0" resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4" @@ -2896,12 +2957,10 @@ logform@^2.7.0: safe-stable-stringify "^2.3.1" triple-beam "^1.3.0" -loupe@^2.3.6, loupe@^2.3.7: - version "2.3.7" - resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" - integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== - dependencies: - get-func-name "^2.0.1" +loupe@^3.1.0, loupe@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.2.tgz#c86e0696804a02218f2206124c45d8b15291a240" + integrity sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg== lru-cache@^10.0.1, lru-cache@^10.2.0: version "10.4.3" @@ -2918,13 +2977,22 @@ luxon@^3.2.1: resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.5.0.tgz#6b6f65c5cd1d61d1fd19dbf07ee87a50bf4b8e20" integrity sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ== -magic-string@^0.30.5: +magic-string@^0.30.12: version "0.30.13" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.13.tgz#92438e3ff4946cf54f18247c981e5c161c46683c" integrity sha512-8rYBO+MsWkgjDSOvLomYnzhdwEG51olQ4zL5KXnNJWV5MNmrb4rTZdrtkhxjnD/QyZUqR/Z/XDsUs/4ej2nx0g== dependencies: "@jridgewell/sourcemap-codec" "^1.5.0" +magicast@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/magicast/-/magicast-0.3.5.tgz#8301c3c7d66704a0771eb1bad74274f0ec036739" + integrity sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ== + dependencies: + "@babel/parser" "^7.25.4" + "@babel/types" "^7.25.4" + source-map-js "^1.2.0" + make-dir@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -2932,6 +3000,13 @@ make-dir@^3.1.0: dependencies: semver "^6.0.0" +make-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" + integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== + dependencies: + semver "^7.5.3" + make-fetch-happen@^13.0.0: version "13.0.1" resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz#273ba2f78f45e1f3a6dca91cede87d9fa4821e36" @@ -2974,11 +3049,6 @@ merge-descriptors@1.0.1: resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -3006,11 +3076,6 @@ mime@2.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== -mimic-fn@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" - integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== - mimic-response@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" @@ -3123,16 +3188,6 @@ mkdirp@^1.0.3: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mlly@^1.7.2, mlly@^1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.3.tgz#d86c0fcd8ad8e16395eb764a5f4b831590cee48c" - integrity sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A== - dependencies: - acorn "^8.14.0" - pathe "^1.1.2" - pkg-types "^1.2.1" - ufo "^1.5.4" - morgan@^1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" @@ -3320,13 +3375,6 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -npm-run-path@^5.1.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.3.0.tgz#e23353d0ebb9317f174e93417e4a4d82d0249e9f" - integrity sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ== - dependencies: - path-key "^4.0.0" - npmlog@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" @@ -3401,13 +3449,6 @@ one-time@^1.0.0: dependencies: fn.name "1.x.x" -onetime@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" - integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== - dependencies: - mimic-fn "^4.0.0" - optionator@^0.9.3: version "0.9.4" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" @@ -3427,13 +3468,6 @@ p-limit@^3.0.2: dependencies: yocto-queue "^0.1.0" -p-limit@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-5.0.0.tgz#6946d5b7140b649b7a33a027d89b4c625b3a5985" - integrity sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ== - dependencies: - yocto-queue "^1.0.0" - p-locate@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" @@ -3494,11 +3528,6 @@ path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-key@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" - integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== - path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" @@ -3517,15 +3546,15 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== -pathe@^1.1.1, pathe@^1.1.2: +pathe@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== -pathval@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" - integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== +pathval@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.0.tgz#7e2550b422601d4f6b8e26f1301bc8f15a741a25" + integrity sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA== pause@0.0.1: version "0.0.1" @@ -3598,7 +3627,7 @@ php-serialize@^4.0.2: resolved "https://registry.yarnpkg.com/php-serialize/-/php-serialize-4.1.1.tgz#1a614fde3da42361af05afffbaf967fb6556591e" integrity sha512-7drCrSZdJ05UdG3hyYEIRW0XyKyUFkxa5A3dpIp3NTjUHpI080pkdBAvqaBtkA+kBkMeXX3XnaSnaLGJRz071A== -picocolors@^1.0.0, picocolors@^1.1.1: +picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== @@ -3608,15 +3637,6 @@ picomatch@^2.0.4, picomatch@^2.2.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -pkg-types@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.2.1.tgz#6ac4e455a5bb4b9a6185c1c79abd544c901db2e5" - integrity sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw== - dependencies: - confbox "^0.1.8" - mlly "^1.7.2" - pathe "^1.1.2" - pluralize@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" @@ -3688,15 +3708,6 @@ prettier@^2.5.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== -pretty-format@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" - integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== - dependencies: - "@jest/schemas" "^29.6.3" - ansi-styles "^5.0.0" - react-is "^18.0.0" - proc-log@^4.1.0, proc-log@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-4.2.0.tgz#b6f461e4026e75fdfe228b265e9f7a00779d7034" @@ -3812,11 +3823,6 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-is@^18.0.0: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" - integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== - readable-stream@^2.2.2: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" @@ -3997,7 +4003,7 @@ semver@^6.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.2, semver@^7.5.4: +semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4: version "7.6.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== @@ -4138,7 +4144,7 @@ signal-exit@^3.0.0: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== -signal-exit@^4.0.1, signal-exit@^4.1.0: +signal-exit@^4.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== @@ -4193,7 +4199,7 @@ socks@^2.8.3: ip-address "^9.0.5" smart-buffer "^4.2.0" -source-map-js@^1.2.1: +source-map-js@^1.2.0, source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== @@ -4245,7 +4251,7 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -std-env@^3.5.0: +std-env@^3.8.0: version "3.8.0" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.8.0.tgz#b56ffc1baf1a29dcc80a3bdf11d7fca7c315e7d5" integrity sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w== @@ -4255,7 +4261,16 @@ streamsearch@^1.1.0: resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.3: +"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@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.3: 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== @@ -4287,7 +4302,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.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== @@ -4301,11 +4323,6 @@ strip-ansi@^7.0.1: dependencies: ansi-regex "^6.0.1" -strip-final-newline@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" - integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== - strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" @@ -4316,13 +4333,6 @@ strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== -strip-literal@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-2.1.0.tgz#6d82ade5e2e74f5c7e8739b6c84692bd65f0bd2a" - integrity sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw== - dependencies: - js-tokens "^9.0.0" - strnum@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" @@ -4409,6 +4419,15 @@ tarn@^3.0.2: resolved "https://registry.yarnpkg.com/tarn/-/tarn-3.0.2.tgz#73b6140fbb881b71559c4f8bfde3d9a4b3d27693" integrity sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ== +test-exclude@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-7.0.1.tgz#20b3ba4906ac20994e275bbcafd68d510264c2a2" + integrity sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^10.4.1" + minimatch "^9.0.4" + text-hex@1.0.x: version "1.0.0" resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" @@ -4424,20 +4443,30 @@ tildify@2.0.0: resolved "https://registry.yarnpkg.com/tildify/-/tildify-2.0.0.tgz#f205f3674d677ce698b7067a99e949ce03b4754a" integrity sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw== -tinybench@^2.5.1: +tinybench@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== -tinypool@^0.8.3: - version "0.8.4" - resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.8.4.tgz#e217fe1270d941b39e98c625dcecebb1408c9aa8" - integrity sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ== +tinyexec@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.1.tgz#0ab0daf93b43e2c211212396bdb836b468c97c98" + integrity sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ== -tinyspy@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.2.1.tgz#117b2342f1f38a0dbdcc73a50a454883adf861d1" - integrity sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A== +tinypool@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.2.tgz#706193cc532f4c100f66aa00b01c42173d9051b2" + integrity sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA== + +tinyrainbow@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-1.2.0.tgz#5c57d2fc0fb3d1afd78465c33ca885d04f02abb5" + integrity sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ== + +tinyspy@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a" + integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q== to-regex-range@^5.0.1: version "5.0.1" @@ -4485,11 +4514,6 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-detect@^4.0.0, type-detect@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.1.0.tgz#deb2453e8f08dcae7ae98c626b13dddb0155906c" - integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw== - type-fest@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" @@ -4508,11 +4532,6 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -ufo@^1.5.4: - version "1.5.4" - resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754" - integrity sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ== - uglify-js@^3.1.4: version "3.19.3" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f" @@ -4579,15 +4598,15 @@ vary@^1, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -vite-node@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.6.0.tgz#2c7e61129bfecc759478fa592754fd9704aaba7f" - integrity sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw== +vite-node@2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.5.tgz#cf28c637b2ebe65921f3118a165b7cf00a1cdf19" + integrity sha512-rd0QIgx74q4S1Rd56XIiL2cYEdyWn13cunYBIuqh9mpmQr7gGS0IxXoP8R6OaZtNQQLyXSWbd4rXKYUbhFpK5w== dependencies: cac "^6.7.14" - debug "^4.3.4" - pathe "^1.1.1" - picocolors "^1.0.0" + debug "^4.3.7" + es-module-lexer "^1.5.4" + pathe "^1.1.2" vite "^5.0.0" vite@^5.0.0: @@ -4601,31 +4620,31 @@ vite@^5.0.0: optionalDependencies: fsevents "~2.3.3" -vitest@^1.1.3: - version "1.6.0" - resolved "https://registry.yarnpkg.com/vitest/-/vitest-1.6.0.tgz#9d5ad4752a3c451be919e412c597126cffb9892f" - integrity sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA== +vitest@^2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.5.tgz#a93b7b84a84650130727baae441354e6df118148" + integrity sha512-P4ljsdpuzRTPI/kbND2sDZ4VmieerR2c9szEZpjc+98Z9ebvnXmM5+0tHEKqYZumXqlvnmfWsjeFOjXVriDG7A== dependencies: - "@vitest/expect" "1.6.0" - "@vitest/runner" "1.6.0" - "@vitest/snapshot" "1.6.0" - "@vitest/spy" "1.6.0" - "@vitest/utils" "1.6.0" - acorn-walk "^8.3.2" - chai "^4.3.10" - debug "^4.3.4" - execa "^8.0.1" - local-pkg "^0.5.0" - magic-string "^0.30.5" - pathe "^1.1.1" - picocolors "^1.0.0" - std-env "^3.5.0" - strip-literal "^2.0.0" - tinybench "^2.5.1" - tinypool "^0.8.3" + "@vitest/expect" "2.1.5" + "@vitest/mocker" "2.1.5" + "@vitest/pretty-format" "^2.1.5" + "@vitest/runner" "2.1.5" + "@vitest/snapshot" "2.1.5" + "@vitest/spy" "2.1.5" + "@vitest/utils" "2.1.5" + chai "^5.1.2" + debug "^4.3.7" + expect-type "^1.1.0" + magic-string "^0.30.12" + pathe "^1.1.2" + std-env "^3.8.0" + tinybench "^2.9.0" + tinyexec "^0.3.1" + tinypool "^1.0.1" + tinyrainbow "^1.2.0" vite "^5.0.0" - vite-node "1.6.0" - why-is-node-running "^2.2.2" + vite-node "2.1.5" + why-is-node-running "^2.3.0" webidl-conversions@^3.0.0: version "3.0.1" @@ -4654,7 +4673,7 @@ which@^4.0.0: dependencies: isexe "^3.1.1" -why-is-node-running@^2.2.2: +why-is-node-running@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04" integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== @@ -4800,8 +4819,3 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -yocto-queue@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.1.1.tgz#fef65ce3ac9f8a32ceac5a634f74e17e5b232110" - integrity sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g== diff --git a/packages/e2e-tests/fixtures/admin/application-auth-clients-page.js b/packages/e2e-tests/fixtures/admin/application-oauth-clients-page.js similarity index 59% rename from packages/e2e-tests/fixtures/admin/application-auth-clients-page.js rename to packages/e2e-tests/fixtures/admin/application-oauth-clients-page.js index bedddbf4..e0258eeb 100644 --- a/packages/e2e-tests/fixtures/admin/application-auth-clients-page.js +++ b/packages/e2e-tests/fixtures/admin/application-oauth-clients-page.js @@ -2,19 +2,25 @@ import { expect } from '@playwright/test'; const { AuthenticatedPage } = require('../authenticated-page'); -export class AdminApplicationAuthClientsPage extends AuthenticatedPage { +export class AdminApplicationOAuthClientsPage extends AuthenticatedPage { /** * @param {import('@playwright/test').Page} page */ constructor(page) { super(page); - this.authClientsTab = this.page.getByText('AUTH CLIENTS'); + this.authClientsTab = this.page.getByTestId('oauth-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 9e1ba00c..2e756d28 100644 --- a/packages/e2e-tests/fixtures/admin/application-settings-page.js +++ b/packages/e2e-tests/fixtures/admin/application-settings-page.js @@ -8,66 +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.allowCustomConnectionsSwitch).not.toBeChecked(); - await this.allowCustomConnectionsSwitch.check(); - await expect(this.allowCustomConnectionsSwitch).toBeChecked(); - await this.saveButton.click(); + async allowUseOnlyPredefinedAuthClients() { + await expect(this.useOnlyPredefinedAuthClients).not.toBeChecked(); + await this.useOnlyPredefinedAuthClients.check(); } - async allowSharedConnections() { - await expect(this.allowSharedConnectionsSwitch).not.toBeChecked(); - await this.allowSharedConnectionsSwitch.check(); - await expect(this.allowSharedConnectionsSwitch).toBeChecked(); - 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 expect(this.disableConnectionsSwitch).toBeChecked(); - await this.saveButton.click(); - } - - async disallowCustomConnections() { - await expect(this.allowCustomConnectionsSwitch).toBeChecked(); - await this.allowCustomConnectionsSwitch.uncheck(); - await expect(this.allowCustomConnectionsSwitch).not.toBeChecked(); - await this.saveButton.click(); - } - - async disallowSharedConnections() { - await expect(this.allowSharedConnectionsSwitch).toBeChecked(); - await this.allowSharedConnectionsSwitch.uncheck(); - await expect(this.allowSharedConnectionsSwitch).not.toBeChecked(); - await this.saveButton.click(); } async allowConnections() { await expect(this.disableConnectionsSwitch).toBeChecked(); await this.disableConnectionsSwitch.uncheck(); - await expect(this.disableConnectionsSwitch).not.toBeChecked(); + } + + async saveSettings() { await this.saveButton.click(); } async expectSuccessSnackbarToBeVisible() { const snackbars = await this.successSnackbar.all(); for (const snackbar of snackbars) { - await expect(await snackbar.getAttribute('data-snackbar-variant')).toBe( - 'success' - ); - // await snackbar.click(); + await expect(snackbar).toBeVisible(); } } } diff --git a/packages/e2e-tests/fixtures/admin/index.js b/packages/e2e-tests/fixtures/admin/index.js index 746c85dd..db99cf35 100644 --- a/packages/e2e-tests/fixtures/admin/index.js +++ b/packages/e2e-tests/fixtures/admin/index.js @@ -8,7 +8,9 @@ const { AdminEditRolePage } = require('./edit-role-page'); const { AdminApplicationsPage } = require('./applications-page'); const { AdminApplicationSettingsPage } = require('./application-settings-page'); -const { AdminApplicationAuthClientsPage } = require('./application-auth-clients-page'); +const { + AdminApplicationOAuthClientsPage, +} = require('./application-oauth-clients-page'); export const adminFixtures = { adminUsersPage: async ({ page }, use) => { @@ -35,8 +37,7 @@ export const adminFixtures = { adminApplicationSettingsPage: async ({ page }, use) => { await use(new AdminApplicationSettingsPage(page)); }, - adminApplicationAuthClientsPage: async ({ page }, use) => { - await use(new AdminApplicationAuthClientsPage(page)); - } + adminApplicationOAuthClientsPage: async ({ page }, use) => { + await use(new AdminApplicationOAuthClientsPage(page)); + }, }; - 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 8cc35450..d66d5917 100644 --- a/packages/e2e-tests/tests/admin/applications.spec.js +++ b/packages/e2e-tests/tests/admin/applications.spec.js @@ -1,23 +1,36 @@ 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'], + const deleteOAuthClients = { + text: 'DELETE FROM oauth_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 - ); - expect(deleteAppAuthClientsResult.command).toBe('DELETE'); + const deleteOAuthClientsResult = await pgPool.query(deleteOAuthClients); + expect(deleteOAuthClientsResult.command).toBe('DELETE'); const deleteAppConfigsResult = await pgPool.query(deleteAppConfigs); expect(deleteAppConfigsResult.command).toBe('DELETE'); } catch (err) { @@ -39,35 +52,126 @@ test.describe('Admin Applications', () => { 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, + adminApplicationOAuthClientsPage, flowEditorPage, page, }) => { + await insertAppConnection('google-drive'); + + // 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 adminApplicationOAuthClientsPage.openAuthClientsTab(); + await expect( + adminApplicationOAuthClientsPage.createFirstAuthClientButton + ).toHaveCount(1); + + 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); + + await flowEditorPage.chooseAppAndEvent( + 'Google Drive', + 'New files in folder' + ); + await flowEditorPage.connectionAutocomplete.click(); + + const newConnectionOption = page + .getByRole('option') + .filter({ hasText: 'Add new connection' }); + const newOAuthConnectionOption = page + .getByRole('option') + .filter({ hasText: 'Add connection with OAuth 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(newOAuthConnectionOption).toHaveCount(0); + }); + + test('should allow only predefined connections and existing custom', async ({ + adminApplicationsPage, + adminApplicationSettingsPage, + adminApplicationOAuthClientsPage, + flowEditorPage, + page, + }) => { + await insertAppConnection('spotify'); + await adminApplicationsPage.openApplication('Spotify'); await expect(page.url()).toContain('/admin-settings/apps/spotify/settings'); - await adminApplicationSettingsPage.allowCustomConnections(); + await expect( + adminApplicationSettingsPage.useOnlyPredefinedAuthClients + ).not.toBeChecked(); + await expect( + adminApplicationSettingsPage.disableConnectionsSwitch + ).not.toBeChecked(); + + await adminApplicationSettingsPage.allowUseOnlyPredefinedAuthClients(); + await adminApplicationSettingsPage.saveSettings(); await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); + await adminApplicationOAuthClientsPage.openAuthClientsTab(); + await adminApplicationOAuthClientsPage.openFirstAuthClientCreateForm(); + const authClientForm = page.getByTestId('auth-client-form'); + await authClientForm.locator(page.getByTestId('switch')).check(); + 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 adminApplicationOAuthClientsPage.submitAuthClientForm(); + await adminApplicationOAuthClientsPage.authClientShouldBeVisible( + 'spotifyAuthClient' + ); + await page.goto('/'); await page.getByTestId('create-flow-button').click(); await page.waitForURL( @@ -84,30 +188,41 @@ test.describe('Admin Applications', () => { const newConnectionOption = page .getByRole('option') .filter({ hasText: 'Add new connection' }); - const newSharedConnectionOption = page + const newOAuthConnectionOption = page .getByRole('option') - .filter({ hasText: 'Add new shared connection' }); + .filter({ hasText: 'Add connection with OAuth client' }); + const existingConnection = page + .getByRole('option') + .filter({ hasText: 'Unnamed' }); - await expect(newConnectionOption).toBeEnabled(); - await expect(newConnectionOption).toHaveCount(1); - await expect(newSharedConnectionOption).toHaveCount(0); + await expect(await existingConnection.count()).toBeGreaterThan(0); + await expect(newConnectionOption).toHaveCount(0); + await expect(newOAuthConnectionOption).toBeEnabled(); + await expect(newOAuthConnectionOption).toHaveCount(1); }); - test('should allow only shared connections', async ({ + test('should allow all connections', async ({ adminApplicationsPage, adminApplicationSettingsPage, - adminApplicationAuthClientsPage, + adminApplicationOAuthClientsPage, flowEditorPage, page, }) => { + await insertAppConnection('reddit'); + await adminApplicationsPage.openApplication('Reddit'); await expect(page.url()).toContain('/admin-settings/apps/reddit/settings'); - await adminApplicationSettingsPage.allowSharedConnections(); - await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); + await expect( + adminApplicationSettingsPage.useOnlyPredefinedAuthClients + ).not.toBeChecked(); + await expect( + adminApplicationSettingsPage.disableConnectionsSwitch + ).not.toBeChecked(); + + await adminApplicationOAuthClientsPage.openAuthClientsTab(); + await adminApplicationOAuthClientsPage.openFirstAuthClientCreateForm(); - await adminApplicationAuthClientsPage.openAuthClientsTab(); - await adminApplicationAuthClientsPage.openFirstAuthClientCreateForm(); const authClientForm = page.getByTestId('auth-client-form'); await authClientForm.locator(page.getByTestId('switch')).check(); await authClientForm @@ -119,8 +234,9 @@ test.describe('Admin Applications', () => { await authClientForm .locator(page.locator('[name="clientSecret"]')) .fill('redditClientSecret'); - await adminApplicationAuthClientsPage.submitAuthClientForm(); - await adminApplicationAuthClientsPage.authClientShouldBeVisible( + + await adminApplicationOAuthClientsPage.submitAuthClientForm(); + await adminApplicationOAuthClientsPage.authClientShouldBeVisible( 'redditAuthClient' ); @@ -140,27 +256,54 @@ test.describe('Admin Applications', () => { const newConnectionOption = page .getByRole('option') .filter({ hasText: 'Add new connection' }); - const newSharedConnectionOption = page + const newOAuthConnectionOption = page .getByRole('option') - .filter({ hasText: 'Add new shared connection' }); + .filter({ hasText: 'Add connection with OAuth client' }); + const existingConnection = page + .getByRole('option') + .filter({ hasText: 'Unnamed' }); - await expect(newConnectionOption).toHaveCount(0); - await expect(newSharedConnectionOption).toBeEnabled(); - await expect(newSharedConnectionOption).toHaveCount(1); + await expect(await existingConnection.count()).toBeGreaterThan(0); + await expect(newConnectionOption).toHaveCount(1); + await expect(newOAuthConnectionOption).toBeEnabled(); + await expect(newOAuthConnectionOption).toHaveCount(1); }); - test('should not allow any connections', async ({ + test('should not allow new connections but existing custom', async ({ adminApplicationsPage, adminApplicationSettingsPage, + adminApplicationOAuthClientsPage, flowEditorPage, page, }) => { - await adminApplicationsPage.openApplication('DeepL'); - await expect(page.url()).toContain('/admin-settings/apps/deepl/settings'); + 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 adminApplicationOAuthClientsPage.openAuthClientsTab(); + await adminApplicationOAuthClientsPage.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 adminApplicationOAuthClientsPage.submitAuthClientForm(); + await adminApplicationOAuthClientsPage.authClientShouldBeVisible( + 'clickupAuthClient' + ); + await page.goto('/'); await page.getByTestId('create-flow-button').click(); await page.waitForURL( @@ -171,68 +314,62 @@ test.describe('Admin Applications', () => { const triggerStep = flowEditorPage.flowStep.last(); await triggerStep.click(); - await flowEditorPage.chooseAppAndEvent('DeepL', 'Translate text'); + await flowEditorPage.chooseAppAndEvent('ClickUp', 'Create folder'); await flowEditorPage.connectionAutocomplete.click(); const newConnectionOption = page .getByRole('option') .filter({ hasText: 'Add new connection' }); - const newSharedConnectionOption = page + const newOAuthConnectionOption = page .getByRole('option') - .filter({ hasText: 'Add new shared connection' }); - const noConnectionsOption = page - .locator('.MuiAutocomplete-noOptions') - .filter({ hasText: 'No options' }); + .filter({ hasText: 'Add connection with OAuth 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(0); - await expect(newSharedConnectionOption).toHaveCount(0); + await expect(newOAuthConnectionOption).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 OAuth clients are enabled', async ({ adminApplicationsPage, adminApplicationSettingsPage, + adminApplicationOAuthClientsPage, flowEditorPage, 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 adminApplicationSettingsPage.allowUseOnlyPredefinedAuthClients(); await adminApplicationSettingsPage.disallowConnections(); + await adminApplicationSettingsPage.saveSettings(); await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); + await adminApplicationOAuthClientsPage.openAuthClientsTab(); + await adminApplicationOAuthClientsPage.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 adminApplicationOAuthClientsPage.submitAuthClientForm(); + await adminApplicationOAuthClientsPage.authClientShouldBeVisible( + 'mailchimpAuthClient' + ); + await page.goto('/'); await page.getByTestId('create-flow-button').click(); await page.waitForURL( @@ -253,9 +390,9 @@ test.describe('Admin Applications', () => { const newConnectionOption = page .getByRole('option') .filter({ hasText: 'Add new connection' }); - const newSharedConnectionOption = page + const newOAuthConnectionOption = page .getByRole('option') - .filter({ hasText: 'Add new shared connection' }); + .filter({ hasText: 'Add connection with OAuth client' }); const noConnectionsOption = page .locator('.MuiAutocomplete-noOptions') .filter({ hasText: 'No options' }); @@ -263,6 +400,6 @@ test.describe('Admin Applications', () => { await expect(await existingConnection.count()).toBeGreaterThan(0); await expect(noConnectionsOption).toHaveCount(0); await expect(newConnectionOption).toHaveCount(0); - await expect(newSharedConnectionOption).toHaveCount(0); + await expect(newOAuthConnectionOption).toHaveCount(0); }); }); diff --git a/packages/e2e-tests/tests/apps/list-apps.spec.js b/packages/e2e-tests/tests/apps/list-apps.spec.js index 42e56880..b382782a 100644 --- a/packages/e2e-tests/tests/apps/list-apps.spec.js +++ b/packages/e2e-tests/tests/apps/list-apps.spec.js @@ -55,7 +55,7 @@ test.describe('Apps page', () => { test('goes to app page to create a connection', async ({ applicationsPage, }) => { - // loading app, app config, app auth clients take time + // loading app, app config, app oauth clients take time test.setTimeout(60000); await applicationsPage.page.getByTestId('app-list-item').first().click(); 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..9fef4c77 100644 --- a/packages/web/src/components/AddAppConnection/index.jsx +++ b/packages/web/src/components/AddAppConnection/index.jsx @@ -9,7 +9,7 @@ import * as React from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { AppPropType } from 'propTypes/propTypes'; -import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee'; +import AppOAuthClientsDialog from 'components/OAuthClientsDialog/index.ee'; import InputCreator from 'components/InputCreator'; import * as URLS from 'config/urls'; import useAuthenticateApp from 'hooks/useAuthenticateApp.ee'; @@ -31,12 +31,12 @@ function AddAppConnection(props) { const [inProgress, setInProgress] = React.useState(false); const hasConnection = Boolean(connectionId); const useShared = searchParams.get('shared') === 'true'; - const appAuthClientId = searchParams.get('appAuthClientId') || undefined; + const oauthClientId = searchParams.get('oauthClientId') || undefined; const { authenticate } = useAuthenticateApp({ appKey: key, connectionId, - appAuthClientId, - useShared: !!appAuthClientId, + oauthClientId, + useShared: !!oauthClientId, }); const queryClient = useQueryClient(); @@ -52,8 +52,8 @@ function AddAppConnection(props) { }, []); React.useEffect( - function initiateSharedAuthenticationForGivenAuthClient() { - if (!appAuthClientId) return; + function initiateSharedAuthenticationForGivenOAuthClient() { + if (!oauthClientId) return; if (!authenticate) return; @@ -64,13 +64,13 @@ function AddAppConnection(props) { asyncAuthenticate(); }, - [appAuthClientId, authenticate], + [oauthClientId, authenticate, key, navigate], ); - const handleClientClick = (appAuthClientId) => - navigate(URLS.APP_ADD_CONNECTION_WITH_AUTH_CLIENT_ID(key, appAuthClientId)); + const handleClientClick = (oauthClientId) => + navigate(URLS.APP_ADD_CONNECTION_WITH_OAUTH_CLIENT_ID(key, oauthClientId)); - const handleAuthClientsDialogClose = () => + const handleOAuthClientsDialogClose = () => navigate(URLS.APP_CONNECTIONS(key)); const submitHandler = React.useCallback( @@ -104,14 +104,14 @@ function AddAppConnection(props) { if (useShared) return ( - ); - if (appAuthClientId) return ; + if (oauthClientId) return ; return ( { let appConfigKey = appConfig?.data?.key; if (!appConfigKey) { const { data: appConfigData } = await createAppConfig({ - customConnectionAllowed: true, - shared: false, + useOnlyPredefinedAuthClients: false, disabled: false, }); + appConfigKey = appConfigData.key; } const { name, active, ...formattedAuthDefaults } = values; - await createAppAuthClient({ + await createOAuthClient({ appKey, name, active, @@ -81,23 +81,23 @@ function AdminApplicationCreateAuthClient(props) { ); return ( - ); } -AdminApplicationCreateAuthClient.propTypes = { +AdminApplicationCreateOAuthClient.propTypes = { appKey: PropTypes.string.isRequired, application: AppPropType.isRequired, onClose: PropTypes.func.isRequired, }; -export default AdminApplicationCreateAuthClient; +export default AdminApplicationCreateOAuthClient; diff --git a/packages/web/src/components/AdminApplicationAuthClientDialog/index.jsx b/packages/web/src/components/AdminApplicationOAuthClientDialog/index.jsx similarity index 89% rename from packages/web/src/components/AdminApplicationAuthClientDialog/index.jsx rename to packages/web/src/components/AdminApplicationOAuthClientDialog/index.jsx index 6c328c11..9bb38959 100644 --- a/packages/web/src/components/AdminApplicationAuthClientDialog/index.jsx +++ b/packages/web/src/components/AdminApplicationOAuthClientDialog/index.jsx @@ -15,7 +15,7 @@ import Switch from 'components/Switch'; import TextField from 'components/TextField'; import { Form } from './style'; -function AdminApplicationAuthClientDialog(props) { +function AdminApplicationOAuthClientDialog(props) { const { error, onClose, @@ -52,12 +52,12 @@ function AdminApplicationAuthClientDialog(props) { <> {authFields?.map((field) => ( @@ -72,7 +72,7 @@ function AdminApplicationAuthClientDialog(props) { loading={submitting} disabled={disabled || !isDirty} > - {formatMessage('authClient.buttonSubmit')} + {formatMessage('oauthClient.buttonSubmit')} )} @@ -84,7 +84,7 @@ function AdminApplicationAuthClientDialog(props) { ); } -AdminApplicationAuthClientDialog.propTypes = { +AdminApplicationOAuthClientDialog.propTypes = { error: PropTypes.shape({ message: PropTypes.string, }), @@ -98,4 +98,4 @@ AdminApplicationAuthClientDialog.propTypes = { disabled: PropTypes.bool, }; -export default AdminApplicationAuthClientDialog; +export default AdminApplicationOAuthClientDialog; diff --git a/packages/web/src/components/AdminApplicationAuthClientDialog/style.js b/packages/web/src/components/AdminApplicationOAuthClientDialog/style.js similarity index 100% rename from packages/web/src/components/AdminApplicationAuthClientDialog/style.js rename to packages/web/src/components/AdminApplicationOAuthClientDialog/style.js diff --git a/packages/web/src/components/AdminApplicationAuthClients/index.jsx b/packages/web/src/components/AdminApplicationOAuthClients/index.jsx similarity index 70% rename from packages/web/src/components/AdminApplicationAuthClients/index.jsx rename to packages/web/src/components/AdminApplicationOAuthClients/index.jsx index f39bc384..ae41e8ab 100644 --- a/packages/web/src/components/AdminApplicationAuthClients/index.jsx +++ b/packages/web/src/components/AdminApplicationOAuthClients/index.jsx @@ -8,29 +8,30 @@ import CardContent from '@mui/material/CardContent'; import Typography from '@mui/material/Typography'; import Chip from '@mui/material/Chip'; import Button from '@mui/material/Button'; + +import NoResultFound from 'components/NoResultFound'; import * as URLS from 'config/urls'; import useFormatMessage from 'hooks/useFormatMessage'; -import useAdminAppAuthClients from 'hooks/useAdminAppAuthClients'; -import NoResultFound from 'components/NoResultFound'; +import useAdminOAuthClients from 'hooks/useAdminOAuthClients'; -function AdminApplicationAuthClients(props) { +function AdminApplicationOAuthClients(props) { const { appKey } = props; const formatMessage = useFormatMessage(); - const { data: appAuthClients, isLoading } = useAdminAppAuthClients(appKey); + const { data: appOAuthClients, isLoading } = useAdminOAuthClients(appKey); if (isLoading) return ; - if (!appAuthClients?.data.length) { + if (!appOAuthClients?.data.length) { return ( ); } - const sortedAuthClients = appAuthClients.data.slice().sort((a, b) => { + const sortedOAuthClients = appOAuthClients.data.slice().sort((a, b) => { if (a.id < b.id) { return -1; } @@ -42,7 +43,7 @@ function AdminApplicationAuthClients(props) { return (
- {sortedAuthClients.map((client) => ( + {sortedOAuthClients.map((client) => ( @@ -70,8 +71,13 @@ function AdminApplicationAuthClients(props) { ))} - @@ -79,8 +85,8 @@ function AdminApplicationAuthClients(props) { ); } -AdminApplicationAuthClients.propTypes = { +AdminApplicationOAuthClients.propTypes = { appKey: PropTypes.string.isRequired, }; -export default AdminApplicationAuthClients; +export default AdminApplicationOAuthClients; 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) { - - + + + ({ ...field, @@ -31,13 +31,13 @@ function AdminApplicationUpdateAuthClient(props) { })); const submitHandler = async (values) => { - if (!adminAppAuthClient) { + if (!adminOAuthClient) { return; } const { name, active, ...formattedAuthDefaults } = values; - await updateAppAuthClient({ + await updateOAuthClient({ name, active, formattedAuthDefaults, @@ -64,31 +64,31 @@ function AdminApplicationUpdateAuthClient(props) { const defaultValues = useMemo( () => ({ - name: adminAppAuthClient?.data?.name || '', - active: adminAppAuthClient?.data?.active || false, + name: adminOAuthClient?.data?.name || '', + active: adminOAuthClient?.data?.active || false, ...getAuthFieldsDefaultValues(), }), - [adminAppAuthClient, getAuthFieldsDefaultValues], + [adminOAuthClient, getAuthFieldsDefaultValues], ); return ( - ); } -AdminApplicationUpdateAuthClient.propTypes = { +AdminApplicationUpdateOAuthClient.propTypes = { application: AppPropType.isRequired, onClose: PropTypes.func.isRequired, }; -export default AdminApplicationUpdateAuthClient; +export default AdminApplicationUpdateOAuthClient; diff --git a/packages/web/src/components/AppAuthClientsDialog/index.ee.jsx b/packages/web/src/components/AppAuthClientsDialog/index.ee.jsx deleted file mode 100644 index a9cce58d..00000000 --- a/packages/web/src/components/AppAuthClientsDialog/index.ee.jsx +++ /dev/null @@ -1,53 +0,0 @@ -import PropTypes from 'prop-types'; -import Dialog from '@mui/material/Dialog'; -import DialogTitle from '@mui/material/DialogTitle'; -import List from '@mui/material/List'; -import ListItem from '@mui/material/ListItem'; -import ListItemButton from '@mui/material/ListItemButton'; -import ListItemText from '@mui/material/ListItemText'; -import * as React from 'react'; -import useAppAuthClients from 'hooks/useAppAuthClients'; -import useFormatMessage from 'hooks/useFormatMessage'; - -function AppAuthClientsDialog(props) { - const { appKey, onClientClick, onClose } = props; - const { data: appAuthClients } = useAppAuthClients(appKey); - - const formatMessage = useFormatMessage(); - - React.useEffect( - function autoAuthenticateSingleClient() { - if (appAuthClients?.data.length === 1) { - onClientClick(appAuthClients.data[0].id); - } - }, - [appAuthClients?.data], - ); - - if (!appAuthClients?.data.length || appAuthClients?.data.length === 1) - return ; - - return ( - - {formatMessage('appAuthClientsDialog.title')} - - - {appAuthClients.data.map((appAuthClient) => ( - - onClientClick(appAuthClient.id)}> - - - - ))} - - - ); -} - -AppAuthClientsDialog.propTypes = { - appKey: PropTypes.string.isRequired, - onClientClick: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, -}; - -export default AppAuthClientsDialog; diff --git a/packages/web/src/components/AppConnectionContextMenu/index.jsx b/packages/web/src/components/AppConnectionContextMenu/index.jsx index fb94e4b3..8e7eb318 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,11 +66,11 @@ function ContextMenu(props) { {(allowed) => ( @@ -109,7 +102,6 @@ ContextMenu.propTypes = { PropTypes.func, PropTypes.shape({ current: PropTypes.instanceOf(Element) }), ]), - disableReconnection: PropTypes.bool.isRequired, }; export default ContextMenu; diff --git a/packages/web/src/components/AppConnectionRow/index.jsx b/packages/web/src/components/AppConnectionRow/index.jsx index 9311827b..dc7e85a6 100644 --- a/packages/web/src/components/AppConnectionRow/index.jsx +++ b/packages/web/src/components/AppConnectionRow/index.jsx @@ -30,8 +30,7 @@ const countTranslation = (value) => ( 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 addConnectionWithOAuthClient = { + label: formatMessage( + 'chooseConnectionSubstep.addConnectionWithOAuthClient', + ), + 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 OAuth clients are allowed for connection creation and there is OAuth client if ( - !appConfig?.data || - (!appConfig.data?.disabled && appConfig.data?.customConnectionAllowed) + appConfig.data.useOnlyPredefinedAuthClients === true && + appOAuthClients.data.length > 0 ) { - options.push({ - label: formatMessage('chooseConnectionSubstep.addNewConnection'), - value: ADD_CONNECTION_VALUE, - }); + return options.concat([addConnectionWithOAuthClient]); } - if (appConfig?.data?.connectionAllowed) { - options.push({ - label: formatMessage('chooseConnectionSubstep.addNewSharedConnection'), - value: ADD_SHARED_CONNECTION_VALUE, - }); + // means there is no OAuth client. so we don't show the `addConnectionWithOAuthClient` + if ( + appConfig.data.useOnlyPredefinedAuthClients === true && + appOAuthClients.data.length === 0 + ) { + return options; } - return options; - }, [data, formatMessage, appConfig?.data]); + if (appOAuthClients.data.length === 0) { + return options.concat([addCustomConnection]); + } - const handleClientClick = async (appAuthClientId) => { + return options.concat([addCustomConnection, addConnectionWithOAuthClient]); + }, [data, formatMessage, appConfig, appOAuthClients]); + + const handleClientClick = async (oauthClientId) => { try { const response = await authenticate?.({ - appAuthClientId, + oauthClientId, }); const connectionId = response?.createConnection.id; @@ -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); @@ -270,7 +292,7 @@ function ChooseConnectionSubstep(props) { )} {application && showAddSharedConnectionDialog && ( - setShowAddSharedConnectionDialog(false)} onClientClick={handleClientClick} diff --git a/packages/web/src/components/OAuthClientsDialog/index.ee.jsx b/packages/web/src/components/OAuthClientsDialog/index.ee.jsx new file mode 100644 index 00000000..c8e204cb --- /dev/null +++ b/packages/web/src/components/OAuthClientsDialog/index.ee.jsx @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemText from '@mui/material/ListItemText'; +import * as React from 'react'; +import useOAuthClients from 'hooks/useOAuthClients'; +import useFormatMessage from 'hooks/useFormatMessage'; + +function AppOAuthClientsDialog(props) { + const { appKey, onClientClick, onClose } = props; + const { data: appOAuthClients } = useOAuthClients(appKey); + + const formatMessage = useFormatMessage(); + + if (!appOAuthClients?.data.length) return ; + + return ( + + {formatMessage('appOAuthClientsDialog.title')} + + + {appOAuthClients.data.map((oauthClient) => ( + + onClientClick(oauthClient.id)}> + + + + ))} + + + ); +} + +AppOAuthClientsDialog.propTypes = { + appKey: PropTypes.string.isRequired, + onClientClick: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, +}; + +export default AppOAuthClientsDialog; diff --git a/packages/web/src/components/PermissionCatalogField/PermissionCatalogFieldLoader/index.jsx b/packages/web/src/components/PermissionCatalogField/PermissionCatalogFieldLoader/index.jsx index 50903dbf..e5752321 100644 --- a/packages/web/src/components/PermissionCatalogField/PermissionCatalogFieldLoader/index.jsx +++ b/packages/web/src/components/PermissionCatalogField/PermissionCatalogFieldLoader/index.jsx @@ -39,14 +39,14 @@ const PermissionCatalogFieldLoader = () => { {[...Array(5)].map((action, index) => ( - + ))} - + diff --git a/packages/web/src/components/PermissionCatalogField/index.ee.jsx b/packages/web/src/components/PermissionCatalogField/index.ee.jsx index 24a86d38..1c8d6902 100644 --- a/packages/web/src/components/PermissionCatalogField/index.ee.jsx +++ b/packages/web/src/components/PermissionCatalogField/index.ee.jsx @@ -21,13 +21,15 @@ const PermissionCatalogField = ({ name = 'permissions', disabled = false, syncIsCreator = false, + loading = false, }) => { const { data, isLoading: isPermissionCatalogLoading } = usePermissionCatalog(); const permissionCatalog = data?.data; const [dialogName, setDialogName] = React.useState(); - if (isPermissionCatalogLoading) return ; + if (isPermissionCatalogLoading || loading) + return ; return ( @@ -118,6 +120,7 @@ PermissionCatalogField.propTypes = { name: PropTypes.string, disabled: PropTypes.bool, syncIsCreator: PropTypes.bool, + loading: PropTypes.bool, }; export default PermissionCatalogField; 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/config/urls.js b/packages/web/src/config/urls.js index db0c50d2..820d721c 100644 --- a/packages/web/src/config/urls.js +++ b/packages/web/src/config/urls.js @@ -17,19 +17,19 @@ export const APP_CONNECTIONS = (appKey) => `/app/${appKey}/connections`; export const APP_CONNECTIONS_PATTERN = '/app/:appKey/connections'; export const APP_ADD_CONNECTION = (appKey, shared = false) => `/app/${appKey}/connections/add?shared=${shared}`; -export const APP_ADD_CONNECTION_WITH_AUTH_CLIENT_ID = ( +export const APP_ADD_CONNECTION_WITH_OAUTH_CLIENT_ID = ( appKey, - appAuthClientId, -) => `/app/${appKey}/connections/add?appAuthClientId=${appAuthClientId}`; + oauthClientId, +) => `/app/${appKey}/connections/add?oauthClientId=${oauthClientId}`; export const APP_ADD_CONNECTION_PATTERN = '/app/:appKey/connections/add'; export const APP_RECONNECT_CONNECTION = ( appKey, connectionId, - appAuthClientId, + oauthClientId, ) => { const path = `/app/${appKey}/connections/${connectionId}/reconnect`; - if (appAuthClientId) { - return `${path}?appAuthClientId=${appAuthClientId}`; + if (oauthClientId) { + return `${path}?oauthClientId=${oauthClientId}`; } return path; }; @@ -71,18 +71,18 @@ export const ADMIN_APPS = `${ADMIN_SETTINGS}/apps`; export const ADMIN_APP = (appKey) => `${ADMIN_SETTINGS}/apps/${appKey}`; export const ADMIN_APP_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey`; export const ADMIN_APP_SETTINGS_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey/settings`; -export const ADMIN_APP_AUTH_CLIENTS_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey/auth-clients`; +export const ADMIN_APP_AUTH_CLIENTS_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey/oauth-clients`; export const ADMIN_APP_CONNECTIONS_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey/connections`; export const ADMIN_APP_CONNECTIONS = (appKey) => `${ADMIN_SETTINGS}/apps/${appKey}/connections`; export const ADMIN_APP_SETTINGS = (appKey) => `${ADMIN_SETTINGS}/apps/${appKey}/settings`; export const ADMIN_APP_AUTH_CLIENTS = (appKey) => - `${ADMIN_SETTINGS}/apps/${appKey}/auth-clients`; + `${ADMIN_SETTINGS}/apps/${appKey}/oauth-clients`; export const ADMIN_APP_AUTH_CLIENT = (appKey, id) => - `${ADMIN_SETTINGS}/apps/${appKey}/auth-clients/${id}`; + `${ADMIN_SETTINGS}/apps/${appKey}/oauth-clients/${id}`; export const ADMIN_APP_AUTH_CLIENTS_CREATE = (appKey) => - `${ADMIN_SETTINGS}/apps/${appKey}/auth-clients/create`; + `${ADMIN_SETTINGS}/apps/${appKey}/oauth-clients/create`; export const DASHBOARD = FLOWS; // External links and paths diff --git a/packages/web/src/hooks/useAdminAppAuthClient.ee.js b/packages/web/src/hooks/useAdminAppAuthClient.ee.js deleted file mode 100644 index 694ba03b..00000000 --- a/packages/web/src/hooks/useAdminAppAuthClient.ee.js +++ /dev/null @@ -1,19 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; - -import api from 'helpers/api'; - -export default function useAdminAppAuthClient(appKey, id) { - const query = useQuery({ - queryKey: ['admin', 'apps', appKey, 'authClients', id], - queryFn: async ({ signal }) => { - const { data } = await api.get(`/v1/admin/apps/${appKey}/auth-clients/${id}`, { - signal, - }); - - return data; - }, - enabled: !!appKey && !!id, - }); - - return query; -} diff --git a/packages/web/src/hooks/useAdminCreateAppAuthClient.ee.js b/packages/web/src/hooks/useAdminCreateOAuthClient.ee.js similarity index 57% rename from packages/web/src/hooks/useAdminCreateAppAuthClient.ee.js rename to packages/web/src/hooks/useAdminCreateOAuthClient.ee.js index 37a39045..4c0e9e80 100644 --- a/packages/web/src/hooks/useAdminCreateAppAuthClient.ee.js +++ b/packages/web/src/hooks/useAdminCreateOAuthClient.ee.js @@ -1,20 +1,23 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import api from 'helpers/api'; -export default function useAdminCreateAppAuthClient(appKey) { +export default function useAdminCreateOAuthClient(appKey) { const queryClient = useQueryClient(); const query = useMutation({ mutationFn: async (payload) => { - const { data } = await api.post(`/v1/admin/apps/${appKey}/auth-clients`, payload); + const { data } = await api.post( + `/v1/admin/apps/${appKey}/oauth-clients`, + payload, + ); return data; }, onSuccess: () => { queryClient.invalidateQueries({ - queryKey: ['admin', 'apps', appKey, 'authClients'], + queryKey: ['admin', 'apps', appKey, 'oauthClients'], }); - } + }, }); return query; diff --git a/packages/web/src/hooks/useAdminOAuthClient.ee.js b/packages/web/src/hooks/useAdminOAuthClient.ee.js new file mode 100644 index 00000000..a4482f5a --- /dev/null +++ b/packages/web/src/hooks/useAdminOAuthClient.ee.js @@ -0,0 +1,22 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useAdminOAuthClient(appKey, id) { + const query = useQuery({ + queryKey: ['admin', 'apps', appKey, 'oauthClients', id], + queryFn: async ({ signal }) => { + const { data } = await api.get( + `/v1/admin/apps/${appKey}/oauth-clients/${id}`, + { + signal, + }, + ); + + return data; + }, + enabled: !!appKey && !!id, + }); + + return query; +} diff --git a/packages/web/src/hooks/useAdminAppAuthClients.js b/packages/web/src/hooks/useAdminOAuthClients.js similarity index 56% rename from packages/web/src/hooks/useAdminAppAuthClients.js rename to packages/web/src/hooks/useAdminOAuthClients.js index a4bc4bc8..d942a18d 100644 --- a/packages/web/src/hooks/useAdminAppAuthClients.js +++ b/packages/web/src/hooks/useAdminOAuthClients.js @@ -1,11 +1,11 @@ import { useQuery } from '@tanstack/react-query'; import api from 'helpers/api'; -export default function useAdminAppAuthClients(appKey) { +export default function useAdminOAuthClients(appKey) { const query = useQuery({ - queryKey: ['admin', 'apps', appKey, 'authClients'], + queryKey: ['admin', 'apps', appKey, 'oauthClients'], queryFn: async ({ signal }) => { - const { data } = await api.get(`/v1/admin/apps/${appKey}/auth-clients`, { + const { data } = await api.get(`/v1/admin/apps/${appKey}/oauth-clients`, { signal, }); return data; diff --git a/packages/web/src/hooks/useAdminUpdateAppAuthClient.ee.js b/packages/web/src/hooks/useAdminUpdateOAuthClient.ee.js similarity index 57% rename from packages/web/src/hooks/useAdminUpdateAppAuthClient.ee.js rename to packages/web/src/hooks/useAdminUpdateOAuthClient.ee.js index a1aa078e..bc397eb8 100644 --- a/packages/web/src/hooks/useAdminUpdateAppAuthClient.ee.js +++ b/packages/web/src/hooks/useAdminUpdateOAuthClient.ee.js @@ -1,13 +1,13 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import api from 'helpers/api'; -export default function useAdminUpdateAppAuthClient(appKey, id) { +export default function useAdminUpdateOAuthClient(appKey, id) { const queryClient = useQueryClient(); - const query = useMutation({ + const mutation = useMutation({ mutationFn: async (payload) => { const { data } = await api.patch( - `/v1/admin/apps/${appKey}/auth-clients/${id}`, + `/v1/admin/apps/${appKey}/oauth-clients/${id}`, payload, ); @@ -15,14 +15,14 @@ export default function useAdminUpdateAppAuthClient(appKey, id) { }, onSuccess: () => { queryClient.invalidateQueries({ - queryKey: ['admin', 'apps', appKey, 'authClients', id], + queryKey: ['admin', 'apps', appKey, 'oauthClients', id], }); queryClient.invalidateQueries({ - queryKey: ['admin', 'apps', appKey, 'authClients'], + queryKey: ['admin', 'apps', appKey, 'oauthClients'], }); }, }); - return query; + return mutation; } diff --git a/packages/web/src/hooks/useAuthenticateApp.ee.js b/packages/web/src/hooks/useAuthenticateApp.ee.js index b0b99b09..061b8e88 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) { @@ -30,18 +31,20 @@ function getSteps(auth, hasConnection, useShared) { } export default function useAuthenticateApp(payload) { - const { appKey, appAuthClientId, connectionId, useShared = false } = payload; + const { appKey, oauthClientId, connectionId, useShared = false } = payload; const { data: auth } = useAppAuth(appKey); const queryClient = useQueryClient(); const { mutateAsync: createConnection } = useCreateConnection(appKey); 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; @@ -52,12 +55,11 @@ export default function useAuthenticateApp(payload) { const response = { key: appKey, - appAuthClientId: appAuthClientId || payload.appAuthClientId, + oauthClientId: oauthClientId || payload.oauthClientId, connectionId, 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, + oauthClientId, connectionId, queryClient, - formatMessage, createConnection, createConnectionAuthUrl, updateConnection, @@ -140,6 +143,24 @@ export default function useAuthenticateApp(payload) { verifyConnection, ]); + useWhatChanged( + [ + steps, + appKey, + oauthClientId, + connectionId, + queryClient, + createConnection, + createConnectionAuthUrl, + updateConnection, + resetConnection, + verifyConnection, + ], + 'steps, appKey, oauthClientId, 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..9c09f8b6 100644 --- a/packages/web/src/hooks/useCreateConnection.js +++ b/packages/web/src/hooks/useCreateConnection.js @@ -3,10 +3,10 @@ import { useMutation } from '@tanstack/react-query'; import api from 'helpers/api'; export default function useCreateConnection(appKey) { - const query = useMutation({ - mutationFn: async ({ appAuthClientId, formattedData }) => { + const mutation = useMutation({ + mutationFn: async ({ oauthClientId, formattedData }) => { const { data } = await api.post(`/v1/apps/${appKey}/connections`, { - appAuthClientId, + oauthClientId, formattedData, }); @@ -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/hooks/useAppAuthClients.js b/packages/web/src/hooks/useOAuthClients.js similarity index 58% rename from packages/web/src/hooks/useAppAuthClients.js rename to packages/web/src/hooks/useOAuthClients.js index 1524c3ac..057fb481 100644 --- a/packages/web/src/hooks/useAppAuthClients.js +++ b/packages/web/src/hooks/useOAuthClients.js @@ -1,11 +1,11 @@ import { useQuery } from '@tanstack/react-query'; import api from 'helpers/api'; -export default function useAppAuthClients(appKey) { +export default function useOAuthClients(appKey) { const query = useQuery({ - queryKey: ['apps', appKey, 'auth-clients'], + queryKey: ['apps', appKey, 'oauth-clients'], queryFn: async ({ signal }) => { - const { data } = await api.get(`/v1/apps/${appKey}/auth-clients`, { + const { data } = await api.get(`/v1/apps/${appKey}/oauth-clients`, { signal, }); return data; diff --git a/packages/web/src/hooks/useUpdateConnection.js b/packages/web/src/hooks/useUpdateConnection.js index 37d87bc4..c48147ff 100644 --- a/packages/web/src/hooks/useUpdateConnection.js +++ b/packages/web/src/hooks/useUpdateConnection.js @@ -4,10 +4,10 @@ import api from 'helpers/api'; export default function useUpdateConnection() { const query = useMutation({ - mutationFn: async ({ connectionId, formattedData, appAuthClientId }) => { + mutationFn: async ({ connectionId, formattedData, oauthClientId }) => { const { data } = await api.patch(`/v1/connections/${connectionId}`, { formattedData, - appAuthClientId, + oauthClientId, }); return data; diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index b121f5e2..a4c0dd2b 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.addConnectionWithOAuthClient": "Add connection with OAuth 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.addConnectionWithOAuthClient": "Add connection with OAuth client", "chooseConnectionSubstep.chooseConnection": "Choose connection", "flow.createdAt": "created {datetime}", "flow.updatedAt": "updated {datetime}", @@ -258,7 +258,7 @@ "permissionSettings.cancel": "Cancel", "permissionSettings.apply": "Apply", "permissionSettings.title": "Conditions", - "appAuthClientsDialog.title": "Choose your authentication client", + "appOAuthClientsDialog.title": "Choose your authentication client", "userInterfacePage.title": "User Interface", "userInterfacePage.successfullyUpdated": "User interface has been updated.", "userInterfacePage.titleFieldLabel": "Title", @@ -290,22 +290,22 @@ "roleMappingsForm.successfullySaved": "Role mappings have been saved.", "adminApps.title": "Apps", "adminApps.connections": "Connections", - "adminApps.authClients": "Auth clients", + "adminApps.oauthClients": "OAuth clients", "adminApps.settings": "Settings", - "adminAppsSettings.customConnectionAllowed": "Allow custom connection", + "adminAppsSettings.useOnlyPredefinedAuthClients": "Use only predefined OAuth clients", "adminAppsSettings.shared": "Shared", "adminAppsSettings.disabled": "Disabled", "adminAppsSettings.save": "Save", "adminAppsSettings.successfullySaved": "Settings have been saved.", - "adminAppsAuthClients.noAuthClients": "You don't have any auth clients yet.", - "adminAppsAuthClients.statusActive": "Active", - "adminAppsAuthClients.statusInactive": "Inactive", - "createAuthClient.button": "Create auth client", - "createAuthClient.title": "Create auth client", - "authClient.buttonSubmit": "Submit", - "authClient.inputName": "Name", - "authClient.inputActive": "Active", - "updateAuthClient.title": "Update auth client", + "adminAppsOAuthClients.noOauthClients": "You don't have any OAuth clients yet.", + "adminAppsOAuthClients.statusActive": "Active", + "adminAppsOAuthClients.statusInactive": "Inactive", + "createOAuthClient.button": "Create OAuth client", + "createOAuthClient.title": "Create OAuth client", + "oauthClient.buttonSubmit": "Submit", + "oauthClient.inputName": "Name", + "oauthClient.inputActive": "Active", + "updateOAuthClient.title": "Update OAuth client", "notFoundPage.title": "We can't seem to find a page you're looking for.", "notFoundPage.button": "Back to home page" } diff --git a/packages/web/src/pages/AdminApplication/index.jsx b/packages/web/src/pages/AdminApplication/index.jsx index 44e73085..a7e24de3 100644 --- a/packages/web/src/pages/AdminApplication/index.jsx +++ b/packages/web/src/pages/AdminApplication/index.jsx @@ -21,9 +21,9 @@ import AppIcon from 'components/AppIcon'; import Container from 'components/Container'; import PageTitle from 'components/PageTitle'; import AdminApplicationSettings from 'components/AdminApplicationSettings'; -import AdminApplicationAuthClients from 'components/AdminApplicationAuthClients'; -import AdminApplicationCreateAuthClient from 'components/AdminApplicationCreateAuthClient'; -import AdminApplicationUpdateAuthClient from 'components/AdminApplicationUpdateAuthClient'; +import AdminApplicationOAuthClients from 'components/AdminApplicationOAuthClients'; +import AdminApplicationCreateOAuthClient from 'components/AdminApplicationCreateOAuthClient'; +import AdminApplicationUpdateOAuthClient from 'components/AdminApplicationUpdateOAuthClient'; import useApp from 'hooks/useApp'; export default function AdminApplication() { @@ -39,7 +39,7 @@ export default function AdminApplication() { path: URLS.ADMIN_APP_SETTINGS_PATTERN, end: false, }); - const authClientsPathMatch = useMatch({ + const oauthClientsPathMatch = useMatch({ path: URLS.ADMIN_APP_AUTH_CLIENTS_PATTERN, end: false, }); @@ -49,7 +49,7 @@ export default function AdminApplication() { const app = data?.data || {}; - const goToAuthClientsPage = () => navigate('auth-clients'); + const goToAuthClientsPage = () => navigate('oauth-clients'); if (loading) return null; @@ -77,7 +77,7 @@ export default function AdminApplication() { value={ settingsPathMatch?.pattern?.path || connectionsPathMatch?.pattern?.path || - authClientsPathMatch?.pattern?.path + oauthClientsPathMatch?.pattern?.path } > - @@ -108,12 +102,8 @@ export default function AdminApplication() { element={} /> } - /> - App connections
} + path={`/oauth-clients/*`} + element={} /> diff --git a/packages/web/src/pages/Application/index.jsx b/packages/web/src/pages/Application/index.jsx index 74794784..0ba6ab62 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 useOAuthClients from 'hooks/useOAuthClients'; import Can from 'components/Can'; import { AppPropType } from 'propTypes/propTypes'; @@ -61,47 +61,59 @@ 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: appOAuthClients } = useOAuthClients(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') || + 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 addConnectionWithOAuthClient = { + label: formatMessage('app.addConnectionWithOAuthClient'), + key: 'addConnectionWithOAuthClient', + 'data-test': 'add-connection-with-auth-client-button', + to: URLS.APP_ADD_CONNECTION(appKey, true), + disabled: + !currentUserAbility.can('create', 'Connection') || + appOAuthClients?.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 OAuth clients are allowed for connection creation + if (appConfig?.data?.useOnlyPredefinedAuthClients === true) { + return [addConnectionWithOAuthClient]; + } + + // means there is no OAuth client. so we don't show the `addConnectionWithOAuthClient` + if (appOAuthClients?.data?.length === 0) { + return [addCustomConnection]; + } + + return [addCustomConnection, addConnectionWithOAuthClient]; + }, [appKey, appConfig, appOAuthClients, 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/pages/Authentication/RoleMappings.jsx b/packages/web/src/pages/Authentication/RoleMappings.jsx index 66a14c38..4440177b 100644 --- a/packages/web/src/pages/Authentication/RoleMappings.jsx +++ b/packages/web/src/pages/Authentication/RoleMappings.jsx @@ -66,8 +66,8 @@ function RoleMappings({ provider, providerLoading }) { const enqueueSnackbar = useEnqueueSnackbar(); const { - mutateAsync: updateSamlAuthProvidersRoleMappings, - isPending: isUpdateSamlAuthProvidersRoleMappingsPending, + mutateAsync: updateRoleMappings, + isPending: isUpdateRoleMappingsPending, } = useAdminUpdateSamlAuthProviderRoleMappings(provider?.id); const { data, isLoading: isAdminSamlAuthProviderRoleMappingsLoading } = @@ -79,7 +79,7 @@ function RoleMappings({ provider, providerLoading }) { const handleRoleMappingsUpdate = async (values) => { try { if (provider?.id) { - await updateSamlAuthProvidersRoleMappings( + await updateRoleMappings( values.roleMappings.map(({ roleId, remoteRoleName }) => ({ roleId, remoteRoleName, @@ -148,7 +148,7 @@ function RoleMappings({ provider, providerLoading }) { variant="contained" color="primary" sx={{ boxShadow: 2 }} - loading={isUpdateSamlAuthProvidersRoleMappingsPending} + loading={isUpdateRoleMappingsPending} > {formatMessage('roleMappingsForm.save')} diff --git a/packages/web/src/pages/CreateRole/index.ee.jsx b/packages/web/src/pages/CreateRole/index.ee.jsx index b5ff22c9..99a66901 100644 --- a/packages/web/src/pages/CreateRole/index.ee.jsx +++ b/packages/web/src/pages/CreateRole/index.ee.jsx @@ -25,7 +25,8 @@ export default function CreateRole() { const enqueueSnackbar = useEnqueueSnackbar(); const { mutateAsync: createRole, isPending: isCreateRolePending } = useAdminCreateRole(); - const { data: permissionCatalogData } = usePermissionCatalog(); + const { data: permissionCatalogData, isLoading: isPermissionCatalogLoading } = + usePermissionCatalog(); const defaultValues = React.useMemo( () => ({ @@ -91,6 +92,7 @@ export default function CreateRole() { label={formatMessage('roleForm.name')} fullWidth data-test="name-input" + disabled={isPermissionCatalogLoading} /> diff --git a/packages/web/src/pages/EditRole/index.ee.jsx b/packages/web/src/pages/EditRole/index.ee.jsx index 1ed4881c..92573e1e 100644 --- a/packages/web/src/pages/EditRole/index.ee.jsx +++ b/packages/web/src/pages/EditRole/index.ee.jsx @@ -1,6 +1,5 @@ import LoadingButton from '@mui/lab/LoadingButton'; import Grid from '@mui/material/Grid'; -import Skeleton from '@mui/material/Skeleton'; import Stack from '@mui/material/Stack'; import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import * as React from 'react'; @@ -30,7 +29,8 @@ export default function EditRole() { const { data: roleData, isLoading: isRoleLoading } = useRole({ roleId }); const { mutateAsync: updateRole, isPending: isUpdateRolePending } = useAdminUpdateRole(roleId); - const { data: permissionCatalogData } = usePermissionCatalog(); + const { data: permissionCatalogData, isLoading: isPermissionCatalogLoading } = + usePermissionCatalog(); const role = roleData?.data; const permissionCatalog = permissionCatalogData?.data; const enqueueSnackbar = useEnqueueSnackbar(); @@ -84,36 +84,30 @@ export default function EditRole() {
- {isRoleLoading && ( - <> - - - - )} - {!isRoleLoading && role && ( - <> - - - - - )} + +