diff --git a/packages/backend/src/controllers/api/v1/admin/config/update.ee.js b/packages/backend/src/controllers/api/v1/admin/config/update.ee.js new file mode 100644 index 00000000..a9e1f1b2 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/config/update.ee.js @@ -0,0 +1,50 @@ +import pick from 'lodash/pick.js'; +import { renderObject } from '../../../../../helpers/renderer.js'; +import Config from '../../../../../models/config.js'; + +export default async (request, response) => { + const config = configParams(request); + const configKeys = Object.keys(config); + const updates = []; + + for (const key of configKeys) { + const newValue = config[key]; + + if (newValue) { + const entryUpdate = Config.query() + .insert({ + key, + value: { + data: newValue, + }, + }) + .onConflict('key') + .merge({ + value: { + data: newValue, + }, + }); + + updates.push(entryUpdate); + } else { + const entryUpdate = Config.query().findOne({ key }).delete(); + updates.push(entryUpdate); + } + } + + await Promise.all(updates); + + renderObject(response, config); +}; + +const configParams = (request) => { + const updatableConfigurationKeys = [ + 'logo.svgData', + 'palette.primary.dark', + 'palette.primary.light', + 'palette.primary.main', + 'title', + ]; + + return pick(request.body, updatableConfigurationKeys); +}; diff --git a/packages/backend/src/controllers/api/v1/admin/config/update.ee.test.js b/packages/backend/src/controllers/api/v1/admin/config/update.ee.test.js new file mode 100644 index 00000000..de9a6bb9 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/config/update.ee.test.js @@ -0,0 +1,88 @@ +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 { createRole } from '../../../../../../test/factories/role.js'; +import { createBulkConfig } from '../../../../../../test/factories/config.js'; +import * as license from '../../../../../helpers/license.ee.js'; + +describe('PATCH /api/v1/admin/config', () => { + let currentUser, adminRole, token; + + beforeEach(async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + adminRole = await createRole({ key: 'admin' }); + currentUser = await createUser({ roleId: adminRole.id }); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return updated config', async () => { + const title = 'Test environment - Automatisch'; + const palettePrimaryMain = '#00adef'; + const palettePrimaryDark = '#222222'; + const palettePrimaryLight = '#f90707'; + const logoSvgData = + 'A'; + + const appConfig = { + title, + 'palette.primary.main': palettePrimaryMain, + 'palette.primary.dark': palettePrimaryDark, + 'palette.primary.light': palettePrimaryLight, + 'logo.svgData': logoSvgData, + }; + + await createBulkConfig(appConfig); + + const newTitle = 'Updated title'; + + const newConfigValues = { + title: newTitle, + }; + + const response = await request(app) + .patch('/api/v1/admin/config') + .set('Authorization', token) + .send(newConfigValues) + .expect(200); + + expect(response.body.data.title).toEqual(newTitle); + expect(response.body.meta.type).toEqual('Object'); + }); + + it('should return created config for unexisting config', async () => { + const newTitle = 'Updated title'; + + const newConfigValues = { + title: newTitle, + }; + + const response = await request(app) + .patch('/api/v1/admin/config') + .set('Authorization', token) + .send(newConfigValues) + .expect(200); + + expect(response.body.data.title).toEqual(newTitle); + expect(response.body.meta.type).toEqual('Object'); + }); + + it('should return null for deleted config entry', async () => { + const newConfigValues = { + title: null, + }; + + const response = await request(app) + .patch('/api/v1/admin/config') + .set('Authorization', token) + .send(newConfigValues) + .expect(200); + + expect(response.body.data.title).toBeNull(); + expect(response.body.meta.type).toEqual('Object'); + }); +}); diff --git a/packages/backend/src/routes/api/v1/admin/config.ee.js b/packages/backend/src/routes/api/v1/admin/config.ee.js new file mode 100644 index 00000000..51b4f254 --- /dev/null +++ b/packages/backend/src/routes/api/v1/admin/config.ee.js @@ -0,0 +1,17 @@ +import { Router } from 'express'; +import { authenticateUser } from '../../../../helpers/authentication.js'; +import { authorizeAdmin } from '../../../../helpers/authorization.js'; +import { checkIsEnterprise } from '../../../../helpers/check-is-enterprise.js'; +import updateConfigAction from '../../../../controllers/api/v1/admin/config/update.ee.js'; + +const router = Router(); + +router.patch( + '/', + authenticateUser, + authorizeAdmin, + checkIsEnterprise, + updateConfigAction +); + +export default router; diff --git a/packages/backend/src/routes/index.js b/packages/backend/src/routes/index.js index 0dee76f2..59de8561 100644 --- a/packages/backend/src/routes/index.js +++ b/packages/backend/src/routes/index.js @@ -14,6 +14,7 @@ import connectionsRouter from './api/v1/connections.js'; import executionsRouter from './api/v1/executions.js'; import samlAuthProvidersRouter from './api/v1/saml-auth-providers.ee.js'; 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 rolesRouter from './api/v1/admin/roles.ee.js'; import permissionsRouter from './api/v1/admin/permissions.ee.js'; @@ -37,6 +38,7 @@ router.use('/api/v1/steps', stepsRouter); router.use('/api/v1/executions', executionsRouter); router.use('/api/v1/saml-auth-providers', samlAuthProvidersRouter); router.use('/api/v1/admin/apps', adminAppsRouter); +router.use('/api/v1/admin/config', adminConfigRouter); router.use('/api/v1/admin/users', adminUsersRouter); router.use('/api/v1/admin/roles', rolesRouter); router.use('/api/v1/admin/permissions', permissionsRouter); diff --git a/packages/backend/test/factories/config.js b/packages/backend/test/factories/config.js index 5a8e316c..45f6395d 100644 --- a/packages/backend/test/factories/config.js +++ b/packages/backend/test/factories/config.js @@ -12,6 +12,24 @@ export const createConfig = async (params = {}) => { return config; }; +export const createBulkConfig = async (params = {}) => { + const updateQueries = Object.entries(params).map(([key, value]) => { + const config = { + key, + value: { data: value }, + }; + + return createConfig(config); + }); + + await Promise.all(updateQueries); + + return await Config.query().whereIn('key', Object.keys(params)); +}; + export const createInstallationCompletedConfig = async () => { - return await createConfig({ key: 'installation.completed', value: { data: true } }); -} + return await createConfig({ + key: 'installation.completed', + value: { data: true }, + }); +}; diff --git a/packages/backend/test/mocks/rest/api/v1/admin/config/update.js b/packages/backend/test/mocks/rest/api/v1/admin/config/update.js new file mode 100644 index 00000000..62da1a31 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/admin/config/update.js @@ -0,0 +1,26 @@ +const updateConfigMock = ( + logoConfig, + primaryDarkConfig, + primaryLightConfig, + primaryMainConfig, + titleConfig +) => { + return { + data: { + 'logo.svgData': logoConfig, + 'palette.primary.dark': primaryDarkConfig, + 'palette.primary.light': primaryLightConfig, + 'palette.primary.main': primaryMainConfig, + title: titleConfig, + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Object', + }, + }; +}; + +export default updateConfigMock;