diff --git a/packages/backend/src/controllers/api/v1/apps/get-connections.js b/packages/backend/src/controllers/api/v1/apps/get-connections.js new file mode 100644 index 00000000..1f5a91ad --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-connections.js @@ -0,0 +1,24 @@ +import { renderObject } from '../../../../helpers/renderer.js'; +import App from '../../../../models/app.js'; + +export default async (request, response) => { + const app = await App.findOneByKey(request.params.appKey); + + const connections = await request.currentUser.authorizedConnections + .clone() + .select('connections.*') + .withGraphFetched({ + appConfig: true, + appAuthClient: true, + }) + .fullOuterJoinRelated('steps') + .where({ + 'connections.key': app.key, + 'connections.draft': false, + }) + .countDistinct('steps.flow_id as flowCount') + .groupBy('connections.id') + .orderBy('created_at', 'desc'); + + renderObject(response, connections); +}; 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 new file mode 100644 index 00000000..603ebc04 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-connections.test.js @@ -0,0 +1,101 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +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 { createConnection } from '../../../../../test/factories/connection.js'; +import { createPermission } from '../../../../../test/factories/permission.js'; +import getConnectionsMock from '../../../../../test/mocks/rest/api/v1/apps/get-connections.js'; + +describe('GET /api/v1/apps/:appKey/connections', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = createAuthTokenByUserId(currentUser.id); + }); + + it('should return the connections data of specified app for current user', async () => { + const currentUserConnectionOne = await createConnection({ + userId: currentUser.id, + key: 'deepl', + draft: false, + }); + + const currentUserConnectionTwo = await createConnection({ + userId: currentUser.id, + key: 'deepl', + draft: false, + }); + + await createPermission({ + action: 'read', + subject: 'Connection', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .get('/api/v1/apps/deepl/connections') + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getConnectionsMock([ + currentUserConnectionTwo, + currentUserConnectionOne, + ]); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return the connections data of specified app for another user', async () => { + const anotherUser = await createUser(); + + const anotherUserConnectionOne = await createConnection({ + userId: anotherUser.id, + key: 'deepl', + draft: false, + }); + + const anotherUserConnectionTwo = await createConnection({ + userId: anotherUser.id, + key: 'deepl', + draft: false, + }); + + await createPermission({ + action: 'read', + subject: 'Connection', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .get('/api/v1/apps/deepl/connections') + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getConnectionsMock([ + anotherUserConnectionTwo, + anotherUserConnectionOne, + ]); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for invalid app key', async () => { + await createPermission({ + action: 'read', + subject: 'Connection', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await request(app) + .get('/api/v1/apps/invalid-app-key/connections') + .set('Authorization', token) + .expect(404); + }); +}); diff --git a/packages/backend/src/helpers/authorization.js b/packages/backend/src/helpers/authorization.js index c3abf6a8..85388a06 100644 --- a/packages/backend/src/helpers/authorization.js +++ b/packages/backend/src/helpers/authorization.js @@ -35,6 +35,10 @@ const authorizationList = { action: 'read', subject: 'Flow', }, + 'GET /api/v1/apps/:appKey/connections': { + action: 'read', + subject: 'Connection', + }, 'GET /api/v1/executions/:executionId': { action: 'read', subject: 'Execution', diff --git a/packages/backend/src/helpers/error-handler.js b/packages/backend/src/helpers/error-handler.js index 3a9fafd9..1520abaf 100644 --- a/packages/backend/src/helpers/error-handler.js +++ b/packages/backend/src/helpers/error-handler.js @@ -37,7 +37,7 @@ const errorHandler = (error, request, response, next) => { const notFoundAppError = (error) => { return ( - error.message.includes('An application with the') || + error.message.includes('An application with the') && error.message.includes("key couldn't be found.") ); }; diff --git a/packages/backend/src/models/user.js b/packages/backend/src/models/user.js index f67e1a63..29ed5741 100644 --- a/packages/backend/src/models/user.js +++ b/packages/backend/src/models/user.js @@ -155,6 +155,13 @@ class User extends Base { return conditions.isCreator ? this.$relatedQuery('steps') : Step.query(); } + get authorizedConnections() { + const conditions = this.can('read', 'Connection'); + return conditions.isCreator + ? this.$relatedQuery('connections') + : Connection.query(); + } + get authorizedExecutions() { const conditions = this.can('read', 'Execution'); return conditions.isCreator diff --git a/packages/backend/src/routes/api/v1/apps.js b/packages/backend/src/routes/api/v1/apps.js index ac524cbd..34971966 100644 --- a/packages/backend/src/routes/api/v1/apps.js +++ b/packages/backend/src/routes/api/v1/apps.js @@ -6,6 +6,7 @@ import { checkIsEnterprise } from '../../../helpers/check-is-enterprise.js'; import getAppAction from '../../../controllers/api/v1/apps/get-app.js'; 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'; @@ -21,6 +22,13 @@ router.get('/', authenticateUser, asyncHandler(getAppsAction)); router.get('/:appKey', authenticateUser, asyncHandler(getAppAction)); router.get('/:appKey/auth', authenticateUser, asyncHandler(getAuthAction)); +router.get( + '/:appKey/connections', + authenticateUser, + authorizeUser, + asyncHandler(getConnectionsAction) +); + router.get( '/:appKey/config', authenticateUser, 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 new file mode 100644 index 00000000..a6242e80 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/apps/get-connections.js @@ -0,0 +1,25 @@ +const getConnectionsMock = (connections) => { + return { + data: connections.map((connection) => ({ + id: connection.id, + key: connection.key, + reconnectable: connection.reconnectable, + verified: connection.verified, + appAuthClientId: connection.appAuthClientId, + formattedData: { + screenName: connection.formattedData.screenName, + }, + createdAt: connection.createdAt.getTime(), + updatedAt: connection.updatedAt.getTime(), + })), + meta: { + count: connections.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'Connection', + }, + }; +}; + +export default getConnectionsMock;