From 820e4cb2c27b360a96942fcd9e1e5b1ac1ad5c46 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Thu, 6 Mar 2025 15:13:09 +0100 Subject: [PATCH 1/8] feat: Implement template serializer for normal users --- packages/backend/src/serializers/index.js | 2 ++ packages/backend/src/serializers/template.ee.js | 11 +++++++++++ 2 files changed, 13 insertions(+) create mode 100644 packages/backend/src/serializers/template.ee.js diff --git a/packages/backend/src/serializers/index.js b/packages/backend/src/serializers/index.js index a96a0072..4cd59010 100644 --- a/packages/backend/src/serializers/index.js +++ b/packages/backend/src/serializers/index.js @@ -3,6 +3,7 @@ import roleSerializer from './role.js'; import permissionSerializer from './permission.js'; import adminSamlAuthProviderSerializer from './admin-saml-auth-provider.ee.js'; import adminTemplateSerializer from './admin/template.ee.js'; +import templateSerializer from './template.ee.js'; import samlAuthProviderSerializer from './saml-auth-provider.ee.js'; import samlAuthProviderRoleMappingSerializer from './role-mapping.ee.js'; import oauthClientSerializer from './oauth-client.js'; @@ -29,6 +30,7 @@ const serializers = { Permission: permissionSerializer, AdminSamlAuthProvider: adminSamlAuthProviderSerializer, AdminTemplate: adminTemplateSerializer, + Template: templateSerializer, SamlAuthProvider: samlAuthProviderSerializer, RoleMapping: samlAuthProviderRoleMappingSerializer, OAuthClient: oauthClientSerializer, diff --git a/packages/backend/src/serializers/template.ee.js b/packages/backend/src/serializers/template.ee.js new file mode 100644 index 00000000..0c5ca5b1 --- /dev/null +++ b/packages/backend/src/serializers/template.ee.js @@ -0,0 +1,11 @@ +const templateSerializer = (template) => { + return { + id: template.id, + name: template.name, + flowData: template.flowData, + createdAt: template.createdAt.getTime(), + updatedAt: template.updatedAt.getTime(), + }; +}; + +export default templateSerializer; From 8ba72db4632c66ec5d7490af616a8f540b2d72f6 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Thu, 6 Mar 2025 15:17:13 +0100 Subject: [PATCH 2/8] feat: Implement get templates API router --- packages/backend/src/helpers/authorization.js | 4 ++++ .../backend/src/routes/api/v1/templates.ee.js | 18 ++++++++++++++++++ packages/backend/src/routes/index.js | 2 ++ 3 files changed, 24 insertions(+) create mode 100644 packages/backend/src/routes/api/v1/templates.ee.js diff --git a/packages/backend/src/helpers/authorization.js b/packages/backend/src/helpers/authorization.js index 107774b1..e921cee8 100644 --- a/packages/backend/src/helpers/authorization.js +++ b/packages/backend/src/helpers/authorization.js @@ -33,6 +33,10 @@ const authorizationList = { action: 'delete', subject: 'Flow', }, + 'GET /api/v1/templates/': { + action: 'create', + subject: 'Flow', + }, 'GET /api/v1/steps/:stepId/connection': { action: 'read', subject: 'Flow', diff --git a/packages/backend/src/routes/api/v1/templates.ee.js b/packages/backend/src/routes/api/v1/templates.ee.js new file mode 100644 index 00000000..1dcae4a0 --- /dev/null +++ b/packages/backend/src/routes/api/v1/templates.ee.js @@ -0,0 +1,18 @@ +import { Router } from 'express'; +import { authenticateUser } from '../../../helpers/authentication.js'; +import { authorizeUser } from '../../../helpers/authorization.js'; +import { checkIsEnterprise } from '../../../helpers/check-is-enterprise.js'; + +import getTemplatesAction from '../../../controllers/api/v1/templates/get-templates.ee.js'; + +const router = Router(); + +router.get( + '/', + authenticateUser, + authorizeUser, + checkIsEnterprise, + getTemplatesAction +); + +export default router; diff --git a/packages/backend/src/routes/index.js b/packages/backend/src/routes/index.js index 958ab180..13634f77 100644 --- a/packages/backend/src/routes/index.js +++ b/packages/backend/src/routes/index.js @@ -16,6 +16,7 @@ import adminAppsRouter from './api/v1/admin/apps.ee.js'; import adminConfigRouter from './api/v1/admin/config.ee.js'; import adminSamlAuthProvidersRouter from './api/v1/admin/saml-auth-providers.ee.js'; import adminTemplatesRouter from './api/v1/admin/templates.ee.js'; +import templatesRouter from './api/v1/templates.ee.js'; import rolesRouter from './api/v1/admin/roles.ee.js'; import permissionsRouter from './api/v1/admin/permissions.ee.js'; import adminUsersRouter from './api/v1/admin/users.ee.js'; @@ -44,6 +45,7 @@ router.use('/api/v1/admin/roles', rolesRouter); router.use('/api/v1/admin/permissions', permissionsRouter); router.use('/api/v1/admin/saml-auth-providers', adminSamlAuthProvidersRouter); router.use('/api/v1/admin/templates', adminTemplatesRouter); +router.use('/api/v1/templates', templatesRouter); router.use('/api/v1/installation/users', installationUsersRouter); router.use('/api/v1/folders', foldersRouter); From 67dfa822e744bf0582418204c3023e542ae5a38d Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Thu, 6 Mar 2025 15:18:33 +0100 Subject: [PATCH 3/8] test: Implement tests for user template serializer --- .../src/serializers/template.ee.test.js | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 packages/backend/src/serializers/template.ee.test.js diff --git a/packages/backend/src/serializers/template.ee.test.js b/packages/backend/src/serializers/template.ee.test.js new file mode 100644 index 00000000..c1158d5e --- /dev/null +++ b/packages/backend/src/serializers/template.ee.test.js @@ -0,0 +1,23 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import templateSerializer from './template.ee.js'; +import { createTemplate } from '../../test/factories/template.js'; + +describe('templateSerializer', () => { + let template; + + beforeEach(async () => { + template = await createTemplate(); + }); + + it('should return flow data', async () => { + const expectedPayload = { + id: template.id, + name: template.name, + flowData: template.flowData, + createdAt: template.createdAt.getTime(), + updatedAt: template.updatedAt.getTime(), + }; + + expect(templateSerializer(template)).toStrictEqual(expectedPayload); + }); +}); From 609360b0e6b62c710ed3b60c53f9769ed97783e4 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Thu, 6 Mar 2025 15:30:11 +0100 Subject: [PATCH 4/8] feat: Add check templates enabled middleware --- .../backend/src/helpers/check-templates-enabled.js | 12 ++++++++++++ packages/backend/src/routes/api/v1/templates.ee.js | 2 ++ 2 files changed, 14 insertions(+) create mode 100644 packages/backend/src/helpers/check-templates-enabled.js diff --git a/packages/backend/src/helpers/check-templates-enabled.js b/packages/backend/src/helpers/check-templates-enabled.js new file mode 100644 index 00000000..e6647541 --- /dev/null +++ b/packages/backend/src/helpers/check-templates-enabled.js @@ -0,0 +1,12 @@ +import Config from '../models/config.js'; +import NotAuthorizedError from '../errors/not-authorized.js'; + +export const checkTemplatesEnabled = async (request, response, next) => { + const config = await Config.get(); + + if (!config.enableTemplates) { + throw new NotAuthorizedError(); + } + + next(); +}; diff --git a/packages/backend/src/routes/api/v1/templates.ee.js b/packages/backend/src/routes/api/v1/templates.ee.js index 1dcae4a0..ccee826f 100644 --- a/packages/backend/src/routes/api/v1/templates.ee.js +++ b/packages/backend/src/routes/api/v1/templates.ee.js @@ -2,6 +2,7 @@ import { Router } from 'express'; import { authenticateUser } from '../../../helpers/authentication.js'; import { authorizeUser } from '../../../helpers/authorization.js'; import { checkIsEnterprise } from '../../../helpers/check-is-enterprise.js'; +import { checkTemplatesEnabled } from '../../../helpers/check-templates-enabled.js'; import getTemplatesAction from '../../../controllers/api/v1/templates/get-templates.ee.js'; @@ -12,6 +13,7 @@ router.get( authenticateUser, authorizeUser, checkIsEnterprise, + checkTemplatesEnabled, getTemplatesAction ); From 4a626016b285ac3bc7dee3f08df257bb4afd24a1 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Thu, 6 Mar 2025 15:34:09 +0100 Subject: [PATCH 5/8] feat: Add user API endpoint to get templates --- .../api/v1/templates/get-templates.ee.js | 10 +++ .../api/v1/templates/get-templates.ee.test.js | 68 +++++++++++++++++++ .../rest/api/v1/templates/get-templates.ee.js | 22 ++++++ 3 files changed, 100 insertions(+) create mode 100644 packages/backend/src/controllers/api/v1/templates/get-templates.ee.js create mode 100644 packages/backend/src/controllers/api/v1/templates/get-templates.ee.test.js create mode 100644 packages/backend/test/mocks/rest/api/v1/templates/get-templates.ee.js diff --git a/packages/backend/src/controllers/api/v1/templates/get-templates.ee.js b/packages/backend/src/controllers/api/v1/templates/get-templates.ee.js new file mode 100644 index 00000000..e9e32613 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/templates/get-templates.ee.js @@ -0,0 +1,10 @@ +import { renderObject } from '../../../../helpers/renderer.js'; +import Template from '../../../../models/template.ee.js'; + +export default async (request, response) => { + const templates = await Template.query().orderBy('created_at', 'asc'); + + renderObject(response, templates, { + serializer: 'Template', + }); +}; diff --git a/packages/backend/src/controllers/api/v1/templates/get-templates.ee.test.js b/packages/backend/src/controllers/api/v1/templates/get-templates.ee.test.js new file mode 100644 index 00000000..c5634c02 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/templates/get-templates.ee.test.js @@ -0,0 +1,68 @@ +import { vi, 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 { createTemplate } from '../../../../../test/factories/template.js'; +import { updateConfig } from '../../../../../test/factories/config.js'; +import { createPermission } from '../../../../../test/factories/permission.js'; +import getTemplatesMock from '../../../../../test/mocks/rest/api/v1/templates/get-templates.ee.js'; +import * as license from '../../../../helpers/license.ee.js'; + +describe('GET /api/v1/templates', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + token = await createAuthTokenByUserId(currentUser.id); + + await updateConfig({ enableTemplates: true }); + }); + + it('should return templates when templates are enabled and user has create flow permission', async () => { + await createPermission({ + action: 'create', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const templateOne = await createTemplate(); + const templateTwo = await createTemplate(); + + const response = await request(app) + .get('/api/v1/templates') + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getTemplatesMock([templateOne, templateTwo]); + + expect(response.body).toStrictEqual(expectedPayload); + }); + + it('should return 403 when templates are disabled', async () => { + await createPermission({ + action: 'create', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await updateConfig({ enableTemplates: false }); + + await request(app) + .get('/api/v1/templates') + .set('Authorization', token) + .expect(403); + }); + + it('should return 403 when user does not have create flow permission', async () => { + await request(app) + .get('/api/v1/templates') + .set('Authorization', token) + .expect(403); + }); +}); diff --git a/packages/backend/test/mocks/rest/api/v1/templates/get-templates.ee.js b/packages/backend/test/mocks/rest/api/v1/templates/get-templates.ee.js new file mode 100644 index 00000000..e296e44b --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/templates/get-templates.ee.js @@ -0,0 +1,22 @@ +const getTemplatesMock = async (templates) => { + const data = templates.map((template) => ({ + id: template.id, + name: template.name, + flowData: template.flowData, + createdAt: template.createdAt.getTime(), + updatedAt: template.updatedAt.getTime(), + })); + + return { + data: data, + meta: { + count: data.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'Template', + }, + }; +}; + +export default getTemplatesMock; From 0a4d0f9f3a6f4457d2e55017c96e78d59df1f20d Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Thu, 6 Mar 2025 17:18:16 +0100 Subject: [PATCH 6/8] refactor: Extract iconUrl generation of steps to helper --- packages/backend/src/helpers/generate-icon-url.js | 7 +++++++ packages/backend/src/models/step.js | 5 ++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 packages/backend/src/helpers/generate-icon-url.js diff --git a/packages/backend/src/helpers/generate-icon-url.js b/packages/backend/src/helpers/generate-icon-url.js new file mode 100644 index 00000000..11c9a1a0 --- /dev/null +++ b/packages/backend/src/helpers/generate-icon-url.js @@ -0,0 +1,7 @@ +import appConfig from '../config/app.js'; + +export const generateIconUrl = (appKey) => { + if (!appKey) return null; + + return `${appConfig.baseUrl}/apps/${appKey}/assets/favicon.svg`; +}; diff --git a/packages/backend/src/models/step.js b/packages/backend/src/models/step.js index 3fe35dbb..1fdd7b4c 100644 --- a/packages/backend/src/models/step.js +++ b/packages/backend/src/models/step.js @@ -9,6 +9,7 @@ import appConfig from '../config/app.js'; import globalVariable from '../helpers/global-variable.js'; import computeParameters from '../helpers/compute-parameters.js'; import testRun from '../services/test-run.js'; +import { generateIconUrl } from '../helpers/generate-icon-url.js'; class Step extends Base { static tableName = 'steps'; @@ -88,9 +89,7 @@ class Step extends Base { } get iconUrl() { - if (!this.appKey) return null; - - return `${appConfig.baseUrl}/apps/${this.appKey}/assets/favicon.svg`; + return generateIconUrl(this.appKey); } get isTrigger() { From 4580027e60912f19b565930b0d8fb55f71c824b2 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Thu, 6 Mar 2025 17:35:10 +0100 Subject: [PATCH 7/8] feat: Add icon urls to template serializer --- packages/backend/src/serializers/template.ee.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/serializers/template.ee.js b/packages/backend/src/serializers/template.ee.js index 0c5ca5b1..1ea943aa 100644 --- a/packages/backend/src/serializers/template.ee.js +++ b/packages/backend/src/serializers/template.ee.js @@ -1,8 +1,18 @@ +import { generateIconUrl } from '../helpers/generate-icon-url.js'; + const templateSerializer = (template) => { + const flowDataWithIconUrls = { + ...template.flowData, + steps: template.flowData.steps?.map((step) => ({ + ...step, + iconUrl: generateIconUrl(step.appKey), + })), + }; + return { id: template.id, name: template.name, - flowData: template.flowData, + flowData: flowDataWithIconUrls, createdAt: template.createdAt.getTime(), updatedAt: template.updatedAt.getTime(), }; From 842833f3d0346699653391fb32ae6172231f71d3 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Fri, 7 Mar 2025 15:55:34 +0100 Subject: [PATCH 8/8] feat: Implement getFlowDataWithIconUrls method for templates --- packages/backend/src/models/template.ee.js | 13 +++ .../backend/src/models/template.ee.test.js | 95 ++++++++++++++++++- .../backend/src/serializers/template.ee.js | 12 +-- .../src/serializers/template.ee.test.js | 3 +- .../rest/api/v1/templates/get-templates.ee.js | 16 ++-- 5 files changed, 118 insertions(+), 21 deletions(-) diff --git a/packages/backend/src/models/template.ee.js b/packages/backend/src/models/template.ee.js index 26f87313..bf0a2899 100644 --- a/packages/backend/src/models/template.ee.js +++ b/packages/backend/src/models/template.ee.js @@ -1,5 +1,6 @@ import Base from './base.js'; import Flow from './flow.js'; +import { generateIconUrl } from '../helpers/generate-icon-url.js'; class Template extends Base { static tableName = 'templates'; @@ -23,6 +24,18 @@ class Template extends Base { return this.query().insertAndFetch({ name, flowData }); } + + getFlowDataWithIconUrls() { + if (!this.flowData) return null; + + return { + ...this.flowData, + steps: this.flowData.steps?.map((step) => ({ + ...step, + iconUrl: generateIconUrl(step.appKey), + })), + }; + } } export default Template; diff --git a/packages/backend/src/models/template.ee.test.js b/packages/backend/src/models/template.ee.test.js index 9ebb4daf..91369c45 100644 --- a/packages/backend/src/models/template.ee.test.js +++ b/packages/backend/src/models/template.ee.test.js @@ -1,8 +1,9 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; import Crypto from 'crypto'; import Template from './template.ee.js'; import { createFlow } from '../../test/factories/flow'; import { createStep } from '../../test/factories/step'; +import appConfig from '../config/app.js'; describe('Template model', () => { it('tableName should return correct name', () => { @@ -76,4 +77,96 @@ describe('Template model', () => { }); }); }); + + describe('getFlowDataWithIconUrls', () => { + beforeEach(() => { + vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue( + 'https://automatisch.io' + ); + }); + + it('should add iconUrl to each step in flowData', () => { + const template = new Template(); + template.flowData = { + id: 'flow-id', + name: 'Test Flow', + steps: [ + { + id: 'step-1', + appKey: 'webhook', + type: 'trigger', + }, + { + id: 'step-2', + appKey: 'formatter', + type: 'action', + }, + ], + }; + + const result = template.getFlowDataWithIconUrls(); + + expect(result.steps[0].iconUrl).toBe( + 'https://automatisch.io/apps/webhook/assets/favicon.svg' + ); + expect(result.steps[1].iconUrl).toBe( + 'https://automatisch.io/apps/formatter/assets/favicon.svg' + ); + }); + + it('should handle steps with null appKey', () => { + const template = new Template(); + template.flowData = { + id: 'flow-id', + name: 'Test Flow', + steps: [ + { + id: 'step-1', + appKey: null, + type: 'trigger', + }, + ], + }; + + const result = template.getFlowDataWithIconUrls(); + + expect(result.steps[0].iconUrl).toBeNull(); + }); + + it('should preserve all other flowData properties', () => { + const template = new Template(); + template.flowData = { + id: 'flow-id', + name: 'Test Flow', + customField: 'test', + steps: [ + { + id: 'step-1', + appKey: 'webhook', + type: 'trigger', + position: 1, + parameters: { test: true }, + }, + ], + }; + + const result = template.getFlowDataWithIconUrls(); + + expect(result).toEqual({ + id: 'flow-id', + name: 'Test Flow', + customField: 'test', + steps: [ + { + id: 'step-1', + appKey: 'webhook', + type: 'trigger', + position: 1, + parameters: { test: true }, + iconUrl: 'https://automatisch.io/apps/webhook/assets/favicon.svg', + }, + ], + }); + }); + }); }); diff --git a/packages/backend/src/serializers/template.ee.js b/packages/backend/src/serializers/template.ee.js index 1ea943aa..9e607fef 100644 --- a/packages/backend/src/serializers/template.ee.js +++ b/packages/backend/src/serializers/template.ee.js @@ -1,18 +1,8 @@ -import { generateIconUrl } from '../helpers/generate-icon-url.js'; - const templateSerializer = (template) => { - const flowDataWithIconUrls = { - ...template.flowData, - steps: template.flowData.steps?.map((step) => ({ - ...step, - iconUrl: generateIconUrl(step.appKey), - })), - }; - return { id: template.id, name: template.name, - flowData: flowDataWithIconUrls, + flowData: template.getFlowDataWithIconUrls(), createdAt: template.createdAt.getTime(), updatedAt: template.updatedAt.getTime(), }; diff --git a/packages/backend/src/serializers/template.ee.test.js b/packages/backend/src/serializers/template.ee.test.js index c1158d5e..440d9e9a 100644 --- a/packages/backend/src/serializers/template.ee.test.js +++ b/packages/backend/src/serializers/template.ee.test.js @@ -1,7 +1,6 @@ import { describe, it, expect, beforeEach } from 'vitest'; import templateSerializer from './template.ee.js'; import { createTemplate } from '../../test/factories/template.js'; - describe('templateSerializer', () => { let template; @@ -13,7 +12,7 @@ describe('templateSerializer', () => { const expectedPayload = { id: template.id, name: template.name, - flowData: template.flowData, + flowData: template.getFlowDataWithIconUrls(), createdAt: template.createdAt.getTime(), updatedAt: template.updatedAt.getTime(), }; diff --git a/packages/backend/test/mocks/rest/api/v1/templates/get-templates.ee.js b/packages/backend/test/mocks/rest/api/v1/templates/get-templates.ee.js index e296e44b..457d3b4d 100644 --- a/packages/backend/test/mocks/rest/api/v1/templates/get-templates.ee.js +++ b/packages/backend/test/mocks/rest/api/v1/templates/get-templates.ee.js @@ -1,11 +1,13 @@ const getTemplatesMock = async (templates) => { - const data = templates.map((template) => ({ - id: template.id, - name: template.name, - flowData: template.flowData, - createdAt: template.createdAt.getTime(), - updatedAt: template.updatedAt.getTime(), - })); + const data = templates.map((template) => { + return { + id: template.id, + name: template.name, + flowData: template.getFlowDataWithIconUrls(), + createdAt: template.createdAt.getTime(), + updatedAt: template.updatedAt.getTime(), + }; + }); return { data: data,