From 7e35f544ebd331b196e2d60521f0b40452a47bd5 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Sun, 25 Feb 2024 23:01:55 +0100 Subject: [PATCH 1/4] feat: Introduce step serializer --- packages/backend/src/serializers/step.js | 15 +++++++++++ packages/backend/src/serializers/step.test.js | 27 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 packages/backend/src/serializers/step.js create mode 100644 packages/backend/src/serializers/step.test.js diff --git a/packages/backend/src/serializers/step.js b/packages/backend/src/serializers/step.js new file mode 100644 index 00000000..8f40856f --- /dev/null +++ b/packages/backend/src/serializers/step.js @@ -0,0 +1,15 @@ +const stepSerializer = (step) => { + return { + id: step.id, + type: step.type, + key: step.key, + appKey: step.appKey, + iconUrl: step.iconUrl, + webhookUrl: step.webhookUrl, + status: step.status, + position: step.position, + parameters: step.parameters, + }; +}; + +export default stepSerializer; diff --git a/packages/backend/src/serializers/step.test.js b/packages/backend/src/serializers/step.test.js new file mode 100644 index 00000000..4ddf918c --- /dev/null +++ b/packages/backend/src/serializers/step.test.js @@ -0,0 +1,27 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { createStep } from '../../test/factories/step'; +import stepSerializer from './step'; + +describe('stepSerializer', () => { + let step; + + beforeEach(async () => { + step = await createStep(); + }); + + it('should return step data', async () => { + const expectedPayload = { + id: step.id, + type: step.type, + key: step.key, + appKey: step.appKey, + iconUrl: step.iconUrl, + webhookUrl: step.webhookUrl, + status: step.status, + position: step.position, + parameters: step.parameters, + }; + + expect(stepSerializer(step)).toEqual(expectedPayload); + }); +}); From 74fbc937a11f8a31b4c944834e19814019c07bfb Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Sun, 25 Feb 2024 23:31:22 +0100 Subject: [PATCH 2/4] feat: Introduce flow serializer --- packages/backend/src/serializers/flow.js | 18 ++++++++ packages/backend/src/serializers/flow.test.js | 44 +++++++++++++++++++ packages/backend/src/serializers/index.js | 4 ++ 3 files changed, 66 insertions(+) create mode 100644 packages/backend/src/serializers/flow.js create mode 100644 packages/backend/src/serializers/flow.test.js diff --git a/packages/backend/src/serializers/flow.js b/packages/backend/src/serializers/flow.js new file mode 100644 index 00000000..b1b131cf --- /dev/null +++ b/packages/backend/src/serializers/flow.js @@ -0,0 +1,18 @@ +import stepSerializer from './step.js'; + +const flowSerializer = (flow) => { + let flowData = { + id: flow.id, + name: flow.name, + active: flow.active, + status: flow.status, + }; + + if (flow.steps) { + flowData.steps = flow.steps.map((step) => stepSerializer(step)); + } + + return flowData; +}; + +export default flowSerializer; diff --git a/packages/backend/src/serializers/flow.test.js b/packages/backend/src/serializers/flow.test.js new file mode 100644 index 00000000..299d2451 --- /dev/null +++ b/packages/backend/src/serializers/flow.test.js @@ -0,0 +1,44 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { createFlow } from '../../test/factories/flow'; +import flowSerializer from './flow'; +import stepSerializer from './step'; +import { createStep } from '../../test/factories/step'; + +describe('flowSerializer', () => { + let flow, stepOne, stepTwo; + + beforeEach(async () => { + flow = await createFlow(); + + stepOne = await createStep({ + flowId: flow.id, + type: 'trigger', + }); + + stepTwo = await createStep({ + flowId: flow.id, + type: 'action', + }); + }); + + it('should return flow data', async () => { + const expectedPayload = { + id: flow.id, + name: flow.name, + active: flow.active, + status: flow.status, + }; + + expect(flowSerializer(flow)).toEqual(expectedPayload); + }); + + it('should return flow data with the steps', async () => { + flow.steps = [stepOne, stepTwo]; + + const expectedPayload = { + steps: [stepSerializer(stepOne), stepSerializer(stepTwo)], + }; + + expect(flowSerializer(flow)).toMatchObject(expectedPayload); + }); +}); diff --git a/packages/backend/src/serializers/index.js b/packages/backend/src/serializers/index.js index c6c48ba5..2f766949 100644 --- a/packages/backend/src/serializers/index.js +++ b/packages/backend/src/serializers/index.js @@ -3,6 +3,8 @@ import roleSerializer from './role.js'; import permissionSerializer from './permission.js'; import samlAuthProviderSerializer from './saml-auth-provider.ee.js'; import appAuthClientSerializer from './app-auth-client.js'; +import flowSerializer from './flow.js'; +import stepSerializer from './step.js'; const serializers = { User: userSerializer, @@ -10,6 +12,8 @@ const serializers = { Permission: permissionSerializer, SamlAuthProvider: samlAuthProviderSerializer, AppAuthClient: appAuthClientSerializer, + Flow: flowSerializer, + Step: stepSerializer, }; export default serializers; From 5aad68ec6218c4671e2846a33f9bd8b889ee2dc0 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Sun, 25 Feb 2024 23:34:41 +0100 Subject: [PATCH 3/4] test: Use nested serializers explicitly for serializer tests --- packages/backend/src/serializers/role.test.js | 6 +++++- packages/backend/src/serializers/user.test.js | 9 +++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/serializers/role.test.js b/packages/backend/src/serializers/role.test.js index 900ad9c2..85f239ec 100644 --- a/packages/backend/src/serializers/role.test.js +++ b/packages/backend/src/serializers/role.test.js @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { createRole } from '../../test/factories/role'; import roleSerializer from './role'; +import permissionSerializer from './permission'; import { createPermission } from '../../test/factories/permission'; describe('roleSerializer', () => { @@ -40,7 +41,10 @@ describe('roleSerializer', () => { role.permissions = [permissionOne, permissionTwo]; const expectedPayload = { - permissions: [permissionOne, permissionTwo], + permissions: [ + permissionSerializer(permissionOne), + permissionSerializer(permissionTwo), + ], }; expect(roleSerializer(role)).toMatchObject(expectedPayload); diff --git a/packages/backend/src/serializers/user.test.js b/packages/backend/src/serializers/user.test.js index 07f6c8cd..8369bb60 100644 --- a/packages/backend/src/serializers/user.test.js +++ b/packages/backend/src/serializers/user.test.js @@ -4,6 +4,8 @@ import appConfig from '../config/app'; import { createUser } from '../../test/factories/user'; import { createPermission } from '../../test/factories/permission'; import userSerializer from './user'; +import roleSerializer from './role'; +import permissionSerializer from './permission'; describe('userSerializer', () => { let user, role, permissionOne, permissionTwo; @@ -43,7 +45,7 @@ describe('userSerializer', () => { user.role = role; const expectedPayload = { - role, + role: roleSerializer(role), }; expect(userSerializer(user)).toMatchObject(expectedPayload); @@ -53,7 +55,10 @@ describe('userSerializer', () => { user.permissions = [permissionOne, permissionTwo]; const expectedPayload = { - permissions: [permissionOne, permissionTwo], + permissions: [ + permissionSerializer(permissionOne), + permissionSerializer(permissionTwo), + ], }; expect(userSerializer(user)).toMatchObject(expectedPayload); From b93b465f09e6b74263a79ee80f3148c3a80fa76d Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Mon, 26 Feb 2024 00:52:02 +0100 Subject: [PATCH 4/4] feat: Implement get flow API endpoint --- .../src/controllers/api/v1/flows/get-flow.js | 11 +++ .../controllers/api/v1/flows/get-flow.test.js | 71 +++++++++++++++++++ packages/backend/src/helpers/authorization.js | 4 ++ packages/backend/src/models/user.js | 5 ++ packages/backend/src/routes/api/v1/flows.js | 10 +++ packages/backend/src/routes/index.js | 2 + .../test/mocks/rest/api/v1/flows/get-flow.js | 32 +++++++++ 7 files changed, 135 insertions(+) create mode 100644 packages/backend/src/controllers/api/v1/flows/get-flow.js create mode 100644 packages/backend/src/controllers/api/v1/flows/get-flow.test.js create mode 100644 packages/backend/src/routes/api/v1/flows.js create mode 100644 packages/backend/test/mocks/rest/api/v1/flows/get-flow.js diff --git a/packages/backend/src/controllers/api/v1/flows/get-flow.js b/packages/backend/src/controllers/api/v1/flows/get-flow.js new file mode 100644 index 00000000..f7ab107e --- /dev/null +++ b/packages/backend/src/controllers/api/v1/flows/get-flow.js @@ -0,0 +1,11 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const flow = await request.currentUser.authorizedFlows + .withGraphJoined({ steps: true }) + .orderBy('steps.position', 'asc') + .findOne({ 'flows.id': request.params.flowId }) + .throwIfNotFound(); + + renderObject(response, flow); +}; diff --git a/packages/backend/src/controllers/api/v1/flows/get-flow.test.js b/packages/backend/src/controllers/api/v1/flows/get-flow.test.js new file mode 100644 index 00000000..91bafffb --- /dev/null +++ b/packages/backend/src/controllers/api/v1/flows/get-flow.test.js @@ -0,0 +1,71 @@ +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'; +import { createUser } from '../../../../../test/factories/user'; +import { createFlow } from '../../../../../test/factories/flow'; +import { createStep } from '../../../../../test/factories/step'; +import { createPermission } from '../../../../../test/factories/permission'; +import getFlowMock from '../../../../../test/mocks/rest/api/v1/flows/get-flow'; + +describe('GET /api/v1/flows/:flowId', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = createAuthTokenByUserId(currentUser.id); + }); + + it('should return the flow data of current user', async () => { + const currentUserflow = await createFlow({ userId: currentUser.id }); + const triggerStep = await createStep({ flowId: currentUserflow.id }); + const actionStep = await createStep({ flowId: currentUserflow.id }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .get(`/api/v1/flows/${currentUserflow.id}`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getFlowMock(currentUserflow, [ + triggerStep, + actionStep, + ]); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return the flow data of another user', async () => { + const anotherUser = await createUser(); + const anotherUserFlow = await createFlow({ userId: anotherUser.id }); + const triggerStep = await createStep({ flowId: anotherUserFlow.id }); + const actionStep = await createStep({ flowId: anotherUserFlow.id }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .get(`/api/v1/flows/${anotherUserFlow.id}`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getFlowMock(anotherUserFlow, [ + triggerStep, + actionStep, + ]); + + expect(response.body).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/helpers/authorization.js b/packages/backend/src/helpers/authorization.js index 8c20c4f4..63ce286c 100644 --- a/packages/backend/src/helpers/authorization.js +++ b/packages/backend/src/helpers/authorization.js @@ -7,6 +7,10 @@ const authorizationList = { action: 'read', subject: 'User', }, + 'GET /api/v1/flows/:flowId': { + action: 'read', + subject: 'Flow', + }, }; export const authorizeUser = async (request, response, next) => { diff --git a/packages/backend/src/models/user.js b/packages/backend/src/models/user.js index 8a0d5a69..0724185e 100644 --- a/packages/backend/src/models/user.js +++ b/packages/backend/src/models/user.js @@ -144,6 +144,11 @@ class User extends Base { }, }); + get authorizedFlows() { + const conditions = this.can('read', 'Flow'); + return conditions.isCreator ? this.$relatedQuery('flows') : Flow.query(); + } + login(password) { return bcrypt.compare(password, this.password); } diff --git a/packages/backend/src/routes/api/v1/flows.js b/packages/backend/src/routes/api/v1/flows.js new file mode 100644 index 00000000..8e51ad44 --- /dev/null +++ b/packages/backend/src/routes/api/v1/flows.js @@ -0,0 +1,10 @@ +import { Router } from 'express'; +import { authenticateUser } from '../../../helpers/authentication.js'; +import { authorizeUser } from '../../../helpers/authorization.js'; +import getFlowAction from '../../../controllers/api/v1/flows/get-flow.js'; + +const router = Router(); + +router.get('/:flowId', authenticateUser, authorizeUser, getFlowAction); + +export default router; diff --git a/packages/backend/src/routes/index.js b/packages/backend/src/routes/index.js index 910ef7ec..996dfc99 100644 --- a/packages/backend/src/routes/index.js +++ b/packages/backend/src/routes/index.js @@ -7,6 +7,7 @@ import automatischRouter from './api/v1/automatisch.js'; import usersRouter from './api/v1/users.js'; import paymentRouter from './api/v1/payment.ee.js'; import appAuthClientsRouter from './api/v1/app-auth-clients.js'; +import flowsRouter from './api/v1/flows.js'; import samlAuthProvidersRouter from './api/v1/admin/saml-auth-providers.ee.js'; import rolesRouter from './api/v1/admin/roles.ee.js'; import permissionsRouter from './api/v1/admin/permissions.ee.js'; @@ -23,6 +24,7 @@ router.use('/api/v1/automatisch', automatischRouter); router.use('/api/v1/users', usersRouter); router.use('/api/v1/payment', paymentRouter); router.use('/api/v1/app-auth-clients', appAuthClientsRouter); +router.use('/api/v1/flows', flowsRouter); router.use('/api/v1/admin/saml-auth-providers', samlAuthProvidersRouter); router.use('/api/v1/admin/roles', rolesRouter); router.use('/api/v1/admin/permissions', permissionsRouter); diff --git a/packages/backend/test/mocks/rest/api/v1/flows/get-flow.js b/packages/backend/test/mocks/rest/api/v1/flows/get-flow.js new file mode 100644 index 00000000..193855ac --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/flows/get-flow.js @@ -0,0 +1,32 @@ +const getFlowMock = async (flow, steps) => { + const data = { + active: flow.active, + id: flow.id, + name: flow.name, + status: flow.active ? 'published' : 'draft', + steps: steps.map((step) => ({ + appKey: step.appKey, + iconUrl: step.iconUrl, + id: step.id, + key: step.key, + parameters: step.parameters, + position: step.position, + status: step.status, + type: step.type, + webhookUrl: step.webhookUrl, + })), + }; + + return { + data: data, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Flow', + }, + }; +}; + +export default getFlowMock;