From 54f509ee38e33a75e342e971222dfef23188c5dc Mon Sep 17 00:00:00 2001 From: "kasia.oczkowska" Date: Fri, 22 Nov 2024 14:48:25 +0000 Subject: [PATCH 01/45] feat: introduce inline error messages for InstallationForm and SignUpForm --- packages/web/src/components/Form/index.jsx | 7 +- .../src/components/InstallationForm/index.jsx | 207 ++++++++++-------- .../src/components/SignUpForm/index.ee.jsx | 156 +++++++------ packages/web/src/helpers/errors.js | 18 ++ packages/web/src/locales/en.json | 2 + 5 files changed, 222 insertions(+), 168 deletions(-) create mode 100644 packages/web/src/helpers/errors.js diff --git a/packages/web/src/components/Form/index.jsx b/packages/web/src/components/Form/index.jsx index 614873f7..352b0a69 100644 --- a/packages/web/src/components/Form/index.jsx +++ b/packages/web/src/components/Form/index.jsx @@ -46,7 +46,12 @@ function Form(props) { return ( -
+ + onSubmit?.(data, event, methods.setError), + )} + {...formProps} + > {render ? render(methods) : children}
diff --git a/packages/web/src/components/InstallationForm/index.jsx b/packages/web/src/components/InstallationForm/index.jsx index 80d66b6c..6e443302 100644 --- a/packages/web/src/components/InstallationForm/index.jsx +++ b/packages/web/src/components/InstallationForm/index.jsx @@ -2,35 +2,55 @@ import * as React from 'react'; import { Link as RouterLink } from 'react-router-dom'; import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; -import { Alert } from '@mui/material'; +import Alert from '@mui/material/Alert'; import LoadingButton from '@mui/lab/LoadingButton'; import * as yup from 'yup'; import { yupResolver } from '@hookform/resolvers/yup'; -import { enqueueSnackbar } from 'notistack'; import { useQueryClient } from '@tanstack/react-query'; import Link from '@mui/material/Link'; +import { getGeneralErrorMessage } from 'helpers/errors'; import useFormatMessage from 'hooks/useFormatMessage'; import useInstallation from 'hooks/useInstallation'; import * as URLS from 'config/urls'; import Form from 'components/Form'; import TextField from 'components/TextField'; -const validationSchema = yup.object().shape({ - fullName: yup.string().trim().required('installationForm.mandatoryInput'), - email: yup - .string() - .trim() - .email('installationForm.validateEmail') - .required('installationForm.mandatoryInput'), - password: yup.string().required('installationForm.mandatoryInput'), - confirmPassword: yup - .string() - .required('installationForm.mandatoryInput') - .oneOf([yup.ref('password')], 'installationForm.passwordsMustMatch'), -}); +const getValidationSchema = (formatMessage) => { + const getMandatoryInputMessage = (inputNameId) => + formatMessage('installationForm.mandatoryInput', { + inputName: formatMessage(inputNameId), + }); -const initialValues = { + return yup.object().shape({ + fullName: yup + .string() + .trim() + .required( + getMandatoryInputMessage('installationForm.fullNameFieldLabel'), + ), + email: yup + .string() + .trim() + .required(getMandatoryInputMessage('installationForm.emailFieldLabel')) + .email(formatMessage('installationForm.validateEmail')), + password: yup + .string() + .required(getMandatoryInputMessage('installationForm.passwordFieldLabel')) + .min(6, formatMessage('installationForm.passwordMinLength')), + confirmPassword: yup + .string() + .required( + getMandatoryInputMessage('installationForm.confirmPasswordFieldLabel'), + ) + .oneOf( + [yup.ref('password')], + formatMessage('installationForm.passwordsMustMatch'), + ), + }); +}; + +const defaultValues = { fullName: '', email: '', password: '', @@ -39,7 +59,7 @@ const initialValues = { function InstallationForm() { const formatMessage = useFormatMessage(); - const install = useInstallation(); + const { mutateAsync: install, isSuccess, isPending } = useInstallation(); const queryClient = useQueryClient(); const handleOnRedirect = () => { @@ -48,21 +68,38 @@ function InstallationForm() { }); }; - const handleSubmit = async (values) => { - const { fullName, email, password } = values; + const handleSubmit = async ({ fullName, email, password }, e, setError) => { try { - await install.mutateAsync({ + await install({ fullName, email, password, }); } catch (error) { - enqueueSnackbar( - error?.message || formatMessage('installationForm.error'), - { - variant: 'error', - }, - ); + const errors = error?.response?.data?.errors; + if (errors) { + const fieldNames = Object.keys(defaultValues); + Object.entries(errors).forEach(([fieldName, fieldErrors]) => { + if (fieldNames.includes(fieldName) && Array.isArray(fieldErrors)) { + setError(fieldName, { + type: 'fieldRequestError', + message: fieldErrors.join(', '), + }); + } + }); + } + + const generalError = getGeneralErrorMessage({ + error, + fallbackMessage: formatMessage('installationForm.error'), + }); + + if (generalError) { + setError('root.general', { + type: 'requestError', + message: generalError, + }); + } } }; @@ -82,11 +119,13 @@ function InstallationForm() { {formatMessage('installationForm.title')}
( + render={({ formState: { errors } }) => ( <> + + + + {errors?.root?.general && ( + + {errors.root.general.message} + + )} + + {isSuccess && ( + + {formatMessage('installationForm.success', { + link: (str) => ( + + {str} + + ), + })} + + )} {formatMessage('installationForm.submit')} )} /> - {install.isSuccess && ( - - {formatMessage('installationForm.success', { - link: (str) => ( - - {str} - - ), - })} - - )} ); } diff --git a/packages/web/src/components/SignUpForm/index.ee.jsx b/packages/web/src/components/SignUpForm/index.ee.jsx index e931d394..3cbc1033 100644 --- a/packages/web/src/components/SignUpForm/index.ee.jsx +++ b/packages/web/src/components/SignUpForm/index.ee.jsx @@ -3,33 +3,52 @@ import { useNavigate } from 'react-router-dom'; import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; import LoadingButton from '@mui/lab/LoadingButton'; +import Alert from '@mui/material/Alert'; import * as yup from 'yup'; import { yupResolver } from '@hookform/resolvers/yup'; +import { getGeneralErrorMessage } from 'helpers/errors'; import useAuthentication from 'hooks/useAuthentication'; import * as URLS from 'config/urls'; import Form from 'components/Form'; import TextField from 'components/TextField'; import useFormatMessage from 'hooks/useFormatMessage'; import useCreateAccessToken from 'hooks/useCreateAccessToken'; -import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import useRegisterUser from 'hooks/useRegisterUser'; -const validationSchema = yup.object().shape({ - fullName: yup.string().trim().required('signupForm.mandatoryInput'), - email: yup - .string() - .trim() - .email('signupForm.validateEmail') - .required('signupForm.mandatoryInput'), - password: yup.string().required('signupForm.mandatoryInput'), - confirmPassword: yup - .string() - .required('signupForm.mandatoryInput') - .oneOf([yup.ref('password')], 'signupForm.passwordsMustMatch'), -}); +const getValidationSchema = (formatMessage) => { + const getMandatoryInputMessage = (inputNameId) => + formatMessage('signupForm.mandatoryInput', { + inputName: formatMessage(inputNameId), + }); -const initialValues = { + return yup.object().shape({ + fullName: yup + .string() + .trim() + .required(getMandatoryInputMessage('signupForm.fullNameFieldLabel')), + email: yup + .string() + .trim() + .required(getMandatoryInputMessage('signupForm.emailFieldLabel')) + .email(formatMessage('signupForm.validateEmail')), + password: yup + .string() + .required(getMandatoryInputMessage('signupForm.passwordFieldLabel')) + .min(6, formatMessage('signupForm.passwordMinLength')), + confirmPassword: yup + .string() + .required( + getMandatoryInputMessage('signupForm.confirmPasswordFieldLabel'), + ) + .oneOf( + [yup.ref('password')], + formatMessage('signupForm.passwordsMustMatch'), + ), + }); +}; + +const defaultValues = { fullName: '', email: '', password: '', @@ -40,7 +59,6 @@ function SignUpForm() { const navigate = useNavigate(); const authentication = useAuthentication(); const formatMessage = useFormatMessage(); - const enqueueSnackbar = useEnqueueSnackbar(); const { mutateAsync: registerUser, isPending: isRegisterUserPending } = useRegisterUser(); const { mutateAsync: createAccessToken, isPending: loginLoading } = @@ -52,7 +70,7 @@ function SignUpForm() { } }, [authentication.isAuthenticated]); - const handleSubmit = async (values) => { + const handleSubmit = async (values, e, setError) => { try { const { fullName, email, password } = values; await registerUser({ @@ -67,25 +85,28 @@ function SignUpForm() { const { token } = data; authentication.updateToken(token); } catch (error) { - const errors = error?.response?.data?.errors - ? Object.values(error.response.data.errors) - : []; + const errors = error?.response?.data?.errors; + if (errors) { + const fieldNames = Object.keys(defaultValues); + Object.entries(errors).forEach(([fieldName, fieldErrors]) => { + if (fieldNames.includes(fieldName) && Array.isArray(fieldErrors)) { + setError(fieldName, { + type: 'fieldRequestError', + message: fieldErrors.join(', '), + }); + } + }); + } - if (errors.length) { - for (const [error] of errors) { - enqueueSnackbar(error, { - variant: 'error', - SnackbarProps: { - 'data-test': 'snackbar-sign-up-error', - }, - }); - } - } else { - enqueueSnackbar(error?.message || formatMessage('signupForm.error'), { - variant: 'error', - SnackbarProps: { - 'data-test': 'snackbar-sign-up-error', - }, + const generalError = getGeneralErrorMessage({ + error, + fallbackMessage: formatMessage('signupForm.error'), + }); + + if (generalError) { + setError('root.general', { + type: 'requestError', + message: generalError, }); } } @@ -108,11 +129,13 @@ function SignUpForm() { ( + render={({ formState: { errors } }) => ( <> + {errors?.root?.general && ( + + {errors.root.general.message} + + )} + { + if (!error) { + return; + } + + const errors = error?.response?.data?.errors; + const generalError = errors?.general; + + if (generalError && Array.isArray(generalError)) { + return generalError.join(' '); + } + + if (!errors) { + return error?.message || fallbackMessage; + } +}; diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index b121f5e2..941af1fa 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -138,6 +138,7 @@ "installationForm.submit": "Create admin", "installationForm.validateEmail": "Email must be valid.", "installationForm.passwordsMustMatch": "Passwords must match.", + "installationForm.passwordMinLength": "Password must be at least 6 characters long.", "installationForm.mandatoryInput": "{inputName} is required.", "installationForm.success": "The admin account has been created, and thus, the installation has been completed. You can now log in here.", "installationForm.error": "Something went wrong. Please try again.", @@ -149,6 +150,7 @@ "signupForm.submit": "Sign up", "signupForm.validateEmail": "Email must be valid.", "signupForm.passwordsMustMatch": "Passwords must match.", + "signupForm.passwordMinLength": "Password must be at least 6 characters long.", "signupForm.mandatoryInput": "{inputName} is required.", "signupForm.error": "Something went wrong. Please try again.", "loginForm.title": "Login", From 3de18ab46f8fc84553660b9addf32ca51ea400e2 Mon Sep 17 00:00:00 2001 From: "Jakub P." Date: Tue, 10 Dec 2024 00:44:03 +0100 Subject: [PATCH 02/45] test: use updated selector for create admin button in installation form --- .../e2e-tests/fixtures/admin-setup-page.js | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/e2e-tests/fixtures/admin-setup-page.js b/packages/e2e-tests/fixtures/admin-setup-page.js index 704a9caf..6d5b85c2 100644 --- a/packages/e2e-tests/fixtures/admin-setup-page.js +++ b/packages/e2e-tests/fixtures/admin-setup-page.js @@ -1,4 +1,4 @@ -import { BasePage } from "./base-page"; +import { BasePage } from './base-page'; const { faker } = require('@faker-js/faker'); const { expect } = require('@playwright/test'); @@ -6,16 +6,18 @@ export class AdminSetupPage extends BasePage { path = '/installation'; /** - * @param {import('@playwright/test').Page} page - */ + * @param {import('@playwright/test').Page} page + */ constructor(page) { super(page); this.fullNameTextField = this.page.getByTestId('fullName-text-field'); this.emailTextField = this.page.getByTestId('email-text-field'); this.passwordTextField = this.page.getByTestId('password-text-field'); - this.repeatPasswordTextField = this.page.getByTestId('repeat-password-text-field'); - this.createAdminButton = this.page.getByTestId('signUp-button'); + this.repeatPasswordTextField = this.page.getByTestId( + 'repeat-password-text-field' + ); + this.createAdminButton = this.page.getByTestId('installation-button'); this.invalidFields = this.page.locator('p.Mui-error'); this.successAlert = this.page.getByTestId('success-alert'); } @@ -46,7 +48,7 @@ export class AdminSetupPage extends BasePage { await this.repeatPasswordTextField.fill(testUser.wronglyRepeatedPassword); } - async submitAdminForm() { + async submitAdminForm() { await this.createAdminButton.click(); } @@ -59,7 +61,10 @@ export class AdminSetupPage extends BasePage { } async expectSuccessMessageToContainLoginLink() { - await expect(await this.successAlert.locator('a')).toHaveAttribute('href', '/login'); + await expect(await this.successAlert.locator('a')).toHaveAttribute( + 'href', + '/login' + ); } generateUser() { @@ -69,7 +74,7 @@ export class AdminSetupPage extends BasePage { fullName: faker.person.fullName(), email: faker.internet.email(), password: faker.internet.password(), - wronglyRepeatedPassword: faker.internet.password() + wronglyRepeatedPassword: faker.internet.password(), }; } -}; +} From e41a331ad7d1bc6882c321dbabe26eee47356a6f Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Thu, 12 Dec 2024 14:51:13 +0100 Subject: [PATCH 03/45] refactor: Abstract queue generation and configuration --- packages/backend/src/queues/action.js | 27 +----------- packages/backend/src/queues/delete-user.ee.js | 27 +----------- packages/backend/src/queues/email.js | 27 +----------- packages/backend/src/queues/flow.js | 27 +----------- packages/backend/src/queues/queue.js | 44 +++++++++++++++++++ .../remove-cancelled-subscriptions.ee.js | 38 ++-------------- packages/backend/src/queues/trigger.js | 27 +----------- 7 files changed, 57 insertions(+), 160 deletions(-) create mode 100644 packages/backend/src/queues/queue.js diff --git a/packages/backend/src/queues/action.js b/packages/backend/src/queues/action.js index 3c413173..dbb0226a 100644 --- a/packages/backend/src/queues/action.js +++ b/packages/backend/src/queues/action.js @@ -1,27 +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); - -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 11794005..8e939523 100644 --- a/packages/backend/src/queues/delete-user.ee.js +++ b/packages/backend/src/queues/delete-user.ee.js @@ -1,27 +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); - -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 5755f7c2..31e55bd5 100644 --- a/packages/backend/src/queues/email.js +++ b/packages/backend/src/queues/email.js @@ -1,27 +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); - -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 48de083a..b9d335fe 100644 --- a/packages/backend/src/queues/flow.js +++ b/packages/backend/src/queues/flow.js @@ -1,27 +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); - -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/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 f5f574a8..bb439722 100644 --- a/packages/backend/src/queues/remove-cancelled-subscriptions.ee.js +++ b/packages/backend/src/queues/remove-cancelled-subscriptions.ee.js @@ -1,40 +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 } ); -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 bc0f9b46..e2134e13 100644 --- a/packages/backend/src/queues/trigger.js +++ b/packages/backend/src/queues/trigger.js @@ -1,27 +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); - -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; From d1de47788a535be1658b1508cf87a1eeb9f0a2f4 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Mon, 16 Dec 2024 16:13:34 +0100 Subject: [PATCH 04/45] feat: Expose supportsOauthClients info for apps --- .../src/controllers/api/v1/users/get-apps.js | 2 +- packages/backend/src/serializers/app.js | 1 + packages/backend/src/serializers/app.test.js | 1 + packages/backend/src/serializers/index.js | 2 ++ packages/backend/src/serializers/user-app.js | 22 +++++++++++++++++++ .../test/mocks/rest/api/v1/apps/get-app.js | 1 + .../test/mocks/rest/api/v1/apps/get-apps.js | 1 + 7 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 packages/backend/src/serializers/user-app.js 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/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/index.js b/packages/backend/src/serializers/index.js index 8fdeb888..3111b2df 100644 --- a/packages/backend/src/serializers/index.js +++ b/packages/backend/src/serializers/index.js @@ -10,6 +10,7 @@ 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'; @@ -33,6 +34,7 @@ const serializers = { Step: stepSerializer, Connection: connectionSerializer, App: appSerializer, + UserApp: userAppSerializer, Auth: authSerializer, Trigger: triggerSerializer, Action: actionSerializer, 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/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 { From 613f744dd6bcfbb3cb9dae2e320aaaa29de839f2 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Wed, 4 Dec 2024 10:43:43 +0000 Subject: [PATCH 05/45] feat(app-config): add useOnlyPredefinedAuthClients property --- ..._use_only_predefined_auth_clients_in_app_config.js | 11 +++++++++++ packages/backend/src/models/app-config.js | 1 + 2 files changed, 12 insertions(+) create mode 100644 packages/backend/src/db/migrations/20241204103153_add_use_only_predefined_auth_clients_in_app_config.js 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/models/app-config.js b/packages/backend/src/models/app-config.js index 1a9176b9..152c3c91 100644 --- a/packages/backend/src/models/app-config.js +++ b/packages/backend/src/models/app-config.js @@ -16,6 +16,7 @@ class AppConfig extends Base { properties: { id: { type: 'string', format: 'uuid' }, key: { type: 'string' }, + useOnlyPredefinedAuthClients: { type: 'boolean', default: false }, connectionAllowed: { type: 'boolean', default: false }, customConnectionAllowed: { type: 'boolean', default: false }, shared: { type: 'boolean', default: false }, From cf7e2e08801774ef525b8820b558dccc45cfd0a2 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Wed, 4 Dec 2024 10:44:27 +0000 Subject: [PATCH 06/45] refactor(app-config): remove obsolete properties --- ...103355_remove_obsolete_fields_in_app_config.js | 15 +++++++++++++++ packages/backend/src/models/app-config.js | 3 --- 2 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 packages/backend/src/db/migrations/20241204103355_remove_obsolete_fields_in_app_config.js 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/models/app-config.js b/packages/backend/src/models/app-config.js index 152c3c91..6dae7cfe 100644 --- a/packages/backend/src/models/app-config.js +++ b/packages/backend/src/models/app-config.js @@ -17,9 +17,6 @@ class AppConfig extends Base { id: { type: 'string', format: 'uuid' }, key: { type: 'string' }, useOnlyPredefinedAuthClients: { type: 'boolean', default: false }, - connectionAllowed: { type: 'boolean', default: false }, - customConnectionAllowed: { type: 'boolean', default: false }, - shared: { type: 'boolean', default: false }, disabled: { type: 'boolean', default: false }, createdAt: { type: 'string' }, updatedAt: { type: 'string' }, From 00e80f1fba4124ff17cb1b6e812052e4b7b26e16 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Wed, 4 Dec 2024 15:46:08 +0000 Subject: [PATCH 07/45] feat(AdminApplication): remove connections tab --- packages/web/src/pages/AdminApplication/index.jsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/web/src/pages/AdminApplication/index.jsx b/packages/web/src/pages/AdminApplication/index.jsx index 44e73085..850f4a0e 100644 --- a/packages/web/src/pages/AdminApplication/index.jsx +++ b/packages/web/src/pages/AdminApplication/index.jsx @@ -92,13 +92,6 @@ export default function AdminApplication() { value={URLS.ADMIN_APP_AUTH_CLIENTS_PATTERN} component={Link} /> - @@ -111,10 +104,6 @@ export default function AdminApplication() { path={`/auth-clients/*`} element={} /> - App connections} - /> Date: Mon, 9 Dec 2024 17:46:51 +0000 Subject: [PATCH 08/45] feat(AppConfig): iterate how apps are managed - auth clients are always shared, cannot be disabled - custom connections are enabled by default, can be disabled - any existing connections can be reconnected regardless of its AppConfig or AppAuthClient states --- .../api/v1/admin/apps/create-config.ee.js | 5 +- .../v1/admin/apps/create-config.ee.test.js | 7 +- .../api/v1/admin/apps/update-config.ee.js | 5 +- .../v1/admin/apps/update-config.ee.test.js | 12 +- .../api/v1/apps/create-connection.test.js | 29 ++-- .../api/v1/apps/get-config.ee.test.js | 3 +- .../v1/connections/reset-connection.test.js | 1 - .../v1/connections/update-connection.test.js | 7 +- .../__snapshots__/app-config.test.js.snap | 16 +-- .../backend/src/models/app-auth-client.js | 17 +-- .../src/models/app-auth-client.test.js | 81 ----------- packages/backend/src/models/app-config.js | 33 ----- .../backend/src/models/app-config.test.js | 126 +----------------- packages/backend/src/models/connection.js | 26 +--- .../backend/src/models/connection.test.js | 105 +-------------- .../backend/src/serializers/app-config.js | 4 +- .../src/serializers/app-config.test.js | 4 +- packages/backend/src/serializers/auth.js | 2 + packages/backend/src/serializers/auth.test.js | 2 + .../backend/src/serializers/connection.js | 1 - .../src/serializers/connection.test.js | 1 - .../rest/api/v1/admin/apps/create-config.js | 3 +- .../rest/api/v1/apps/create-connection.js | 1 - .../test/mocks/rest/api/v1/apps/get-auth.js | 2 + .../test/mocks/rest/api/v1/apps/get-config.js | 4 +- .../mocks/rest/api/v1/apps/get-connections.js | 1 - .../api/v1/connections/reset-connection.js | 1 - .../api/v1/connections/update-connection.js | 1 - .../mocks/rest/api/v1/steps/get-connection.js | 1 - packages/backend/vitest.config.js | 8 +- packages/web/package.json | 1 + .../src/components/AddAppConnection/index.jsx | 3 +- .../index.jsx | 4 +- .../AdminApplicationSettings/index.jsx | 22 ++- .../AppAuthClientsDialog/index.ee.jsx | 12 +- .../AppConnectionContextMenu/index.jsx | 12 +- .../src/components/AppConnectionRow/index.jsx | 4 +- .../ChooseConnectionSubstep/index.jsx | 13 +- .../web/src/components/SplitButton/index.jsx | 11 +- .../web/src/hooks/useAuthenticateApp.ee.js | 33 ++++- packages/web/src/hooks/useAutomatischInfo.js | 2 +- packages/web/src/hooks/useCreateConnection.js | 4 +- packages/web/src/hooks/useLicense.js | 15 +++ packages/web/src/index.jsx | 3 + packages/web/src/locales/en.json | 4 +- packages/web/src/pages/Application/index.jsx | 63 ++++----- packages/web/src/propTypes/propTypes.js | 4 +- packages/web/yarn.lock | 36 ++++- 48 files changed, 192 insertions(+), 563 deletions(-) create mode 100644 packages/web/src/hooks/useLicense.js 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/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/apps/create-connection.test.js b/packages/backend/src/controllers/api/v1/apps/create-connection.test.js index 4a12aa99..c73df6b6 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 @@ -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,14 +266,14 @@ describe('POST /api/v1/apps/:appKey/connections', () => { }); }); - describe('with auth clients enabled', async () => { + describe('with auth client enabled', async () => { let appAuthClient; beforeEach(async () => { await createAppConfig({ key: 'gitlab', disabled: false, - shared: true, + useOnlyPredefinedAuthClients: false, }); appAuthClient = await createAppAuthClient({ @@ -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,18 +336,20 @@ describe('POST /api/v1/apps/:appKey/connections', () => { }); }); }); - describe('with auth clients disabled', async () => { + + describe('with auth client disabled', async () => { let appAuthClient; beforeEach(async () => { await createAppConfig({ key: 'gitlab', disabled: false, - shared: false, + useOnlyPredefinedAuthClients: false, }); appAuthClient = await createAppAuthClient({ appKey: 'gitlab', + active: false, }); }); @@ -373,7 +362,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => { .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-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/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.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/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/app-auth-client.js b/packages/backend/src/models/app-auth-client.js index 90a9bda3..48800841 100644 --- a/packages/backend/src/models/app-auth-client.js +++ b/packages/backend/src/models/app-auth-client.js @@ -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() { diff --git a/packages/backend/src/models/app-auth-client.test.js b/packages/backend/src/models/app-auth-client.test.js index af1fefc2..ddee5c5e 100644 --- a/packages/backend/src/models/app-auth-client.test.js +++ b/packages/backend/src/models/app-auth-client.test.js @@ -164,63 +164,6 @@ describe('AppAuthClient model', () => { }); }); - 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, @@ -232,17 +175,6 @@ describe('AppAuthClient model', () => { 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(); @@ -256,19 +188,6 @@ describe('AppAuthClient model', () => { 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(); diff --git a/packages/backend/src/models/app-config.js b/packages/backend/src/models/app-config.js index 6dae7cfe..6763e9f8 100644 --- a/packages/backend/src/models/app-config.js +++ b/packages/backend/src/models/app-config.js @@ -39,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..2e6f05be 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'; describe('AppConfig model', () => { it('tableName should return correct name', () => { @@ -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..4a8d5351 100644 --- a/packages/backend/src/models/connection.js +++ b/packages/backend/src/models/connection.js @@ -33,10 +33,6 @@ class Connection extends Base { }, }; - static get virtualAttributes() { - return ['reconnectable']; - } - static relationMappings = () => ({ user: { relation: Base.BelongsToOneRelation, @@ -83,18 +79,6 @@ class Connection extends Base { }, }); - 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,19 +128,13 @@ 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) diff --git a/packages/backend/src/models/connection.test.js b/packages/backend/src/models/connection.test.js index 7c5057bb..329fdfe6 100644 --- a/packages/backend/src/models/connection.test.js +++ b/packages/backend/src/models/connection.test.js @@ -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(); @@ -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,32 +307,10 @@ 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 () => { await createAppConfig({ key: 'gitlab', disabled: false, - customConnectionAllowed: true, - shared: true, }); const appAuthClient = await createAppAuthClient({ 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/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..388a6b87 100644 --- a/packages/backend/src/serializers/connection.js +++ b/packages/backend/src/serializers/connection.js @@ -2,7 +2,6 @@ const connectionSerializer = (connection) => { return { id: connection.id, key: connection.key, - reconnectable: connection.reconnectable, appAuthClientId: connection.appAuthClientId, 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..3ea7b324 100644 --- a/packages/backend/src/serializers/connection.test.js +++ b/packages/backend/src/serializers/connection.test.js @@ -13,7 +13,6 @@ describe('connectionSerializer', () => { const expectedPayload = { id: connection.id, key: connection.key, - reconnectable: connection.reconnectable, appAuthClientId: connection.appAuthClientId, formattedData: { screenName: connection.formattedData.screenName, 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/apps/create-connection.js b/packages/backend/test/mocks/rest/api/v1/apps/create-connection.js index 9e993a4c..2eb1fd7f 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,7 +2,6 @@ const createConnection = (connection) => { const connectionData = { id: connection.id, key: connection.key, - reconnectable: connection.reconnectable || true, appAuthClientId: connection.appAuthClientId, formattedData: connection.formattedData, verified: connection.verified || false, 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..bd3bfa4c 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,7 +3,6 @@ const getConnectionsMock = (connections) => { data: connections.map((connection) => ({ id: connection.id, key: connection.key, - reconnectable: connection.reconnectable, verified: connection.verified, appAuthClientId: connection.appAuthClientId, formattedData: { 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..0d8131c8 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,7 +3,6 @@ const resetConnectionMock = (connection) => { id: connection.id, key: connection.key, verified: connection.verified, - reconnectable: connection.reconnectable, appAuthClientId: connection.appAuthClientId, 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..d46b9a0c 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,7 +3,6 @@ const updateConnectionMock = (connection) => { id: connection.id, key: connection.key, verified: connection.verified, - reconnectable: connection.reconnectable, appAuthClientId: connection.appAuthClientId, 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..831a148a 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,7 +3,6 @@ const getConnectionMock = async (connection) => { id: connection.id, key: connection.key, verified: connection.verified, - reconnectable: connection.reconnectable, appAuthClientId: connection.appAuthClientId, formattedData: { screenName: connection.formattedData.screenName, diff --git a/packages/backend/vitest.config.js b/packages/backend/vitest.config.js index a62dfa53..645c7fbf 100644 --- a/packages/backend/vitest.config.js +++ b/packages/backend/vitest.config.js @@ -16,10 +16,10 @@ export default defineConfig({ include: ['**/src/models/**', '**/src/controllers/**'], thresholds: { autoUpdate: true, - statements: 93.41, - branches: 93.46, - functions: 95.95, - lines: 93.41, + statements: 95.16, + branches: 94.66, + functions: 97.65, + lines: 95.16, }, }, }, 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..9147ff50 100644 --- a/packages/web/src/components/AddAppConnection/index.jsx +++ b/packages/web/src/components/AddAppConnection/index.jsx @@ -18,6 +18,7 @@ import { generateExternalLink } from 'helpers/translationValues'; import { Form } from './style'; import useAppAuth from 'hooks/useAppAuth'; import { useQueryClient } from '@tanstack/react-query'; +import { useWhatChanged } from '@simbathesailor/use-what-changed'; function AddAppConnection(props) { const { application, connectionId, onClose } = props; @@ -64,7 +65,7 @@ function AddAppConnection(props) { asyncAuthenticate(); }, - [appAuthClientId, authenticate], + [appAuthClientId, authenticate, key, navigate], ); const handleClientClick = (appAuthClientId) => diff --git a/packages/web/src/components/AdminApplicationCreateAuthClient/index.jsx b/packages/web/src/components/AdminApplicationCreateAuthClient/index.jsx index ccda0d0e..4747e876 100644 --- a/packages/web/src/components/AdminApplicationCreateAuthClient/index.jsx +++ b/packages/web/src/components/AdminApplicationCreateAuthClient/index.jsx @@ -34,10 +34,10 @@ function AdminApplicationCreateAuthClient(props) { if (!appConfigKey) { const { data: appConfigData } = await createAppConfig({ - customConnectionAllowed: true, - shared: false, + useOnlyPredefinedAuthClients: false, disabled: false, }); + appConfigKey = appConfigData.key; } 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) { - - + + + ; + if (!appAuthClients?.data.length) return ; return ( diff --git a/packages/web/src/components/AppConnectionContextMenu/index.jsx b/packages/web/src/components/AppConnectionContextMenu/index.jsx index fb94e4b3..f17fb860 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,7 +66,7 @@ function ContextMenu(props) { {(allowed) => ( ( 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) { - {({ TransitionProps, placement }) => ( - + {({ TransitionProps }) => ( + diff --git a/packages/web/src/hooks/useAuthenticateApp.ee.js b/packages/web/src/hooks/useAuthenticateApp.ee.js index b0b99b09..45c17bfb 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) { @@ -37,11 +38,13 @@ export default function useAuthenticateApp(payload) { 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; @@ -57,7 +60,6 @@ export default function useAuthenticateApp(payload) { 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, connectionId, queryClient, - formatMessage, createConnection, createConnectionAuthUrl, updateConnection, @@ -140,6 +143,24 @@ export default function useAuthenticateApp(payload) { verifyConnection, ]); + useWhatChanged( + [ + steps, + appKey, + appAuthClientId, + connectionId, + queryClient, + createConnection, + createConnectionAuthUrl, + updateConnection, + resetConnection, + verifyConnection, + ], + 'steps, appKey, appAuthClientId, 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..7615ab6d 100644 --- a/packages/web/src/hooks/useCreateConnection.js +++ b/packages/web/src/hooks/useCreateConnection.js @@ -3,7 +3,7 @@ import { useMutation } from '@tanstack/react-query'; import api from 'helpers/api'; export default function useCreateConnection(appKey) { - const query = useMutation({ + const mutation = useMutation({ mutationFn: async ({ appAuthClientId, formattedData }) => { const { data } = await api.post(`/v1/apps/${appKey}/connections`, { appAuthClientId, @@ -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/index.jsx b/packages/web/src/index.jsx index 8c03da02..916baf28 100644 --- a/packages/web/src/index.jsx +++ b/packages/web/src/index.jsx @@ -1,5 +1,6 @@ import { createRoot } from 'react-dom/client'; import { Settings } from 'luxon'; +import { setUseWhatChange } from '@simbathesailor/use-what-changed'; import ThemeProvider from 'components/ThemeProvider'; import IntlProvider from 'components/IntlProvider'; @@ -14,6 +15,8 @@ import reportWebVitals from './reportWebVitals'; // Sets the default locale to English for all luxon DateTime instances created afterwards. Settings.defaultLocale = 'en'; +setUseWhatChange(process.env.NODE_ENV === 'development'); + const container = document.getElementById('root'); const root = createRoot(container); diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index b121f5e2..d7786d9e 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.addConnectionWithAuthClient": "Add connection with auth client", "app.reconnectConnection": "Reconnect connection", "app.createFlow": "Create flow", "app.settings": "Settings", @@ -292,7 +292,7 @@ "adminApps.connections": "Connections", "adminApps.authClients": "Auth clients", "adminApps.settings": "Settings", - "adminAppsSettings.customConnectionAllowed": "Allow custom connection", + "adminAppsSettings.useOnlyPredefinedAuthClients": "Use only predefined auth clients", "adminAppsSettings.shared": "Shared", "adminAppsSettings.disabled": "Disabled", "adminAppsSettings.save": "Save", diff --git a/packages/web/src/pages/Application/index.jsx b/packages/web/src/pages/Application/index.jsx index 74794784..1afd7605 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 useAppAuthClients from 'hooks/useAppAuthClients'; import Can from 'components/Can'; import { AppPropType } from 'propTypes/propTypes'; @@ -61,47 +61,53 @@ 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: appAuthClients } = useAppAuthClients(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'), + }; - 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 addConnectionWithAuthClient = { + label: formatMessage('app.addConnectionWithAuthClient'), + key: 'addConnectionWithAuthClient', + 'data-test': 'add-custom-connection-button', + to: URLS.APP_ADD_CONNECTION(appKey, true), + disabled: !currentUserAbility.can('create', 'Connection'), + }; - 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 there is no app auth client. so we don't show the `addConnectionWithAuthClient` + if (appAuthClients?.data?.length === 0) { + return [addCustomConnection]; + } + + // means only auth clients are allowed for connection creation + if (appConfig?.data?.useOnlyPredefinedAuthClients === true) { + return [addConnectionWithAuthClient]; + } + + return [addCustomConnection, addConnectionWithAuthClient]; + }, [appKey, appConfig, appAuthClients, currentUserAbility, formatMessage]); if (loading) return null; @@ -154,12 +160,7 @@ export default function Application() { {(allowed) => ( disabled) + !allowed || appConfig?.data?.disabled === true } options={connectionOptions} /> diff --git a/packages/web/src/propTypes/propTypes.js b/packages/web/src/propTypes/propTypes.js index e6660c75..c92f51a4 100644 --- a/packages/web/src/propTypes/propTypes.js +++ b/packages/web/src/propTypes/propTypes.js @@ -211,7 +211,6 @@ export const ConnectionPropType = PropTypes.shape({ flowCount: PropTypes.number, appData: AppPropType, createdAt: PropTypes.number, - reconnectable: PropTypes.bool, appAuthClientId: PropTypes.string, }); @@ -459,8 +458,7 @@ export const SamlAuthProviderRolePropType = PropTypes.shape({ export const AppConfigPropType = PropTypes.shape({ id: PropTypes.string, key: PropTypes.string, - customConnectionAllowed: PropTypes.bool, - connectionAllowed: PropTypes.bool, + useOnlyPredefinedAuthClients: PropTypes.bool, shared: PropTypes.bool, disabled: PropTypes.bool, }); diff --git a/packages/web/yarn.lock b/packages/web/yarn.lock index 253ca91e..8023fb30 100644 --- a/packages/web/yarn.lock +++ b/packages/web/yarn.lock @@ -2126,6 +2126,11 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz#427d5549943a9c6fce808e39ea64dbe60d4047f1" integrity sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA== +"@simbathesailor/use-what-changed@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@simbathesailor/use-what-changed/-/use-what-changed-2.0.0.tgz#7f82d78f92c8588b5fadd702065dde93bd781403" + integrity sha512-ulBNrPSvfho9UN6zS2fii3AsdEcp2fMaKeqUZZeCNPaZbB6aXyTUhpEN9atjMAbu/eyK3AY8L4SYJUG62Ekocw== + "@sinclair/typebox@^0.24.1": version "0.24.51" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f" @@ -9784,7 +9789,16 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0: +"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@^4.1.0, 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== @@ -9888,7 +9902,14 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.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== @@ -10952,7 +10973,16 @@ workbox-window@6.6.1: "@types/trusted-types" "^2.0.2" workbox-core "6.6.1" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From a1c1287548878c6274f62ed482d190afa2069e88 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Thu, 12 Dec 2024 11:55:56 +0000 Subject: [PATCH 09/45] test(AppAuthClient): remove unused AppConfig import statement --- packages/backend/src/models/app-auth-client.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend/src/models/app-auth-client.test.js b/packages/backend/src/models/app-auth-client.test.js index ddee5c5e..bc4be9fc 100644 --- a/packages/backend/src/models/app-auth-client.test.js +++ b/packages/backend/src/models/app-auth-client.test.js @@ -7,7 +7,6 @@ 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', () => { From bccd56f994197b2c6365fb6d2cec13bb67e2443f Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Thu, 12 Dec 2024 12:36:23 +0000 Subject: [PATCH 10/45] test(user): expect sorted connections in authorizedConnections --- packages/backend/src/models/user.test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/models/user.test.js b/packages/backend/src/models/user.test.js index 4b4d0cd3..159af7ee 100644 --- a/packages/backend/src/models/user.test.js +++ b/packages/backend/src/models/user.test.js @@ -376,7 +376,10 @@ describe('User model', () => { const anotherUserConnection = await createConnection(); expect( - await userWithRoleAndPermissions.authorizedConnections + await userWithRoleAndPermissions.authorizedConnections.orderBy( + 'created_at', + 'asc' + ) ).toStrictEqual([userConnection, anotherUserConnection]); }); From c78646ed8e634f63be3a83d44e05dbcb4c3b0542 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Thu, 12 Dec 2024 22:42:32 +0000 Subject: [PATCH 11/45] feat(connections): iterate add button availability --- .../src/components/AddAppConnection/index.jsx | 1 - .../ChooseConnectionSubstep/index.jsx | 61 +++++++++++++------ packages/web/src/index.jsx | 3 - packages/web/src/locales/en.json | 2 +- packages/web/src/pages/Application/index.jsx | 26 ++++---- 5 files changed, 58 insertions(+), 35 deletions(-) diff --git a/packages/web/src/components/AddAppConnection/index.jsx b/packages/web/src/components/AddAppConnection/index.jsx index 9147ff50..a074b035 100644 --- a/packages/web/src/components/AddAppConnection/index.jsx +++ b/packages/web/src/components/AddAppConnection/index.jsx @@ -18,7 +18,6 @@ import { generateExternalLink } from 'helpers/translationValues'; import { Form } from './style'; import useAppAuth from 'hooks/useAppAuth'; import { useQueryClient } from '@tanstack/react-query'; -import { useWhatChanged } from '@simbathesailor/use-what-changed'; function AddAppConnection(props) { const { application, connectionId, onClose } = props; diff --git a/packages/web/src/components/ChooseConnectionSubstep/index.jsx b/packages/web/src/components/ChooseConnectionSubstep/index.jsx index 816619d7..0c6ef5c8 100644 --- a/packages/web/src/components/ChooseConnectionSubstep/index.jsx +++ b/packages/web/src/components/ChooseConnectionSubstep/index.jsx @@ -22,6 +22,7 @@ import useStepConnection from 'hooks/useStepConnection'; import { useQueryClient } from '@tanstack/react-query'; import useAppConnections from 'hooks/useAppConnections'; import useTestConnection from 'hooks/useTestConnection'; +import useAppAuthClients from 'hooks/useAppAuthClients'; const ADD_CONNECTION_VALUE = 'ADD_CONNECTION'; const ADD_SHARED_CONNECTION_VALUE = 'ADD_SHARED_CONNECTION'; @@ -53,6 +54,7 @@ function ChooseConnectionSubstep(props) { const [showAddSharedConnectionDialog, setShowAddSharedConnectionDialog] = React.useState(false); const queryClient = useQueryClient(); + const { data: appAuthClients } = useAppAuthClients(application.key); const { authenticate } = useAuthenticateApp({ appKey: application.key, @@ -93,24 +95,48 @@ function ChooseConnectionSubstep(props) { appWithConnections?.map((connection) => optionGenerator(connection)) || []; - if ( - !appConfig?.data || - (!appConfig.data?.disabled === false && - appConfig.data?.useOnlyPredefinedAuthClients === false) - ) { - options.push({ - label: formatMessage('chooseConnectionSubstep.addNewConnection'), - value: ADD_CONNECTION_VALUE, - }); + const addCustomConnection = { + label: formatMessage('chooseConnectionSubstep.addNewConnection'), + value: ADD_CONNECTION_VALUE, + }; + + const addConnectionWithAuthClient = { + label: formatMessage( + 'chooseConnectionSubstep.addConnectionWithAuthClient', + ), + value: ADD_SHARED_CONNECTION_VALUE, + }; + + // means there is no app config. defaulting to custom connections only + if (!appConfig?.data) { + return options.concat([addCustomConnection]); } - options.push({ - label: formatMessage('chooseConnectionSubstep.addNewSharedConnection'), - value: ADD_SHARED_CONNECTION_VALUE, - }); + // app is disabled. + if (appConfig.data.disabled) return options; - return options; - }, [data, formatMessage, appConfig?.data]); + // means only auth clients are allowed for connection creation and there is app auth client + if ( + appConfig.data.useOnlyPredefinedAuthClients === true && + appAuthClients.data.length > 0 + ) { + return options.concat([addConnectionWithAuthClient]); + } + + // means there is no app auth client. so we don't show the `addConnectionWithAuthClient` + if ( + appConfig.data.useOnlyPredefinedAuthClients === true && + appAuthClients.data.length === 0 + ) { + return options; + } + + if (appAuthClients.data.length === 0) { + return options.concat([addCustomConnection]); + } + + return options.concat([addCustomConnection, addConnectionWithAuthClient]); + }, [data, formatMessage, appConfig, appAuthClients]); const handleClientClick = async (appAuthClientId) => { try { @@ -161,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); diff --git a/packages/web/src/index.jsx b/packages/web/src/index.jsx index 916baf28..8c03da02 100644 --- a/packages/web/src/index.jsx +++ b/packages/web/src/index.jsx @@ -1,6 +1,5 @@ import { createRoot } from 'react-dom/client'; import { Settings } from 'luxon'; -import { setUseWhatChange } from '@simbathesailor/use-what-changed'; import ThemeProvider from 'components/ThemeProvider'; import IntlProvider from 'components/IntlProvider'; @@ -15,8 +14,6 @@ import reportWebVitals from './reportWebVitals'; // Sets the default locale to English for all luxon DateTime instances created afterwards. Settings.defaultLocale = 'en'; -setUseWhatChange(process.env.NODE_ENV === 'development'); - const container = document.getElementById('root'); const root = createRoot(container); diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index d7786d9e..40073d61 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -74,7 +74,7 @@ "filterConditions.orContinueIf": "OR continue if…", "chooseConnectionSubstep.continue": "Continue", "chooseConnectionSubstep.addNewConnection": "Add new connection", - "chooseConnectionSubstep.addNewSharedConnection": "Add new shared connection", + "chooseConnectionSubstep.addConnectionWithAuthClient": "Add connection with auth client", "chooseConnectionSubstep.chooseConnection": "Choose connection", "flow.createdAt": "created {datetime}", "flow.updatedAt": "updated {datetime}", diff --git a/packages/web/src/pages/Application/index.jsx b/packages/web/src/pages/Application/index.jsx index 1afd7605..73528b3e 100644 --- a/packages/web/src/pages/Application/index.jsx +++ b/packages/web/src/pages/Application/index.jsx @@ -80,15 +80,21 @@ export default function Application() { key: 'addConnection', 'data-test': 'add-connection-button', to: URLS.APP_ADD_CONNECTION(appKey, false), - disabled: !currentUserAbility.can('create', 'Connection'), + disabled: + !currentUserAbility.can('create', 'Connection') || + appConfig?.data?.useOnlyPredefinedAuthClients === true || + appConfig?.data?.disabled === true, }; const addConnectionWithAuthClient = { label: formatMessage('app.addConnectionWithAuthClient'), key: 'addConnectionWithAuthClient', - 'data-test': 'add-custom-connection-button', + 'data-test': 'add-connection-with-auth-client-button', to: URLS.APP_ADD_CONNECTION(appKey, true), - disabled: !currentUserAbility.can('create', 'Connection'), + disabled: + !currentUserAbility.can('create', 'Connection') || + appAuthClients?.data?.length === 0 || + appConfig?.data?.disabled === true, }; // means there is no app config. defaulting to custom connections only @@ -96,16 +102,16 @@ export default function Application() { return [addCustomConnection]; } - // means there is no app auth client. so we don't show the `addConnectionWithAuthClient` - if (appAuthClients?.data?.length === 0) { - return [addCustomConnection]; - } - // means only auth clients are allowed for connection creation if (appConfig?.data?.useOnlyPredefinedAuthClients === true) { return [addConnectionWithAuthClient]; } + // means there is no app auth client. so we don't show the `addConnectionWithAuthClient` + if (appAuthClients?.data?.length === 0) { + return [addCustomConnection]; + } + return [addCustomConnection, addConnectionWithAuthClient]; }, [appKey, appConfig, appAuthClients, currentUserAbility, formatMessage]); @@ -159,9 +165,7 @@ export default function Application() { {(allowed) => ( )} From a8ed13da336c1ce215b25fe834db8ec2acf2e9c3 Mon Sep 17 00:00:00 2001 From: "Jakub P." Date: Mon, 16 Dec 2024 09:15:44 +0100 Subject: [PATCH 12/45] test: adapt apps tests to new logic --- .../admin/application-auth-clients-page.js | 18 +- .../admin/application-settings-page.js | 49 +-- packages/e2e-tests/helpers/db-helpers.js | 32 ++ .../tests/admin/applications.spec.js | 335 +++++++++++++----- .../web/src/pages/AdminApplication/index.jsx | 1 + 5 files changed, 319 insertions(+), 116 deletions(-) create mode 100644 packages/e2e-tests/helpers/db-helpers.js diff --git a/packages/e2e-tests/fixtures/admin/application-auth-clients-page.js b/packages/e2e-tests/fixtures/admin/application-auth-clients-page.js index bedddbf4..c1b852a1 100644 --- a/packages/e2e-tests/fixtures/admin/application-auth-clients-page.js +++ b/packages/e2e-tests/fixtures/admin/application-auth-clients-page.js @@ -9,12 +9,18 @@ export class AdminApplicationAuthClientsPage extends AuthenticatedPage { constructor(page) { super(page); - this.authClientsTab = this.page.getByText('AUTH CLIENTS'); + this.authClientsTab = this.page.getByTestId('auth-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 57858ccb..2e756d28 100644 --- a/packages/e2e-tests/fixtures/admin/application-settings-page.js +++ b/packages/e2e-tests/fixtures/admin/application-settings-page.js @@ -8,56 +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.disableConnectionsSwitch).not.toBeChecked(); - await this.allowCustomConnectionsSwitch.check(); - await this.saveButton.click(); + async allowUseOnlyPredefinedAuthClients() { + await expect(this.useOnlyPredefinedAuthClients).not.toBeChecked(); + await this.useOnlyPredefinedAuthClients.check(); } - async allowSharedConnections() { - await expect(this.disableConnectionsSwitch).not.toBeChecked(); - await this.allowSharedConnectionsSwitch.check(); - 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 this.saveButton.click(); - } - - async disallowCustomConnections() { - await expect(this.disableConnectionsSwitch).toBeChecked(); - await this.allowCustomConnectionsSwitch.uncheck(); - await this.saveButton.click(); - } - - async disallowSharedConnections() { - await expect(this.disableConnectionsSwitch).toBeChecked(); - await this.allowSharedConnectionsSwitch.uncheck(); - await this.saveButton.click(); } async allowConnections() { await expect(this.disableConnectionsSwitch).toBeChecked(); await this.disableConnectionsSwitch.uncheck(); + } + + async saveSettings() { await this.saveButton.click(); } async expectSuccessSnackbarToBeVisible() { - await expect(this.successSnackbar).toHaveCount(1); - await this.successSnackbar.click(); - await expect(this.successSnackbar).toHaveCount(0); + const snackbars = await this.successSnackbar.all(); + for (const snackbar of snackbars) { + await expect(snackbar).toBeVisible(); + } } } 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 2fad49b9..847adc41 100644 --- a/packages/e2e-tests/tests/admin/applications.spec.js +++ b/packages/e2e-tests/tests/admin/applications.spec.js @@ -1,20 +1,37 @@ 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'] + text: 'DELETE FROM app_auth_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); + const deleteAppAuthClientsResult = await pgPool.query( + deleteAppAuthClients + ); expect(deleteAppAuthClientsResult.command).toBe('DELETE'); const deleteAppConfigsResult = await pgPool.query(deleteAppConfigs); expect(deleteAppConfigsResult.command).toBe('DELETE'); @@ -31,39 +48,59 @@ test.describe('Admin Applications', () => { test('Admin should be able to toggle Application settings', async ({ adminApplicationsPage, adminApplicationSettingsPage, - page + page, }) => { 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, + adminApplicationAuthClientsPage, flowEditorPage, - page + page, }) => { - await adminApplicationsPage.openApplication('Spotify'); - await expect(page.url()).toContain('/admin-settings/apps/spotify/settings'); + await insertAppConnection('google-drive'); - await adminApplicationSettingsPage.allowCustomConnections(); - await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); + // 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 adminApplicationAuthClientsPage.openAuthClientsTab(); + await expect( + adminApplicationAuthClientsPage.createFirstAuthClientButton + ).toHaveCount(1); await page.goto('/'); await page.getByTestId('create-flow-button').click(); @@ -72,42 +109,69 @@ test.describe('Admin Applications', () => { ); await expect(flowEditorPage.flowStep).toHaveCount(2); - const triggerStep = flowEditorPage.flowStep.last(); - await triggerStep.click(); - await flowEditorPage.chooseAppAndEvent("Spotify", "Create Playlist"); + await flowEditorPage.chooseAppAndEvent( + 'Google Drive', + 'New files in folder' + ); await flowEditorPage.connectionAutocomplete.click(); - const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' }); - const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' }); + const newConnectionOption = page + .getByRole('option') + .filter({ hasText: 'Add new connection' }); + const newSharedConnectionOption = page + .getByRole('option') + .filter({ hasText: 'Add new connection with auth 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(newSharedConnectionOption).toHaveCount(0); }); - test('should allow only shared connections', async ({ + test('should allow only predefined connections and existing custom', async ({ adminApplicationsPage, adminApplicationSettingsPage, adminApplicationAuthClientsPage, flowEditorPage, - page + page, }) => { - await adminApplicationsPage.openApplication('Reddit'); - await expect(page.url()).toContain('/admin-settings/apps/reddit/settings'); + await insertAppConnection('spotify'); - await adminApplicationSettingsPage.allowSharedConnections(); + await adminApplicationsPage.openApplication('Spotify'); + await expect(page.url()).toContain('/admin-settings/apps/spotify/settings'); + + await expect( + adminApplicationSettingsPage.useOnlyPredefinedAuthClients + ).not.toBeChecked(); + await expect( + adminApplicationSettingsPage.disableConnectionsSwitch + ).not.toBeChecked(); + + await adminApplicationSettingsPage.allowUseOnlyPredefinedAuthClients(); + await adminApplicationSettingsPage.saveSettings(); await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); await adminApplicationAuthClientsPage.openAuthClientsTab(); await adminApplicationAuthClientsPage.openFirstAuthClientCreateForm(); - const authClientForm = page.getByTestId("auth-client-form"); + const authClientForm = page.getByTestId('auth-client-form'); await authClientForm.locator(page.getByTestId('switch')).check(); - await authClientForm.locator(page.locator('[name="name"]')).fill('redditAuthClient'); - await authClientForm.locator(page.locator('[name="clientId"]')).fill('redditClientId'); - await authClientForm.locator(page.locator('[name="clientSecret"]')).fill('redditClientSecret'); + 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 adminApplicationAuthClientsPage.submitAuthClientForm(); - await adminApplicationAuthClientsPage.authClientShouldBeVisible('redditAuthClient'); + await adminApplicationAuthClientsPage.authClientShouldBeVisible( + 'spotifyAuthClient' + ); await page.goto('/'); await page.getByTestId('create-flow-button').click(); @@ -119,28 +183,61 @@ test.describe('Admin Applications', () => { const triggerStep = flowEditorPage.flowStep.last(); await triggerStep.click(); - await flowEditorPage.chooseAppAndEvent("Reddit", "Create link post"); + await flowEditorPage.chooseAppAndEvent('Spotify', 'Create playlist'); await flowEditorPage.connectionAutocomplete.click(); - const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' }); - const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' }); + const newConnectionOption = page + .getByRole('option') + .filter({ hasText: 'Add new connection' }); + const newSharedConnectionOption = page + .getByRole('option') + .filter({ hasText: 'Add connection with auth client' }); + const existingConnection = page + .getByRole('option') + .filter({ hasText: 'Unnamed' }); + await expect(await existingConnection.count()).toBeGreaterThan(0); await expect(newConnectionOption).toHaveCount(0); await expect(newSharedConnectionOption).toBeEnabled(); await expect(newSharedConnectionOption).toHaveCount(1); }); - test('should not allow any connections', async ({ + test('should allow all connections', async ({ adminApplicationsPage, adminApplicationSettingsPage, + adminApplicationAuthClientsPage, flowEditorPage, - page + page, }) => { - await adminApplicationsPage.openApplication('DeepL'); - await expect(page.url()).toContain('/admin-settings/apps/deepl/settings'); + await insertAppConnection('reddit'); - await adminApplicationSettingsPage.disallowConnections(); - await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); + await adminApplicationsPage.openApplication('Reddit'); + await expect(page.url()).toContain('/admin-settings/apps/reddit/settings'); + + await expect( + adminApplicationSettingsPage.useOnlyPredefinedAuthClients + ).not.toBeChecked(); + await expect( + adminApplicationSettingsPage.disableConnectionsSwitch + ).not.toBeChecked(); + + await adminApplicationAuthClientsPage.openAuthClientsTab(); + await adminApplicationAuthClientsPage.openFirstAuthClientCreateForm(); + const authClientForm = page.getByTestId('auth-client-form'); + await authClientForm.locator(page.getByTestId('switch')).check(); + await authClientForm + .locator(page.locator('[name="name"]')) + .fill('redditAuthClient'); + await authClientForm + .locator(page.locator('[name="clientId"]')) + .fill('redditClientId'); + await authClientForm + .locator(page.locator('[name="clientSecret"]')) + .fill('redditClientSecret'); + await adminApplicationAuthClientsPage.submitAuthClientForm(); + await adminApplicationAuthClientsPage.authClientShouldBeVisible( + 'redditAuthClient' + ); await page.goto('/'); await page.getByTestId('create-flow-button').click(); @@ -152,58 +249,126 @@ test.describe('Admin Applications', () => { const triggerStep = flowEditorPage.flowStep.last(); await triggerStep.click(); - await flowEditorPage.chooseAppAndEvent("DeepL", "Translate text"); + await flowEditorPage.chooseAppAndEvent('Reddit', 'Create link post'); await flowEditorPage.connectionAutocomplete.click(); - const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' }); - const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' }); - const noConnectionsOption = page.locator('.MuiAutocomplete-noOptions').filter({ hasText: 'No options' }); + const newConnectionOption = page + .getByRole('option') + .filter({ hasText: 'Add new connection' }); + const newSharedConnectionOption = page + .getByRole('option') + .filter({ hasText: 'Add connection with auth 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(1); + await expect(newSharedConnectionOption).toBeEnabled(); + await expect(newSharedConnectionOption).toHaveCount(1); + }); + + test('should not allow new connections but existing custom', async ({ + adminApplicationsPage, + adminApplicationSettingsPage, + adminApplicationAuthClientsPage, + flowEditorPage, + page, + }) => { + 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 adminApplicationAuthClientsPage.openAuthClientsTab(); + await adminApplicationAuthClientsPage.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 adminApplicationAuthClientsPage.submitAuthClientForm(); + await adminApplicationAuthClientsPage.authClientShouldBeVisible( + 'clickupAuthClient' + ); + + 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); + const triggerStep = flowEditorPage.flowStep.last(); + await triggerStep.click(); + + await flowEditorPage.chooseAppAndEvent('ClickUp', 'Create folder'); + await flowEditorPage.connectionAutocomplete.click(); + + const newConnectionOption = page + .getByRole('option') + .filter({ hasText: 'Add new connection' }); + const newSharedConnectionOption = page + .getByRole('option') + .filter({ hasText: 'Add connection with auth client' }); + const existingConnection = page + .getByRole('option') + .filter({ hasText: 'Unnamed' }); + + await expect(await existingConnection.count()).toBeGreaterThan(0); await expect(newConnectionOption).toHaveCount(0); await expect(newSharedConnectionOption).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 auth clients are enabled', async ({ adminApplicationsPage, adminApplicationSettingsPage, + adminApplicationAuthClientsPage, flowEditorPage, - page + 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 expect(page.url()).toContain( + '/admin-settings/apps/mailchimp/settings' + ); + await adminApplicationSettingsPage.allowUseOnlyPredefinedAuthClients(); await adminApplicationSettingsPage.disallowConnections(); + await adminApplicationSettingsPage.saveSettings(); await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); + await adminApplicationAuthClientsPage.openAuthClientsTab(); + await adminApplicationAuthClientsPage.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 adminApplicationAuthClientsPage.submitAuthClientForm(); + await adminApplicationAuthClientsPage.authClientShouldBeVisible( + 'mailchimpAuthClient' + ); + await page.goto('/'); await page.getByTestId('create-flow-button').click(); await page.waitForURL( @@ -214,14 +379,22 @@ test.describe('Admin Applications', () => { const triggerStep = flowEditorPage.flowStep.last(); await triggerStep.click(); - await flowEditorPage.chooseAppAndEvent("Mailchimp", "Create campaign"); + await flowEditorPage.chooseAppAndEvent('Mailchimp', 'Create campaign'); await flowEditorPage.connectionAutocomplete.click(); await expect(page.getByRole('option').first()).toHaveText('Unnamed'); - const existingConnection = page.getByRole('option').filter({ hasText: 'Unnamed' }); - const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' }); - const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' }); - const noConnectionsOption = page.locator('.MuiAutocomplete-noOptions').filter({ hasText: 'No options' }); + const existingConnection = page + .getByRole('option') + .filter({ hasText: 'Unnamed' }); + const newConnectionOption = page + .getByRole('option') + .filter({ hasText: 'Add new connection' }); + const newSharedConnectionOption = page + .getByRole('option') + .filter({ hasText: 'Add new shared connection' }); + const noConnectionsOption = page + .locator('.MuiAutocomplete-noOptions') + .filter({ hasText: 'No options' }); await expect(await existingConnection.count()).toBeGreaterThan(0); await expect(noConnectionsOption).toHaveCount(0); diff --git a/packages/web/src/pages/AdminApplication/index.jsx b/packages/web/src/pages/AdminApplication/index.jsx index 850f4a0e..85e15bba 100644 --- a/packages/web/src/pages/AdminApplication/index.jsx +++ b/packages/web/src/pages/AdminApplication/index.jsx @@ -87,6 +87,7 @@ export default function AdminApplication() { component={Link} /> Date: Tue, 17 Dec 2024 17:32:31 +0000 Subject: [PATCH 13/45] refactor(Form): centralize error management --- packages/web/src/components/Form/index.jsx | 50 +++++++++++++++++-- .../src/components/InstallationForm/index.jsx | 25 +--------- packages/web/src/hooks/useFormatMessage.js | 10 +++- packages/web/src/locales/en.json | 2 +- 4 files changed, 59 insertions(+), 28 deletions(-) diff --git a/packages/web/src/components/Form/index.jsx b/packages/web/src/components/Form/index.jsx index 352b0a69..d501e5a8 100644 --- a/packages/web/src/components/Form/index.jsx +++ b/packages/web/src/components/Form/index.jsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { FormProvider, useForm, useWatch } from 'react-hook-form'; import PropTypes from 'prop-types'; import isEqual from 'lodash/isEqual'; +import useFormatMessage from 'hooks/useFormatMessage'; const noop = () => null; @@ -18,6 +19,8 @@ function Form(props) { ...formProps } = props; + const formatMessage = useFormatMessage(); + const methods = useForm({ defaultValues, reValidateMode, @@ -25,6 +28,8 @@ function Form(props) { mode, }); + const { setError } = methods; + const form = useWatch({ control: methods.control }); const prevDefaultValues = React.useRef(defaultValues); @@ -44,12 +49,51 @@ function Form(props) { } }, [defaultValues]); + const handleErrors = React.useCallback( + function (errors) { + if (!errors) return; + + let shouldSetGenericGeneralError = true; + const fieldNames = Object.keys(defaultValues); + + Object.entries(errors).forEach(([fieldName, fieldErrors]) => { + if (fieldNames.includes(fieldName) && Array.isArray(fieldErrors)) { + shouldSetGenericGeneralError = false; + setError(fieldName, { + type: 'fieldRequestError', + message: fieldErrors.join(', '), + }); + } + }); + + // in case of general errors + if (Array.isArray(errors.general)) { + for (const error of errors.general) { + shouldSetGenericGeneralError = false; + setError('root.general', { type: 'requestError', message: error }); + } + } + + if (shouldSetGenericGeneralError) { + setError('root.general', { + type: 'requestError', + message: formatMessage('form.genericError'), + }); + } + }, + [defaultValues, formatMessage, setError], + ); + return ( - onSubmit?.(data, event, methods.setError), - )} + onSubmit={methods.handleSubmit(async (data, event) => { + try { + return await onSubmit?.(data); + } catch (errors) { + handleErrors(errors); + } + })} {...formProps} > {render ? render(methods) : children} diff --git a/packages/web/src/components/InstallationForm/index.jsx b/packages/web/src/components/InstallationForm/index.jsx index 6e443302..f1e30051 100644 --- a/packages/web/src/components/InstallationForm/index.jsx +++ b/packages/web/src/components/InstallationForm/index.jsx @@ -68,7 +68,7 @@ function InstallationForm() { }); }; - const handleSubmit = async ({ fullName, email, password }, e, setError) => { + const handleSubmit = async ({ fullName, email, password }) => { try { await install({ fullName, @@ -77,29 +77,8 @@ function InstallationForm() { }); } catch (error) { const errors = error?.response?.data?.errors; - if (errors) { - const fieldNames = Object.keys(defaultValues); - Object.entries(errors).forEach(([fieldName, fieldErrors]) => { - if (fieldNames.includes(fieldName) && Array.isArray(fieldErrors)) { - setError(fieldName, { - type: 'fieldRequestError', - message: fieldErrors.join(', '), - }); - } - }); - } - const generalError = getGeneralErrorMessage({ - error, - fallbackMessage: formatMessage('installationForm.error'), - }); - - if (generalError) { - setError('root.general', { - type: 'requestError', - message: generalError, - }); - } + throw errors; } }; diff --git a/packages/web/src/hooks/useFormatMessage.js b/packages/web/src/hooks/useFormatMessage.js index ef76fe0c..62e95e28 100644 --- a/packages/web/src/hooks/useFormatMessage.js +++ b/packages/web/src/hooks/useFormatMessage.js @@ -1,5 +1,13 @@ +import * as React from 'react'; import { useIntl } from 'react-intl'; + export default function useFormatMessage() { const { formatMessage } = useIntl(); - return (id, values = {}) => formatMessage({ id }, values); + + const customFormatMessage = React.useCallback( + (id, values = {}) => formatMessage({ id }, values), + [formatMessage], + ); + + return customFormatMessage; } diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index 941af1fa..19c8aec4 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -130,6 +130,7 @@ "webhookUrlInfo.description": "You'll need to configure your application with this webhook URL.", "webhookUrlInfo.helperText": "We've generated a custom webhook URL for you to send requests to. Learn more about webhooks.", "webhookUrlInfo.copy": "Copy", + "form.genericError": "Something went wrong. Please try again.", "installationForm.title": "Installation", "installationForm.fullNameFieldLabel": "Full name", "installationForm.emailFieldLabel": "Email", @@ -141,7 +142,6 @@ "installationForm.passwordMinLength": "Password must be at least 6 characters long.", "installationForm.mandatoryInput": "{inputName} is required.", "installationForm.success": "The admin account has been created, and thus, the installation has been completed. You can now log in here.", - "installationForm.error": "Something went wrong. Please try again.", "signupForm.title": "Sign up", "signupForm.fullNameFieldLabel": "Full name", "signupForm.emailFieldLabel": "Email", From 8c4b67e147117eafd46097fb4ea976aa270887d0 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Wed, 18 Dec 2024 17:42:04 +0100 Subject: [PATCH 14/45] refactor: Rename AppAuthClient model as OAuthClient --- ...client.ee.js => create-oauth-client.ee.js} | 10 +- ...test.js => create-oauth-client.ee.test.js} | 24 +-- .../api/v1/admin/apps/get-auth-client.ee.js | 11 - .../api/v1/admin/apps/get-oauth-client.ee.js | 11 + ...ee.test.js => get-oauth-client.ee.test.js} | 24 +-- ...-clients.ee.js => get-oauth-clients.ee.js} | 6 +- ...e.test.js => get-oauth-clients.ee.test.js} | 20 +- .../v1/admin/apps/update-auth-client.ee.js | 22 -- .../v1/admin/apps/update-oauth-client.ee.js | 22 ++ ...test.js => update-oauth-client.ee.test.js} | 38 ++-- .../api/v1/apps/create-connection.js | 6 +- .../api/v1/apps/create-connection.test.js | 14 +- .../api/v1/apps/get-action-substeps.test.js | 2 +- .../api/v1/apps/get-auth-client.ee.js | 11 - .../controllers/api/v1/apps/get-config.ee.js | 2 +- .../api/v1/apps/get-connections.js | 2 +- .../api/v1/apps/get-oauth-client.ee.js | 11 + ...ee.test.js => get-oauth-client.ee.test.js} | 24 +-- ...-clients.ee.js => get-oauth-clients.ee.js} | 6 +- ...e.test.js => get-oauth-clients.ee.test.js} | 20 +- .../api/v1/apps/get-trigger-substeps.test.js | 2 +- .../api/v1/connections/update-connection.js | 4 +- ...hange_app_auth_clients_as_oauth_clients.js | 31 +++ .../src/helpers/add-authentication-steps.js | 4 +- .../__snapshots__/connection.test.js.snap | 8 +- ...test.js.snap => oauth-client.test.js.snap} | 2 +- .../src/models/app-auth-client.test.js | 202 ------------------ packages/backend/src/models/app-config.js | 8 +- .../backend/src/models/app-config.test.js | 8 +- packages/backend/src/models/connection.js | 26 +-- .../backend/src/models/connection.test.js | 26 +-- .../{app-auth-client.js => oauth-client.js} | 8 +- .../backend/src/models/oauth-client.test.js | 192 +++++++++++++++++ .../src/routes/api/v1/admin/apps.ee.js | 24 +-- packages/backend/src/routes/api/v1/apps.js | 12 +- .../src/serializers/app-auth-client.js | 10 - .../src/serializers/app-auth-client.test.js | 24 --- .../backend/src/serializers/connection.js | 2 +- .../src/serializers/connection.test.js | 2 +- packages/backend/src/serializers/index.js | 4 +- .../backend/src/serializers/oauth-client.js | 10 + .../src/serializers/oauth-client.test.js | 22 ++ .../{app-auth-client.js => oauth-client.js} | 8 +- .../api/v1/admin/apps/create-auth-client.js | 17 -- .../api/v1/admin/apps/create-oauth-client.js | 17 ++ .../rest/api/v1/admin/apps/get-auth-client.js | 18 -- .../api/v1/admin/apps/get-auth-clients.js | 18 -- .../api/v1/admin/apps/get-oauth-client.js | 18 ++ .../api/v1/admin/apps/get-oauth-clients.js | 18 ++ .../api/v1/admin/apps/update-auth-client.js | 18 -- .../api/v1/admin/apps/update-oauth-client.js | 18 ++ .../rest/api/v1/apps/create-connection.js | 2 +- .../mocks/rest/api/v1/apps/get-auth-client.js | 18 -- .../rest/api/v1/apps/get-auth-clients.js | 18 -- .../mocks/rest/api/v1/apps/get-connections.js | 2 +- .../rest/api/v1/apps/get-oauth-client.js | 18 ++ .../rest/api/v1/apps/get-oauth-clients.js | 18 ++ .../api/v1/connections/reset-connection.js | 2 +- .../api/v1/connections/update-connection.js | 2 +- .../mocks/rest/api/v1/steps/get-connection.js | 2 +- .../tests/admin/applications.spec.js | 10 +- 61 files changed, 588 insertions(+), 571 deletions(-) rename packages/backend/src/controllers/api/v1/admin/apps/{create-auth-client.ee.js => create-oauth-client.ee.js} (66%) rename packages/backend/src/controllers/api/v1/admin/apps/{create-auth-client.ee.test.js => create-oauth-client.ee.test.js} (81%) delete mode 100644 packages/backend/src/controllers/api/v1/admin/apps/get-auth-client.ee.js create mode 100644 packages/backend/src/controllers/api/v1/admin/apps/get-oauth-client.ee.js rename packages/backend/src/controllers/api/v1/admin/apps/{get-auth-client.ee.test.js => get-oauth-client.ee.test.js} (56%) rename packages/backend/src/controllers/api/v1/admin/apps/{get-auth-clients.ee.js => get-oauth-clients.ee.js} (54%) rename packages/backend/src/controllers/api/v1/admin/apps/{get-auth-clients.ee.test.js => get-oauth-clients.ee.test.js} (62%) delete mode 100644 packages/backend/src/controllers/api/v1/admin/apps/update-auth-client.ee.js create mode 100644 packages/backend/src/controllers/api/v1/admin/apps/update-oauth-client.ee.js rename packages/backend/src/controllers/api/v1/admin/apps/{update-auth-client.ee.test.js => update-oauth-client.ee.test.js} (65%) delete mode 100644 packages/backend/src/controllers/api/v1/apps/get-auth-client.ee.js create mode 100644 packages/backend/src/controllers/api/v1/apps/get-oauth-client.ee.js rename packages/backend/src/controllers/api/v1/apps/{get-auth-client.ee.test.js => get-oauth-client.ee.test.js} (54%) rename packages/backend/src/controllers/api/v1/apps/{get-auth-clients.ee.js => get-oauth-clients.ee.js} (56%) rename packages/backend/src/controllers/api/v1/apps/{get-auth-clients.ee.test.js => get-oauth-clients.ee.test.js} (59%) create mode 100644 packages/backend/src/db/migrations/20241217170447_change_app_auth_clients_as_oauth_clients.js rename packages/backend/src/models/__snapshots__/{app-auth-client.test.js.snap => oauth-client.test.js.snap} (87%) delete mode 100644 packages/backend/src/models/app-auth-client.test.js rename packages/backend/src/models/{app-auth-client.js => oauth-client.js} (93%) create mode 100644 packages/backend/src/models/oauth-client.test.js delete mode 100644 packages/backend/src/serializers/app-auth-client.js delete mode 100644 packages/backend/src/serializers/app-auth-client.test.js create mode 100644 packages/backend/src/serializers/oauth-client.js create mode 100644 packages/backend/src/serializers/oauth-client.test.js rename packages/backend/test/factories/{app-auth-client.js => oauth-client.js} (67%) delete mode 100644 packages/backend/test/mocks/rest/api/v1/admin/apps/create-auth-client.js create mode 100644 packages/backend/test/mocks/rest/api/v1/admin/apps/create-oauth-client.js delete mode 100644 packages/backend/test/mocks/rest/api/v1/admin/apps/get-auth-client.js delete mode 100644 packages/backend/test/mocks/rest/api/v1/admin/apps/get-auth-clients.js create mode 100644 packages/backend/test/mocks/rest/api/v1/admin/apps/get-oauth-client.js create mode 100644 packages/backend/test/mocks/rest/api/v1/admin/apps/get-oauth-clients.js delete mode 100644 packages/backend/test/mocks/rest/api/v1/admin/apps/update-auth-client.js create mode 100644 packages/backend/test/mocks/rest/api/v1/admin/apps/update-oauth-client.js delete mode 100644 packages/backend/test/mocks/rest/api/v1/apps/get-auth-client.js delete mode 100644 packages/backend/test/mocks/rest/api/v1/apps/get-auth-clients.js create mode 100644 packages/backend/test/mocks/rest/api/v1/apps/get-oauth-client.js create mode 100644 packages/backend/test/mocks/rest/api/v1/apps/get-oauth-clients.js 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-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/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 c73df6b6..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'; @@ -267,7 +267,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => { }); describe('with auth client enabled', async () => { - let appAuthClient; + let oauthClient; beforeEach(async () => { await createAppConfig({ @@ -276,7 +276,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => { 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) @@ -338,7 +338,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => { }); describe('with auth client disabled', async () => { - let appAuthClient; + let oauthClient; beforeEach(async () => { await createAppConfig({ @@ -347,7 +347,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => { useOnlyPredefinedAuthClients: false, }); - appAuthClient = await createAppAuthClient({ + oauthClient = await createOAuthClient({ appKey: 'gitlab', active: false, }); @@ -355,7 +355,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => { it('should return with not authorized response', async () => { const connectionData = { - appAuthClientId: appAuthClient.id, + oauthClientId: oauthClient.id, }; await request(app) 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-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-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/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/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/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/app-auth-client.test.js b/packages/backend/src/models/app-auth-client.test.js deleted file mode 100644 index bc4be9fc..00000000 --- a/packages/backend/src/models/app-auth-client.test.js +++ /dev/null @@ -1,202 +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'; - -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); - }); - }); - - it('$beforeInsert should call AppAuthClient.encryptData', async () => { - const appAuthClientBeforeInsertSpy = vi.spyOn( - AppAuthClient.prototype, - 'encryptData' - ); - - await createAppAuthClient(); - - expect(appAuthClientBeforeInsertSpy).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('$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 6763e9f8..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 { @@ -24,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', }, }, }); diff --git a/packages/backend/src/models/app-config.test.js b/packages/backend/src/models/app-config.test.js index 2e6f05be..a68b393f 100644 --- a/packages/backend/src/models/app-config.test.js +++ b/packages/backend/src/models/app-config.test.js @@ -3,7 +3,7 @@ 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 OAuthClient from './oauth-client.js'; describe('AppConfig model', () => { it('tableName should return correct name', () => { @@ -22,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', }, }, }; diff --git a/packages/backend/src/models/connection.js b/packages/backend/src/models/connection.js index 4a8d5351..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' }, @@ -69,12 +69,12 @@ 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', }, }, }); @@ -136,8 +136,8 @@ class Connection extends Base { if (!this.formattedData) { const authClient = await appConfig - .$relatedQuery('appAuthClients') - .findById(this.appAuthClientId) + .$relatedQuery('oauthClients') + .findById(this.oauthClientId) .where({ active: true }) .throwIfNotFound(); @@ -215,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 329fdfe6..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', () => { @@ -61,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', }, }, }; @@ -307,13 +307,13 @@ describe('Connection model', () => { ); }); - 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, }); - const appAuthClient = await createAppAuthClient({ + const oauthClient = await createOAuthClient({ appKey: 'gitlab', active: true, formattedAuthDefaults: { @@ -323,7 +323,7 @@ describe('Connection model', () => { const connection = await createConnection({ key: 'gitlab', - appAuthClientId: appAuthClient.id, + oauthClientId: oauthClient.id, formattedData: null, }); @@ -559,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 93% rename from packages/backend/src/models/app-auth-client.js rename to packages/backend/src/models/oauth-client.js index 48800841..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', }, }, @@ -87,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/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/connection.js b/packages/backend/src/serializers/connection.js index 388a6b87..70224476 100644 --- a/packages/backend/src/serializers/connection.js +++ b/packages/backend/src/serializers/connection.js @@ -2,7 +2,7 @@ const connectionSerializer = (connection) => { return { id: connection.id, key: connection.key, - 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 3ea7b324..bb9db58a 100644 --- a/packages/backend/src/serializers/connection.test.js +++ b/packages/backend/src/serializers/connection.test.js @@ -13,7 +13,7 @@ describe('connectionSerializer', () => { const expectedPayload = { id: connection.id, key: connection.key, - 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 3111b2df..4525b5ae 100644 --- a/packages/backend/src/serializers/index.js +++ b/packages/backend/src/serializers/index.js @@ -4,7 +4,7 @@ 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'; @@ -28,7 +28,7 @@ const serializers = { AdminSamlAuthProvider: adminSamlAuthProviderSerializer, SamlAuthProvider: samlAuthProviderSerializer, RoleMapping: samlAuthProviderRoleMappingSerializer, - AppAuthClient: appAuthClientSerializer, + OAuthClient: oauthClientSerializer, AppConfig: appConfigSerializer, Flow: flowSerializer, Step: stepSerializer, 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/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/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-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/apps/create-connection.js b/packages/backend/test/mocks/rest/api/v1/apps/create-connection.js index 2eb1fd7f..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,7 +2,7 @@ const createConnection = (connection) => { const connectionData = { id: connection.id, key: connection.key, - 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-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-connections.js b/packages/backend/test/mocks/rest/api/v1/apps/get-connections.js index bd3bfa4c..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 @@ -4,7 +4,7 @@ const getConnectionsMock = (connections) => { id: connection.id, key: connection.key, 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 0d8131c8..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,7 +3,7 @@ const resetConnectionMock = (connection) => { id: connection.id, key: connection.key, verified: connection.verified, - 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 d46b9a0c..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,7 +3,7 @@ const updateConnectionMock = (connection) => { id: connection.id, key: connection.key, verified: connection.verified, - 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 831a148a..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,7 +3,7 @@ const getConnectionMock = async (connection) => { id: connection.id, key: connection.key, verified: connection.verified, - appAuthClientId: connection.appAuthClientId, + oauthClientId: connection.oauthClientId, formattedData: { screenName: connection.formattedData.screenName, }, diff --git a/packages/e2e-tests/tests/admin/applications.spec.js b/packages/e2e-tests/tests/admin/applications.spec.js index 847adc41..d5bfb4ba 100644 --- a/packages/e2e-tests/tests/admin/applications.spec.js +++ b/packages/e2e-tests/tests/admin/applications.spec.js @@ -4,8 +4,8 @@ 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, $6)', + const deleteOAuthClients = { + text: 'DELETE FROM oauth_clients WHERE app_key in ($1, $2, $3, $4, $5, $6)', values: [ 'carbone', 'spotify', @@ -29,10 +29,8 @@ test.describe('Admin Applications', () => { }; 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) { From d320e8eec6d0a348f075930e32c99d469e9a729a Mon Sep 17 00:00:00 2001 From: "kasia.oczkowska" Date: Thu, 19 Dec 2024 12:21:22 +0000 Subject: [PATCH 15/45] feat: disable inputs when data is loading --- .../PermissionCatalogFieldLoader/index.jsx | 4 +- .../PermissionCatalogField/index.ee.jsx | 5 +- .../web/src/pages/CreateRole/index.ee.jsx | 5 +- packages/web/src/pages/EditRole/index.ee.jsx | 50 ++++++++----------- 4 files changed, 32 insertions(+), 32 deletions(-) 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 5f2bf909..21c89f81 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/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 && ( - <> - - - - - )} + + Date: Thu, 19 Dec 2024 14:53:47 +0000 Subject: [PATCH 16/45] refactor(web): use oauth client instead of app auth client --- .../src/components/AddAppConnection/index.jsx | 26 +++++------ .../index.jsx | 28 +++++------ .../index.jsx | 12 ++--- .../style.js | 0 .../index.jsx | 34 ++++++++------ .../index.jsx | 46 +++++++++---------- .../AppConnectionContextMenu/index.jsx | 2 +- .../ChooseConnectionSubstep/index.jsx | 32 ++++++------- .../index.ee.jsx | 22 ++++----- packages/web/src/config/urls.js | 20 ++++---- .../web/src/hooks/useAdminAppAuthClient.ee.js | 19 -------- ....ee.js => useAdminCreateOAuthClient.ee.js} | 11 +++-- .../web/src/hooks/useAdminOAuthClient.ee.js | 22 +++++++++ ...AuthClients.js => useAdminOAuthClients.js} | 6 +-- ....ee.js => useAdminUpdateOAuthClient.ee.js} | 12 ++--- .../web/src/hooks/useAuthenticateApp.ee.js | 10 ++-- packages/web/src/hooks/useCreateConnection.js | 4 +- ...seAppAuthClients.js => useOAuthClients.js} | 6 +-- packages/web/src/hooks/useUpdateConnection.js | 4 +- packages/web/src/locales/en.json | 28 +++++------ .../web/src/pages/AdminApplication/index.jsx | 28 +++++------ packages/web/src/pages/Application/index.jsx | 24 +++++----- packages/web/src/propTypes/propTypes.js | 4 +- 23 files changed, 206 insertions(+), 194 deletions(-) rename packages/web/src/components/{AdminApplicationCreateAuthClient => AdminApplicationCreateOAuthClient}/index.jsx (74%) rename packages/web/src/components/{AdminApplicationAuthClientDialog => AdminApplicationOAuthClientDialog}/index.jsx (89%) rename packages/web/src/components/{AdminApplicationAuthClientDialog => AdminApplicationOAuthClientDialog}/style.js (100%) rename packages/web/src/components/{AdminApplicationAuthClients => AdminApplicationOAuthClients}/index.jsx (70%) rename packages/web/src/components/{AdminApplicationUpdateAuthClient => AdminApplicationUpdateOAuthClient}/index.jsx (56%) rename packages/web/src/components/{AppAuthClientsDialog => OAuthClientsDialog}/index.ee.jsx (57%) delete mode 100644 packages/web/src/hooks/useAdminAppAuthClient.ee.js rename packages/web/src/hooks/{useAdminCreateAppAuthClient.ee.js => useAdminCreateOAuthClient.ee.js} (57%) create mode 100644 packages/web/src/hooks/useAdminOAuthClient.ee.js rename packages/web/src/hooks/{useAdminAppAuthClients.js => useAdminOAuthClients.js} (56%) rename packages/web/src/hooks/{useAdminUpdateAppAuthClient.ee.js => useAdminUpdateOAuthClient.ee.js} (57%) rename packages/web/src/hooks/{useAppAuthClients.js => useOAuthClients.js} (58%) diff --git a/packages/web/src/components/AddAppConnection/index.jsx b/packages/web/src/components/AddAppConnection/index.jsx index a074b035..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, key, navigate], + [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; @@ -43,7 +43,7 @@ function AdminApplicationCreateAuthClient(props) { 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/AdminApplicationUpdateAuthClient/index.jsx b/packages/web/src/components/AdminApplicationUpdateOAuthClient/index.jsx similarity index 56% rename from packages/web/src/components/AdminApplicationUpdateAuthClient/index.jsx rename to packages/web/src/components/AdminApplicationUpdateOAuthClient/index.jsx index 0dc3d795..18389bed 100644 --- a/packages/web/src/components/AdminApplicationUpdateAuthClient/index.jsx +++ b/packages/web/src/components/AdminApplicationUpdateOAuthClient/index.jsx @@ -4,26 +4,26 @@ import { useParams } from 'react-router-dom'; import { AppPropType } from 'propTypes/propTypes'; import useFormatMessage from 'hooks/useFormatMessage'; -import AdminApplicationAuthClientDialog from 'components/AdminApplicationAuthClientDialog'; -import useAdminAppAuthClient from 'hooks/useAdminAppAuthClient.ee'; -import useAdminUpdateAppAuthClient from 'hooks/useAdminUpdateAppAuthClient.ee'; +import AdminApplicationOAuthClientDialog from 'components/AdminApplicationOAuthClientDialog'; +import useAdminOAuthClient from 'hooks/useAdminOAuthClient.ee'; +import useAdminUpdateOAuthClient from 'hooks/useAdminUpdateOAuthClient.ee'; import useAppAuth from 'hooks/useAppAuth'; -function AdminApplicationUpdateAuthClient(props) { +function AdminApplicationUpdateOAuthClient(props) { const { application, onClose } = props; const formatMessage = useFormatMessage(); const { clientId } = useParams(); - const { data: adminAppAuthClient, isLoading: isAdminAuthClientLoading } = - useAdminAppAuthClient(application.key, clientId); + const { data: adminOAuthClient, isLoading: isAdminOAuthClientLoading } = + useAdminOAuthClient(application.key, clientId); const { data: auth } = useAppAuth(application.key); const { - mutateAsync: updateAppAuthClient, - isPending: isUpdateAppAuthClientPending, - error: updateAppAuthClientError, - } = useAdminUpdateAppAuthClient(application.key, clientId); + mutateAsync: updateOAuthClient, + isPending: isUpdateOAuthClientPending, + error: updateOAuthClientError, + } = useAdminUpdateOAuthClient(application.key, clientId); const authFields = auth?.data?.fields?.map((field) => ({ ...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/AppConnectionContextMenu/index.jsx b/packages/web/src/components/AppConnectionContextMenu/index.jsx index f17fb860..8e7eb318 100644 --- a/packages/web/src/components/AppConnectionContextMenu/index.jsx +++ b/packages/web/src/components/AppConnectionContextMenu/index.jsx @@ -70,7 +70,7 @@ function ContextMenu(props) { to={URLS.APP_RECONNECT_CONNECTION( appKey, connection.id, - connection.appAuthClientId, + connection.oauthClientId, )} onClick={createActionHandler({ type: 'reconnect' })} > diff --git a/packages/web/src/components/ChooseConnectionSubstep/index.jsx b/packages/web/src/components/ChooseConnectionSubstep/index.jsx index 0c6ef5c8..2f5df9ee 100644 --- a/packages/web/src/components/ChooseConnectionSubstep/index.jsx +++ b/packages/web/src/components/ChooseConnectionSubstep/index.jsx @@ -7,7 +7,7 @@ import TextField from '@mui/material/TextField'; import * as React from 'react'; import AddAppConnection from 'components/AddAppConnection'; -import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee'; +import AppOAuthClientsDialog from 'components/OAuthClientsDialog/index.ee'; import FlowSubstepTitle from 'components/FlowSubstepTitle'; import useAppConfig from 'hooks/useAppConfig.ee'; import { EditorContext } from 'contexts/Editor'; @@ -22,7 +22,7 @@ import useStepConnection from 'hooks/useStepConnection'; import { useQueryClient } from '@tanstack/react-query'; import useAppConnections from 'hooks/useAppConnections'; import useTestConnection from 'hooks/useTestConnection'; -import useAppAuthClients from 'hooks/useAppAuthClients'; +import useOAuthClients from 'hooks/useOAuthClients'; const ADD_CONNECTION_VALUE = 'ADD_CONNECTION'; const ADD_SHARED_CONNECTION_VALUE = 'ADD_SHARED_CONNECTION'; @@ -54,7 +54,7 @@ function ChooseConnectionSubstep(props) { const [showAddSharedConnectionDialog, setShowAddSharedConnectionDialog] = React.useState(false); const queryClient = useQueryClient(); - const { data: appAuthClients } = useAppAuthClients(application.key); + const { data: appOAuthClients } = useOAuthClients(application.key); const { authenticate } = useAuthenticateApp({ appKey: application.key, @@ -100,9 +100,9 @@ function ChooseConnectionSubstep(props) { value: ADD_CONNECTION_VALUE, }; - const addConnectionWithAuthClient = { + const addConnectionWithOAuthClient = { label: formatMessage( - 'chooseConnectionSubstep.addConnectionWithAuthClient', + 'chooseConnectionSubstep.addConnectionWithOAuthClient', ), value: ADD_SHARED_CONNECTION_VALUE, }; @@ -115,33 +115,33 @@ function ChooseConnectionSubstep(props) { // app is disabled. if (appConfig.data.disabled) return options; - // means only auth clients are allowed for connection creation and there is app auth client + // means only OAuth clients are allowed for connection creation and there is OAuth client if ( appConfig.data.useOnlyPredefinedAuthClients === true && - appAuthClients.data.length > 0 + appOAuthClients.data.length > 0 ) { - return options.concat([addConnectionWithAuthClient]); + return options.concat([addConnectionWithOAuthClient]); } - // means there is no app auth client. so we don't show the `addConnectionWithAuthClient` + // means there is no OAuth client. so we don't show the `addConnectionWithOAuthClient` if ( appConfig.data.useOnlyPredefinedAuthClients === true && - appAuthClients.data.length === 0 + appOAuthClients.data.length === 0 ) { return options; } - if (appAuthClients.data.length === 0) { + if (appOAuthClients.data.length === 0) { return options.concat([addCustomConnection]); } - return options.concat([addCustomConnection, addConnectionWithAuthClient]); - }, [data, formatMessage, appConfig, appAuthClients]); + return options.concat([addCustomConnection, addConnectionWithOAuthClient]); + }, [data, formatMessage, appConfig, appOAuthClients]); - const handleClientClick = async (appAuthClientId) => { + const handleClientClick = async (oauthClientId) => { try { const response = await authenticate?.({ - appAuthClientId, + oauthClientId, }); const connectionId = response?.createConnection.id; @@ -292,7 +292,7 @@ function ChooseConnectionSubstep(props) { )} {application && showAddSharedConnectionDialog && ( - setShowAddSharedConnectionDialog(false)} onClientClick={handleClientClick} diff --git a/packages/web/src/components/AppAuthClientsDialog/index.ee.jsx b/packages/web/src/components/OAuthClientsDialog/index.ee.jsx similarity index 57% rename from packages/web/src/components/AppAuthClientsDialog/index.ee.jsx rename to packages/web/src/components/OAuthClientsDialog/index.ee.jsx index eea6674b..c8e204cb 100644 --- a/packages/web/src/components/AppAuthClientsDialog/index.ee.jsx +++ b/packages/web/src/components/OAuthClientsDialog/index.ee.jsx @@ -6,26 +6,26 @@ 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 useOAuthClients from 'hooks/useOAuthClients'; import useFormatMessage from 'hooks/useFormatMessage'; -function AppAuthClientsDialog(props) { +function AppOAuthClientsDialog(props) { const { appKey, onClientClick, onClose } = props; - const { data: appAuthClients } = useAppAuthClients(appKey); + const { data: appOAuthClients } = useOAuthClients(appKey); const formatMessage = useFormatMessage(); - if (!appAuthClients?.data.length) return ; + if (!appOAuthClients?.data.length) return ; return ( - {formatMessage('appAuthClientsDialog.title')} + {formatMessage('appOAuthClientsDialog.title')} - {appAuthClients.data.map((appAuthClient) => ( - - onClientClick(appAuthClient.id)}> - + {appOAuthClients.data.map((oauthClient) => ( + + onClientClick(oauthClient.id)}> + ))} @@ -34,10 +34,10 @@ function AppAuthClientsDialog(props) { ); } -AppAuthClientsDialog.propTypes = { +AppOAuthClientsDialog.propTypes = { appKey: PropTypes.string.isRequired, onClientClick: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, }; -export default AppAuthClientsDialog; +export default AppOAuthClientsDialog; 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 45c17bfb..061b8e88 100644 --- a/packages/web/src/hooks/useAuthenticateApp.ee.js +++ b/packages/web/src/hooks/useAuthenticateApp.ee.js @@ -31,7 +31,7 @@ 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); @@ -55,7 +55,7 @@ export default function useAuthenticateApp(payload) { const response = { key: appKey, - appAuthClientId: appAuthClientId || payload.appAuthClientId, + oauthClientId: oauthClientId || payload.oauthClientId, connectionId, fields, }; @@ -133,7 +133,7 @@ export default function useAuthenticateApp(payload) { }, [ steps, appKey, - appAuthClientId, + oauthClientId, connectionId, queryClient, createConnection, @@ -147,7 +147,7 @@ export default function useAuthenticateApp(payload) { [ steps, appKey, - appAuthClientId, + oauthClientId, connectionId, queryClient, createConnection, @@ -156,7 +156,7 @@ export default function useAuthenticateApp(payload) { resetConnection, verifyConnection, ], - 'steps, appKey, appAuthClientId, connectionId, queryClient, createConnection, createConnectionAuthUrl, updateConnection, resetConnection, verifyConnection', + 'steps, appKey, oauthClientId, connectionId, queryClient, createConnection, createConnectionAuthUrl, updateConnection, resetConnection, verifyConnection', '', 'useAuthenticate', ); diff --git a/packages/web/src/hooks/useCreateConnection.js b/packages/web/src/hooks/useCreateConnection.js index 7615ab6d..9c09f8b6 100644 --- a/packages/web/src/hooks/useCreateConnection.js +++ b/packages/web/src/hooks/useCreateConnection.js @@ -4,9 +4,9 @@ import api from 'helpers/api'; export default function useCreateConnection(appKey) { const mutation = useMutation({ - mutationFn: async ({ appAuthClientId, formattedData }) => { + mutationFn: async ({ oauthClientId, formattedData }) => { const { data } = await api.post(`/v1/apps/${appKey}/connections`, { - appAuthClientId, + oauthClientId, formattedData, }); 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 40073d61..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.addConnectionWithAuthClient": "Add connection with auth client", + "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.addConnectionWithAuthClient": "Add connection with auth client", + "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.useOnlyPredefinedAuthClients": "Use only predefined auth clients", + "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 85e15bba..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 } > } /> } + path={`/oauth-clients/*`} + element={} /> diff --git a/packages/web/src/pages/Application/index.jsx b/packages/web/src/pages/Application/index.jsx index 73528b3e..0ba6ab62 100644 --- a/packages/web/src/pages/Application/index.jsx +++ b/packages/web/src/pages/Application/index.jsx @@ -30,7 +30,7 @@ import AppIcon from 'components/AppIcon'; import Container from 'components/Container'; import PageTitle from 'components/PageTitle'; import useApp from 'hooks/useApp'; -import useAppAuthClients from 'hooks/useAppAuthClients'; +import useOAuthClients from 'hooks/useOAuthClients'; import Can from 'components/Can'; import { AppPropType } from 'propTypes/propTypes'; @@ -63,7 +63,7 @@ export default function Application() { const flowsPathMatch = useMatch({ path: URLS.APP_FLOWS_PATTERN, end: false }); const { appKey } = useParams(); const navigate = useNavigate(); - const { data: appAuthClients } = useAppAuthClients(appKey); + const { data: appOAuthClients } = useOAuthClients(appKey); const { data, loading } = useApp(appKey); const app = data?.data || {}; @@ -86,14 +86,14 @@ export default function Application() { appConfig?.data?.disabled === true, }; - const addConnectionWithAuthClient = { - label: formatMessage('app.addConnectionWithAuthClient'), - key: 'addConnectionWithAuthClient', + 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') || - appAuthClients?.data?.length === 0 || + appOAuthClients?.data?.length === 0 || appConfig?.data?.disabled === true, }; @@ -102,18 +102,18 @@ export default function Application() { return [addCustomConnection]; } - // means only auth clients are allowed for connection creation + // means only OAuth clients are allowed for connection creation if (appConfig?.data?.useOnlyPredefinedAuthClients === true) { - return [addConnectionWithAuthClient]; + return [addConnectionWithOAuthClient]; } - // means there is no app auth client. so we don't show the `addConnectionWithAuthClient` - if (appAuthClients?.data?.length === 0) { + // means there is no OAuth client. so we don't show the `addConnectionWithOAuthClient` + if (appOAuthClients?.data?.length === 0) { return [addCustomConnection]; } - return [addCustomConnection, addConnectionWithAuthClient]; - }, [appKey, appConfig, appAuthClients, currentUserAbility, formatMessage]); + return [addCustomConnection, addConnectionWithOAuthClient]; + }, [appKey, appConfig, appOAuthClients, currentUserAbility, formatMessage]); if (loading) return null; diff --git a/packages/web/src/propTypes/propTypes.js b/packages/web/src/propTypes/propTypes.js index c92f51a4..bb594912 100644 --- a/packages/web/src/propTypes/propTypes.js +++ b/packages/web/src/propTypes/propTypes.js @@ -211,7 +211,7 @@ export const ConnectionPropType = PropTypes.shape({ flowCount: PropTypes.number, appData: AppPropType, createdAt: PropTypes.number, - appAuthClientId: PropTypes.string, + oauthClientId: PropTypes.string, }); AppPropType.connection = PropTypes.arrayOf(ConnectionPropType); @@ -463,7 +463,7 @@ export const AppConfigPropType = PropTypes.shape({ disabled: PropTypes.bool, }); -export const AppAuthClientPropType = PropTypes.shape({ +export const OAuthClientPropType = PropTypes.shape({ id: PropTypes.string, name: PropTypes.string, appConfigKey: PropTypes.string, From d675fd6e69cad93e495ded363d06fc3760987409 Mon Sep 17 00:00:00 2001 From: "Jakub P." Date: Thu, 19 Dec 2024 20:19:28 +0100 Subject: [PATCH 17/45] test: adapt tests to OAuth client rename --- ...e.js => application-oauth-clients-page.js} | 4 +- packages/e2e-tests/fixtures/admin/index.js | 11 +-- .../tests/admin/applications.spec.js | 82 +++++++++---------- .../e2e-tests/tests/apps/list-apps.spec.js | 2 +- 4 files changed, 50 insertions(+), 49 deletions(-) rename packages/e2e-tests/fixtures/admin/{application-auth-clients-page.js => application-oauth-clients-page.js} (89%) 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 89% 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 c1b852a1..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,14 +2,14 @@ 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.getByTestId('auth-clients-tab'); + 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' 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/tests/admin/applications.spec.js b/packages/e2e-tests/tests/admin/applications.spec.js index d5bfb4ba..c487ae3f 100644 --- a/packages/e2e-tests/tests/admin/applications.spec.js +++ b/packages/e2e-tests/tests/admin/applications.spec.js @@ -71,7 +71,7 @@ test.describe('Admin Applications', () => { test('should allow only custom connections', async ({ adminApplicationsPage, adminApplicationSettingsPage, - adminApplicationAuthClientsPage, + adminApplicationOAuthClientsPage, flowEditorPage, page, }) => { @@ -95,9 +95,9 @@ test.describe('Admin Applications', () => { adminApplicationSettingsPage.disableConnectionsSwitch ).not.toBeChecked(); - await adminApplicationAuthClientsPage.openAuthClientsTab(); + await adminApplicationOAuthClientsPage.openAuthClientsTab(); await expect( - adminApplicationAuthClientsPage.createFirstAuthClientButton + adminApplicationOAuthClientsPage.createFirstAuthClientButton ).toHaveCount(1); await page.goto('/'); @@ -117,9 +117,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 connection with auth client' }); + .filter({ hasText: 'Add connection with OAuth client' }); const existingConnection = page .getByRole('option') .filter({ hasText: 'Unnamed' }); @@ -127,13 +127,13 @@ test.describe('Admin Applications', () => { await expect(await existingConnection.count()).toBeGreaterThan(0); await expect(newConnectionOption).toBeEnabled(); await expect(newConnectionOption).toHaveCount(1); - await expect(newSharedConnectionOption).toHaveCount(0); + await expect(newOAuthConnectionOption).toHaveCount(0); }); test('should allow only predefined connections and existing custom', async ({ adminApplicationsPage, adminApplicationSettingsPage, - adminApplicationAuthClientsPage, + adminApplicationOAuthClientsPage, flowEditorPage, page, }) => { @@ -153,8 +153,8 @@ test.describe('Admin Applications', () => { await adminApplicationSettingsPage.saveSettings(); await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); - await adminApplicationAuthClientsPage.openAuthClientsTab(); - await adminApplicationAuthClientsPage.openFirstAuthClientCreateForm(); + await adminApplicationOAuthClientsPage.openAuthClientsTab(); + await adminApplicationOAuthClientsPage.openFirstAuthClientCreateForm(); const authClientForm = page.getByTestId('auth-client-form'); await authClientForm.locator(page.getByTestId('switch')).check(); await authClientForm @@ -166,8 +166,8 @@ test.describe('Admin Applications', () => { await authClientForm .locator(page.locator('[name="clientSecret"]')) .fill('spotifyClientSecret'); - await adminApplicationAuthClientsPage.submitAuthClientForm(); - await adminApplicationAuthClientsPage.authClientShouldBeVisible( + await adminApplicationOAuthClientsPage.submitAuthClientForm(); + await adminApplicationOAuthClientsPage.authClientShouldBeVisible( 'spotifyAuthClient' ); @@ -187,23 +187,23 @@ 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 connection with auth client' }); + .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).toHaveCount(0); - await expect(newSharedConnectionOption).toBeEnabled(); - await expect(newSharedConnectionOption).toHaveCount(1); + await expect(newOAuthConnectionOption).toBeEnabled(); + await expect(newOAuthConnectionOption).toHaveCount(1); }); test('should allow all connections', async ({ adminApplicationsPage, adminApplicationSettingsPage, - adminApplicationAuthClientsPage, + adminApplicationOAuthClientsPage, flowEditorPage, page, }) => { @@ -219,8 +219,8 @@ test.describe('Admin Applications', () => { adminApplicationSettingsPage.disableConnectionsSwitch ).not.toBeChecked(); - await adminApplicationAuthClientsPage.openAuthClientsTab(); - await adminApplicationAuthClientsPage.openFirstAuthClientCreateForm(); + await adminApplicationOAuthClientsPage.openAuthClientsTab(); + await adminApplicationOAuthClientsPage.openFirstAuthClientCreateForm(); const authClientForm = page.getByTestId('auth-client-form'); await authClientForm.locator(page.getByTestId('switch')).check(); await authClientForm @@ -232,8 +232,8 @@ 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' ); @@ -253,23 +253,23 @@ 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 connection with auth client' }); + .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).toHaveCount(1); - await expect(newSharedConnectionOption).toBeEnabled(); - await expect(newSharedConnectionOption).toHaveCount(1); + await expect(newOAuthConnectionOption).toBeEnabled(); + await expect(newOAuthConnectionOption).toHaveCount(1); }); test('should not allow new connections but existing custom', async ({ adminApplicationsPage, adminApplicationSettingsPage, - adminApplicationAuthClientsPage, + adminApplicationOAuthClientsPage, flowEditorPage, page, }) => { @@ -282,8 +282,8 @@ test.describe('Admin Applications', () => { await adminApplicationSettingsPage.saveSettings(); await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); - await adminApplicationAuthClientsPage.openAuthClientsTab(); - await adminApplicationAuthClientsPage.openFirstAuthClientCreateForm(); + await adminApplicationOAuthClientsPage.openAuthClientsTab(); + await adminApplicationOAuthClientsPage.openFirstAuthClientCreateForm(); const authClientForm = page.getByTestId('auth-client-form'); await authClientForm.locator(page.getByTestId('switch')).check(); @@ -296,8 +296,8 @@ test.describe('Admin Applications', () => { await authClientForm .locator(page.locator('[name="clientSecret"]')) .fill('clickupClientSecret'); - await adminApplicationAuthClientsPage.submitAuthClientForm(); - await adminApplicationAuthClientsPage.authClientShouldBeVisible( + await adminApplicationOAuthClientsPage.submitAuthClientForm(); + await adminApplicationOAuthClientsPage.authClientShouldBeVisible( 'clickupAuthClient' ); @@ -317,22 +317,22 @@ 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 connection with auth client' }); + .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).toHaveCount(0); - await expect(newSharedConnectionOption).toHaveCount(0); + await expect(newOAuthConnectionOption).toHaveCount(0); }); - test('should not allow new connections but existing custom even if predefined auth clients are enabled', async ({ + test('should not allow new connections but existing custom even if predefined OAuth clients are enabled', async ({ adminApplicationsPage, adminApplicationSettingsPage, - adminApplicationAuthClientsPage, + adminApplicationOAuthClientsPage, flowEditorPage, page, }) => { @@ -348,8 +348,8 @@ test.describe('Admin Applications', () => { await adminApplicationSettingsPage.saveSettings(); await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); - await adminApplicationAuthClientsPage.openAuthClientsTab(); - await adminApplicationAuthClientsPage.openFirstAuthClientCreateForm(); + await adminApplicationOAuthClientsPage.openAuthClientsTab(); + await adminApplicationOAuthClientsPage.openFirstAuthClientCreateForm(); const authClientForm = page.getByTestId('auth-client-form'); await authClientForm.locator(page.getByTestId('switch')).check(); @@ -362,8 +362,8 @@ test.describe('Admin Applications', () => { await authClientForm .locator(page.locator('[name="clientSecret"]')) .fill('mailchimpClientSecret'); - await adminApplicationAuthClientsPage.submitAuthClientForm(); - await adminApplicationAuthClientsPage.authClientShouldBeVisible( + await adminApplicationOAuthClientsPage.submitAuthClientForm(); + await adminApplicationOAuthClientsPage.authClientShouldBeVisible( 'mailchimpAuthClient' ); @@ -387,9 +387,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' }); @@ -397,6 +397,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(); From ebc21e90acfba87293261dd7c515616587e2a6bf Mon Sep 17 00:00:00 2001 From: "kasia.oczkowska" Date: Thu, 19 Dec 2024 13:23:33 +0000 Subject: [PATCH 18/45] refactor: remove redundant errors setting in SignUpForm --- .../src/components/InstallationForm/index.jsx | 4 +-- .../src/components/SignUpForm/index.ee.jsx | 27 ++----------------- packages/web/src/helpers/errors.js | 18 ------------- packages/web/src/locales/en.json | 1 - 4 files changed, 3 insertions(+), 47 deletions(-) delete mode 100644 packages/web/src/helpers/errors.js diff --git a/packages/web/src/components/InstallationForm/index.jsx b/packages/web/src/components/InstallationForm/index.jsx index f1e30051..bba4d314 100644 --- a/packages/web/src/components/InstallationForm/index.jsx +++ b/packages/web/src/components/InstallationForm/index.jsx @@ -9,7 +9,6 @@ import { yupResolver } from '@hookform/resolvers/yup'; import { useQueryClient } from '@tanstack/react-query'; import Link from '@mui/material/Link'; -import { getGeneralErrorMessage } from 'helpers/errors'; import useFormatMessage from 'hooks/useFormatMessage'; import useInstallation from 'hooks/useInstallation'; import * as URLS from 'config/urls'; @@ -77,8 +76,7 @@ function InstallationForm() { }); } catch (error) { const errors = error?.response?.data?.errors; - - throw errors; + throw errors || error; } }; diff --git a/packages/web/src/components/SignUpForm/index.ee.jsx b/packages/web/src/components/SignUpForm/index.ee.jsx index 3cbc1033..a7daa326 100644 --- a/packages/web/src/components/SignUpForm/index.ee.jsx +++ b/packages/web/src/components/SignUpForm/index.ee.jsx @@ -7,7 +7,6 @@ import Alert from '@mui/material/Alert'; import * as yup from 'yup'; import { yupResolver } from '@hookform/resolvers/yup'; -import { getGeneralErrorMessage } from 'helpers/errors'; import useAuthentication from 'hooks/useAuthentication'; import * as URLS from 'config/urls'; import Form from 'components/Form'; @@ -70,7 +69,7 @@ function SignUpForm() { } }, [authentication.isAuthenticated]); - const handleSubmit = async (values, e, setError) => { + const handleSubmit = async (values) => { try { const { fullName, email, password } = values; await registerUser({ @@ -86,29 +85,7 @@ function SignUpForm() { authentication.updateToken(token); } catch (error) { const errors = error?.response?.data?.errors; - if (errors) { - const fieldNames = Object.keys(defaultValues); - Object.entries(errors).forEach(([fieldName, fieldErrors]) => { - if (fieldNames.includes(fieldName) && Array.isArray(fieldErrors)) { - setError(fieldName, { - type: 'fieldRequestError', - message: fieldErrors.join(', '), - }); - } - }); - } - - const generalError = getGeneralErrorMessage({ - error, - fallbackMessage: formatMessage('signupForm.error'), - }); - - if (generalError) { - setError('root.general', { - type: 'requestError', - message: generalError, - }); - } + throw errors || error; } }; diff --git a/packages/web/src/helpers/errors.js b/packages/web/src/helpers/errors.js deleted file mode 100644 index dc73867d..00000000 --- a/packages/web/src/helpers/errors.js +++ /dev/null @@ -1,18 +0,0 @@ -// Helpers to extract errors received from the API - -export const getGeneralErrorMessage = ({ error, fallbackMessage }) => { - if (!error) { - return; - } - - const errors = error?.response?.data?.errors; - const generalError = errors?.general; - - if (generalError && Array.isArray(generalError)) { - return generalError.join(' '); - } - - if (!errors) { - return error?.message || fallbackMessage; - } -}; diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index 19c8aec4..373521e0 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -152,7 +152,6 @@ "signupForm.passwordsMustMatch": "Passwords must match.", "signupForm.passwordMinLength": "Password must be at least 6 characters long.", "signupForm.mandatoryInput": "{inputName} is required.", - "signupForm.error": "Something went wrong. Please try again.", "loginForm.title": "Login", "loginForm.emailFieldLabel": "Email", "loginForm.passwordFieldLabel": "Password", From bc6314ac7e8c78c3527dd55e08f8d48a5ee064e4 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Thu, 19 Dec 2024 13:26:36 +0100 Subject: [PATCH 19/45] refactor: Extract background jobs to separate files --- packages/backend/src/jobs/delete-user.ee.js | 37 +++++++++++++ packages/backend/src/jobs/execute-action.js | 46 ++++++++++++++++ packages/backend/src/jobs/execute-flow.js | 54 +++++++++++++++++++ packages/backend/src/jobs/execute-trigger.js | 32 +++++++++++ .../jobs/remove-cancelled-subscriptions.ee.js | 15 ++++++ packages/backend/src/jobs/send-email.js | 31 +++++++++++ 6 files changed, 215 insertions(+) create mode 100644 packages/backend/src/jobs/delete-user.ee.js create mode 100644 packages/backend/src/jobs/execute-action.js create mode 100644 packages/backend/src/jobs/execute-flow.js create mode 100644 packages/backend/src/jobs/execute-trigger.js create mode 100644 packages/backend/src/jobs/remove-cancelled-subscriptions.ee.js create mode 100644 packages/backend/src/jobs/send-email.js 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'); +}; From c7072a29bb0d34d7bda4b0a44a5b4231c1bdacad Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Thu, 19 Dec 2024 13:26:49 +0100 Subject: [PATCH 20/45] refactor: Worker structure to work with jobs --- packages/backend/src/workers/action.js | 76 +-------------- .../backend/src/workers/delete-user.ee.js | 69 +------------ packages/backend/src/workers/email.js | 62 +----------- packages/backend/src/workers/flow.js | 97 +------------------ .../remove-cancelled-subscriptions.ee.js | 43 +------- packages/backend/src/workers/trigger.js | 62 +----------- packages/backend/src/workers/worker.js | 28 ++++++ 7 files changed, 47 insertions(+), 390 deletions(-) create mode 100644 packages/backend/src/workers/worker.js diff --git a/packages/backend/src/workers/action.js b/packages/backend/src/workers/action.js index 3159a7d6..b0728059 100644 --- a/packages/backend/src/workers/action.js +++ b/packages/backend/src/workers/action.js @@ -1,76 +1,6 @@ -import { Worker } from 'bullmq'; +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 DEFAULT_DELAY_DURATION = 0; - -const actionWorker = 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 } -); - -actionWorker.on('completed', (job) => { - logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`); -}); - -actionWorker.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, - }, - }); -}); +const actionWorker = generateWorker('action', executeActionJob); export default actionWorker; diff --git a/packages/backend/src/workers/delete-user.ee.js b/packages/backend/src/workers/delete-user.ee.js index 9081df20..c47093b9 100644 --- a/packages/backend/src/workers/delete-user.ee.js +++ b/packages/backend/src/workers/delete-user.ee.js @@ -1,69 +1,6 @@ -import { Worker } from 'bullmq'; +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 = 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 } -); - -deleteUserWorker.on('completed', (job) => { - logger.info( - `JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has been deleted!` - ); -}); - -deleteUserWorker.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, - }, - }); -}); +const deleteUserWorker = generateWorker('delete-user', deleteUserJob); export default deleteUserWorker; diff --git a/packages/backend/src/workers/email.js b/packages/backend/src/workers/email.js index 92bf0367..4cd2c1bb 100644 --- a/packages/backend/src/workers/email.js +++ b/packages/backend/src/workers/email.js @@ -1,62 +1,6 @@ -import { Worker } from 'bullmq'; +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 isCloudSandbox = () => { - return appConfig.isCloud && !appConfig.isProd; -}; - -const isAutomatischEmail = (email) => { - return email.endsWith('@automatisch.io'); -}; - -const emailWorker = 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 } -); - -emailWorker.on('completed', (job) => { - logger.info( - `JOB ID: ${job.id} - ${job.data.subject} email sent to ${job.data.email}!` - ); -}); - -emailWorker.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, - }, - }); -}); +const emailWorker = generateWorker('email', sendEmailJob); export default emailWorker; diff --git a/packages/backend/src/workers/flow.js b/packages/backend/src/workers/flow.js index 8c08d5e1..7b04bef2 100644 --- a/packages/backend/src/workers/flow.js +++ b/packages/backend/src/workers/flow.js @@ -1,97 +1,6 @@ -import { Worker } from 'bullmq'; +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 = 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 } -); - -flowWorker.on('completed', (job) => { - logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`); -}); - -flowWorker.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, - }, - }); -}); +const flowWorker = generateWorker('flow', executeFlowJob); export default flowWorker; diff --git a/packages/backend/src/workers/remove-cancelled-subscriptions.ee.js b/packages/backend/src/workers/remove-cancelled-subscriptions.ee.js index 6ee0ae17..83df0865 100644 --- a/packages/backend/src/workers/remove-cancelled-subscriptions.ee.js +++ b/packages/backend/src/workers/remove-cancelled-subscriptions.ee.js @@ -1,44 +1,9 @@ -import { Worker } from 'bullmq'; -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'; -const removeCancelledSubscriptionsWorker = 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 ); -removeCancelledSubscriptionsWorker.on('completed', (job) => { - logger.info( - `JOB ID: ${job.id} - The cancelled subscriptions have been removed!` - ); -}); - -removeCancelledSubscriptionsWorker.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, - }, - }); -}); - export default removeCancelledSubscriptionsWorker; diff --git a/packages/backend/src/workers/trigger.js b/packages/backend/src/workers/trigger.js index 64056dd9..e25915fc 100644 --- a/packages/backend/src/workers/trigger.js +++ b/packages/backend/src/workers/trigger.js @@ -1,62 +1,6 @@ -import { Worker } from 'bullmq'; +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 = 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 } -); - -triggerWorker.on('completed', (job) => { - logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`); -}); - -triggerWorker.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, - }, - }); -}); +const triggerWorker = generateWorker('flow', executeTriggerJob); 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; +}; From af64c086466f6d0e8e028a84d0ecc8b7ac8be0a7 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Fri, 20 Dec 2024 14:26:46 +0000 Subject: [PATCH 21/45] chore(test): exclude uncovered controllers --- packages/backend/vitest.config.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/backend/vitest.config.js b/packages/backend/vitest.config.js index 645c7fbf..4a0fbf06 100644 --- a/packages/backend/vitest.config.js +++ b/packages/backend/vitest.config.js @@ -14,6 +14,10 @@ export default defineConfig({ reporter: ['text', 'lcov'], all: true, include: ['**/src/models/**', '**/src/controllers/**'], + exclude: [ + '**/src/controllers/webhooks/**', + '**/src/controllers/paddle/**', + ], thresholds: { autoUpdate: true, statements: 95.16, From 6454bea99e3d48c7c5515e12016aaf50459967f6 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Fri, 20 Dec 2024 14:29:16 +0000 Subject: [PATCH 22/45] test: update coverage thresholds --- packages/backend/vitest.config.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/backend/vitest.config.js b/packages/backend/vitest.config.js index 4a0fbf06..e75ea51d 100644 --- a/packages/backend/vitest.config.js +++ b/packages/backend/vitest.config.js @@ -20,10 +20,10 @@ export default defineConfig({ ], thresholds: { autoUpdate: true, - statements: 95.16, - branches: 94.66, - functions: 97.65, - lines: 95.16, + statements: 99.44, + branches: 97.78, + functions: 99.1, + lines: 99.44, }, }, }, From bc87e18d3f238a44c3285d45df2445bcd55a90a1 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Mon, 6 Jan 2025 16:42:00 +0300 Subject: [PATCH 23/45] feat: Add name column to Step model --- .../controllers/api/v1/steps/update-step.js | 3 ++- .../api/v1/steps/update-step.test.js | 2 ++ ...20250106114602_add_name_column_to_steps.js | 26 +++++++++++++++++++ .../models/__snapshots__/step.test.js.snap | 8 ++++++ packages/backend/src/models/step.js | 10 ++++++- packages/backend/src/serializers/step.js | 1 + packages/backend/src/serializers/step.test.js | 1 + .../api/v1/executions/get-execution-steps.js | 1 + .../rest/api/v1/executions/get-execution.js | 1 + .../rest/api/v1/executions/get-executions.js | 1 + .../mocks/rest/api/v1/flows/duplicate-flow.js | 1 + .../test/mocks/rest/api/v1/flows/get-flow.js | 1 + .../test/mocks/rest/api/v1/flows/get-flows.js | 1 + .../rest/api/v1/flows/update-flow-status.js | 1 + .../rest/api/v1/steps/get-previous-steps.js | 1 + .../mocks/rest/api/v1/steps/update-step.js | 1 + 16 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 packages/backend/src/db/migrations/20250106114602_add_name_column_to_steps.js diff --git a/packages/backend/src/controllers/api/v1/steps/update-step.js b/packages/backend/src/controllers/api/v1/steps/update-step.js index c707726d..70f0b98f 100644 --- a/packages/backend/src/controllers/api/v1/steps/update-step.js +++ b/packages/backend/src/controllers/api/v1/steps/update-step.js @@ -11,12 +11,13 @@ export default async (request, response) => { }; const stepParams = (request) => { - const { connectionId, appKey, key, parameters } = request.body; + const { connectionId, appKey, key, name, parameters } = request.body; return { connectionId, appKey, key, + name, parameters, }; }; diff --git a/packages/backend/src/controllers/api/v1/steps/update-step.test.js b/packages/backend/src/controllers/api/v1/steps/update-step.test.js index 1a5ae1b9..c219dee0 100644 --- a/packages/backend/src/controllers/api/v1/steps/update-step.test.js +++ b/packages/backend/src/controllers/api/v1/steps/update-step.test.js @@ -35,6 +35,7 @@ describe('PATCH /api/v1/steps/:stepId', () => { connectionId: currentUserConnection.id, appKey: 'deepl', key: 'translateText', + name: 'Translate text', }); await createPermission({ @@ -58,6 +59,7 @@ describe('PATCH /api/v1/steps/:stepId', () => { parameters: { text: 'Hello world!', targetLanguage: 'de', + name: 'Translate text - Updated step name', }, }) .expect(200); diff --git a/packages/backend/src/db/migrations/20250106114602_add_name_column_to_steps.js b/packages/backend/src/db/migrations/20250106114602_add_name_column_to_steps.js new file mode 100644 index 00000000..bde4d6c5 --- /dev/null +++ b/packages/backend/src/db/migrations/20250106114602_add_name_column_to_steps.js @@ -0,0 +1,26 @@ +import toLower from 'lodash/toLower.js'; +import startCase from 'lodash/startCase.js'; +import upperFirst from 'lodash/upperFirst.js'; + +export async function up(knex) { + await knex.schema.table('steps', function (table) { + table.string('name'); + }); + + const rows = await knex('steps').select('id', 'key'); + + const updates = rows.map((row) => { + if (!row.key) return; + + const humanizedKey = upperFirst(toLower(startCase(row.key))); + return knex('steps').where({ id: row.id }).update({ name: humanizedKey }); + }); + + return await Promise.all(updates); +} + +export async function down(knex) { + return knex.schema.table('steps', function (table) { + table.dropColumn('name'); + }); +} diff --git a/packages/backend/src/models/__snapshots__/step.test.js.snap b/packages/backend/src/models/__snapshots__/step.test.js.snap index aa78645e..d9a45102 100644 --- a/packages/backend/src/models/__snapshots__/step.test.js.snap +++ b/packages/backend/src/models/__snapshots__/step.test.js.snap @@ -38,6 +38,14 @@ exports[`Step model > jsonSchema should have correct validations 1`] = ` "null", ], }, + "name": { + "maxLength": 255, + "minLength": 1, + "type": [ + "string", + "null", + ], + }, "parameters": { "type": "object", }, diff --git a/packages/backend/src/models/step.js b/packages/backend/src/models/step.js index 9f5f3f70..7c31e0a6 100644 --- a/packages/backend/src/models/step.js +++ b/packages/backend/src/models/step.js @@ -22,6 +22,7 @@ class Step extends Base { id: { type: 'string', format: 'uuid' }, flowId: { type: 'string', format: 'uuid' }, key: { type: ['string', 'null'] }, + name: { type: ['string', 'null'], minLength: 1, maxLength: 255 }, appKey: { type: ['string', 'null'], minLength: 1, maxLength: 255 }, type: { type: 'string', enum: ['action', 'trigger'] }, connectionId: { type: ['string', 'null'], format: 'uuid' }, @@ -314,7 +315,13 @@ class Step extends Base { } async updateFor(user, newStepData) { - const { appKey = this.appKey, connectionId, key, parameters } = newStepData; + const { + appKey = this.appKey, + name, + connectionId, + key, + parameters, + } = newStepData; if (connectionId && appKey) { await user.authorizedConnections @@ -335,6 +342,7 @@ class Step extends Base { const updatedStep = await this.$query().patchAndFetch({ key, + name, appKey, connectionId: connectionId, parameters: parameters, diff --git a/packages/backend/src/serializers/step.js b/packages/backend/src/serializers/step.js index f5ae1c26..5c1e0d38 100644 --- a/packages/backend/src/serializers/step.js +++ b/packages/backend/src/serializers/step.js @@ -5,6 +5,7 @@ const stepSerializer = (step) => { id: step.id, type: step.type, key: step.key, + name: step.name, appKey: step.appKey, iconUrl: step.iconUrl, webhookUrl: step.webhookUrl, diff --git a/packages/backend/src/serializers/step.test.js b/packages/backend/src/serializers/step.test.js index dfeffbeb..2b26cfcf 100644 --- a/packages/backend/src/serializers/step.test.js +++ b/packages/backend/src/serializers/step.test.js @@ -16,6 +16,7 @@ describe('stepSerializer', () => { id: step.id, type: step.type, key: step.key, + name: step.name, appKey: step.appKey, iconUrl: step.iconUrl, webhookUrl: step.webhookUrl, diff --git a/packages/backend/test/mocks/rest/api/v1/executions/get-execution-steps.js b/packages/backend/test/mocks/rest/api/v1/executions/get-execution-steps.js index f7b50194..f694f70a 100644 --- a/packages/backend/test/mocks/rest/api/v1/executions/get-execution-steps.js +++ b/packages/backend/test/mocks/rest/api/v1/executions/get-execution-steps.js @@ -14,6 +14,7 @@ const getExecutionStepsMock = async (executionSteps, steps) => { id: step.id, type: step.type, key: step.key, + name: step.name, appKey: step.appKey, iconUrl: step.iconUrl, webhookUrl: step.webhookUrl, diff --git a/packages/backend/test/mocks/rest/api/v1/executions/get-execution.js b/packages/backend/test/mocks/rest/api/v1/executions/get-execution.js index 3957e9d8..61feddd8 100644 --- a/packages/backend/test/mocks/rest/api/v1/executions/get-execution.js +++ b/packages/backend/test/mocks/rest/api/v1/executions/get-execution.js @@ -15,6 +15,7 @@ const getExecutionMock = async (execution, flow, steps) => { id: step.id, type: step.type, key: step.key, + name: step.name, appKey: step.appKey, iconUrl: step.iconUrl, webhookUrl: step.webhookUrl, diff --git a/packages/backend/test/mocks/rest/api/v1/executions/get-executions.js b/packages/backend/test/mocks/rest/api/v1/executions/get-executions.js index 21d36376..b194bee2 100644 --- a/packages/backend/test/mocks/rest/api/v1/executions/get-executions.js +++ b/packages/backend/test/mocks/rest/api/v1/executions/get-executions.js @@ -16,6 +16,7 @@ const getExecutionsMock = async (executions, flow, steps) => { id: step.id, type: step.type, key: step.key, + name: step.name, appKey: step.appKey, iconUrl: step.iconUrl, webhookUrl: step.webhookUrl, diff --git a/packages/backend/test/mocks/rest/api/v1/flows/duplicate-flow.js b/packages/backend/test/mocks/rest/api/v1/flows/duplicate-flow.js index 67684191..36426774 100644 --- a/packages/backend/test/mocks/rest/api/v1/flows/duplicate-flow.js +++ b/packages/backend/test/mocks/rest/api/v1/flows/duplicate-flow.js @@ -14,6 +14,7 @@ const duplicateFlowMock = async (flow, steps = []) => { iconUrl: step.iconUrl, id: step.id, key: step.key, + name: step.name, parameters: step.parameters, position: step.position, status: step.status, 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 index db1e4a47..49efe83c 100644 --- a/packages/backend/test/mocks/rest/api/v1/flows/get-flow.js +++ b/packages/backend/test/mocks/rest/api/v1/flows/get-flow.js @@ -14,6 +14,7 @@ const getFlowMock = async (flow, steps = []) => { iconUrl: step.iconUrl, id: step.id, key: step.key, + name: step.name, parameters: step.parameters, position: step.position, status: step.status, diff --git a/packages/backend/test/mocks/rest/api/v1/flows/get-flows.js b/packages/backend/test/mocks/rest/api/v1/flows/get-flows.js index 0509aec3..6012a6f6 100644 --- a/packages/backend/test/mocks/rest/api/v1/flows/get-flows.js +++ b/packages/backend/test/mocks/rest/api/v1/flows/get-flows.js @@ -14,6 +14,7 @@ const getFlowsMock = async (flows, steps) => { iconUrl: step.iconUrl, id: step.id, key: step.key, + name: step.name, parameters: step.parameters, position: step.position, status: step.status, diff --git a/packages/backend/test/mocks/rest/api/v1/flows/update-flow-status.js b/packages/backend/test/mocks/rest/api/v1/flows/update-flow-status.js index f303f295..f7c32b3b 100644 --- a/packages/backend/test/mocks/rest/api/v1/flows/update-flow-status.js +++ b/packages/backend/test/mocks/rest/api/v1/flows/update-flow-status.js @@ -14,6 +14,7 @@ const updateFlowStatusMock = async (flow, steps = []) => { iconUrl: step.iconUrl, id: step.id, key: step.key, + name: step.name, parameters: step.parameters, position: step.position, status: step.status, diff --git a/packages/backend/test/mocks/rest/api/v1/steps/get-previous-steps.js b/packages/backend/test/mocks/rest/api/v1/steps/get-previous-steps.js index 7b5515ed..4ae477d6 100644 --- a/packages/backend/test/mocks/rest/api/v1/steps/get-previous-steps.js +++ b/packages/backend/test/mocks/rest/api/v1/steps/get-previous-steps.js @@ -8,6 +8,7 @@ const getPreviousStepsMock = async (steps, executionSteps) => { id: step.id, type: step.type, key: step.key, + name: step.name, appKey: step.appKey, iconUrl: step.iconUrl, webhookUrl: step.webhookUrl, diff --git a/packages/backend/test/mocks/rest/api/v1/steps/update-step.js b/packages/backend/test/mocks/rest/api/v1/steps/update-step.js index 87514ef9..a7ad0dea 100644 --- a/packages/backend/test/mocks/rest/api/v1/steps/update-step.js +++ b/packages/backend/test/mocks/rest/api/v1/steps/update-step.js @@ -3,6 +3,7 @@ const updateStepMock = (step) => { id: step.id, type: step.type || 'action', key: step.key || null, + name: step.name || null, appKey: step.appKey || null, iconUrl: step.iconUrl || null, webhookUrl: step.webhookUrl || null, From e29e71bdf1e2d11d1e0af94b892df4ae3fbc3897 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 7 Jan 2025 13:46:07 +0000 Subject: [PATCH 24/45] feat: add support to rename flow step --- .../ChooseAppAndEventSubstep/index.jsx | 14 ++--- .../components/EditableTypography/index.jsx | 57 +++++++++++++++++-- .../components/EditableTypography/style.js | 10 +++- packages/web/src/components/Editor/index.jsx | 4 ++ .../src/components/EditorNew/EditorNew.jsx | 4 ++ .../web/src/components/FlowStep/index.jsx | 49 ++++++++++++---- .../src/components/FlowSubstepTitle/style.jsx | 2 + packages/web/src/hooks/useUpdateStep.js | 7 ++- packages/web/src/styles/theme.js | 23 ++++++++ 9 files changed, 140 insertions(+), 30 deletions(-) diff --git a/packages/web/src/components/ChooseAppAndEventSubstep/index.jsx b/packages/web/src/components/ChooseAppAndEventSubstep/index.jsx index 17712178..40936244 100644 --- a/packages/web/src/components/ChooseAppAndEventSubstep/index.jsx +++ b/packages/web/src/components/ChooseAppAndEventSubstep/index.jsx @@ -91,15 +91,15 @@ function ChooseAppAndEventSubstep(props) { const onEventChange = React.useCallback( (event, selectedOption) => { if (typeof selectedOption === 'object') { - // TODO: try to simplify type casting below. - const typedSelectedOption = selectedOption; - const option = typedSelectedOption; - const eventKey = option?.value; + const eventKey = selectedOption?.value; + const eventLabel = selectedOption?.label; + if (step.key !== eventKey) { onChange({ step: { ...step, key: eventKey, + keyLabel: eventLabel, }, }); } @@ -111,10 +111,8 @@ function ChooseAppAndEventSubstep(props) { const onAppChange = React.useCallback( (event, selectedOption) => { if (typeof selectedOption === 'object') { - // TODO: try to simplify type casting below. - const typedSelectedOption = selectedOption; - const option = typedSelectedOption; - const appKey = option?.value; + const appKey = selectedOption?.value; + if (step.appKey !== appKey) { onChange({ step: { diff --git a/packages/web/src/components/EditableTypography/index.jsx b/packages/web/src/components/EditableTypography/index.jsx index bb234326..255ebbd3 100644 --- a/packages/web/src/components/EditableTypography/index.jsx +++ b/packages/web/src/components/EditableTypography/index.jsx @@ -7,37 +7,68 @@ import { Box, TextField } from './style'; const noop = () => null; function EditableTypography(props) { - const { children, onConfirm = noop, sx, ...typographyProps } = props; + const { + children, + onConfirm = noop, + iconPosition = 'start', + iconSize = 'large', + sx, + disabledEditing = false, + prefixValue = '', + ...typographyProps + } = props; + const [editing, setEditing] = React.useState(false); + const handleClick = React.useCallback(() => { + if (disabledEditing) return; + setEditing((editing) => !editing); - }, []); + }, [disabledEditing]); + const handleTextFieldClick = React.useCallback((event) => { event.stopPropagation(); }, []); + const handleTextFieldKeyDown = React.useCallback( async (event) => { const target = event.target; + if (event.key === 'Enter') { if (target.value !== children) { await onConfirm(target.value); } + + setEditing(false); + } + + if (event.key === 'Escape') { setEditing(false); } }, [children], ); + const handleTextFieldBlur = React.useCallback( async (event) => { const value = event.target.value; + if (value !== children) { await onConfirm(value); } + setEditing(false); }, [onConfirm, children], ); - let component = {children}; + + let component = ( + + {prefixValue} + {children} + + ); + if (editing) { component = ( ); } + return ( - - + + {iconPosition === 'start' && editing === false && ( + + )} {component} + + {iconPosition === 'end' && editing === false && ( + + )} ); } EditableTypography.propTypes = { children: PropTypes.string.isRequired, + disabledEditing: PropTypes.bool, + iconPosition: PropTypes.oneOf(['start', 'end']), + iconSize: PropTypes.oneOf(['small', 'large']), onConfirm: PropTypes.func, + prefixValue: PropTypes.string, sx: PropTypes.object, }; diff --git a/packages/web/src/components/EditableTypography/style.js b/packages/web/src/components/EditableTypography/style.js index 8e11fd83..e99cb6ea 100644 --- a/packages/web/src/components/EditableTypography/style.js +++ b/packages/web/src/components/EditableTypography/style.js @@ -2,17 +2,23 @@ import { styled } from '@mui/material/styles'; import MuiBox from '@mui/material/Box'; import MuiTextField from '@mui/material/TextField'; import { inputClasses } from '@mui/material/Input'; -const boxShouldForwardProp = (prop) => !['editing'].includes(prop); + +const boxShouldForwardProp = (prop) => + !['editing', 'disabledEditing'].includes(prop); + export const Box = styled(MuiBox, { shouldForwardProp: boxShouldForwardProp, })` display: flex; flex: 1; - width: 300px; + min-width: 300px; + max-width: 90%; height: 33px; align-items: center; + ${({ disabledEditing }) => !disabledEditing && 'cursor: pointer;'} ${({ editing }) => editing && 'border-bottom: 1px dashed #000;'} `; + export const TextField = styled(MuiTextField)({ width: '100%', [`.${inputClasses.root}:before, .${inputClasses.root}:after, .${inputClasses.root}:hover`]: diff --git a/packages/web/src/components/Editor/index.jsx b/packages/web/src/components/Editor/index.jsx index 96ca3258..5de95d4e 100644 --- a/packages/web/src/components/Editor/index.jsx +++ b/packages/web/src/components/Editor/index.jsx @@ -27,6 +27,10 @@ function Editor(props) { connectionId: step.connection?.id, }; + if (step.name) { + payload.name = step.name; + } + if (step.appKey) { payload.appKey = step.appKey; } diff --git a/packages/web/src/components/EditorNew/EditorNew.jsx b/packages/web/src/components/EditorNew/EditorNew.jsx index fc7a5589..7d52e3cc 100644 --- a/packages/web/src/components/EditorNew/EditorNew.jsx +++ b/packages/web/src/components/EditorNew/EditorNew.jsx @@ -90,6 +90,10 @@ const EditorNew = ({ flow }) => { connectionId: step.connection?.id, }; + if (step.name) { + payload.name = step.name || step.keyLabel; + } + if (step.appKey) { payload.appKey = step.appKey; } diff --git a/packages/web/src/components/FlowStep/index.jsx b/packages/web/src/components/FlowStep/index.jsx index e9a4596c..df7e8ec6 100644 --- a/packages/web/src/components/FlowStep/index.jsx +++ b/packages/web/src/components/FlowStep/index.jsx @@ -3,6 +3,7 @@ import * as React from 'react'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; import Box from '@mui/material/Box'; +import Chip from '@mui/material/Chip'; import Button from '@mui/material/Button'; import Collapse from '@mui/material/Collapse'; import List from '@mui/material/List'; @@ -18,6 +19,7 @@ import { isEqual } from 'lodash'; import { EditorContext } from 'contexts/Editor'; import { StepExecutionsProvider } from 'contexts/StepExecutions'; import TestSubstep from 'components/TestSubstep'; +import EditableTypography from 'components/EditableTypography'; import FlowSubstep from 'components/FlowSubstep'; import ChooseAppAndEventSubstep from 'components/ChooseAppAndEventSubstep'; import ChooseConnectionSubstep from 'components/ChooseConnectionSubstep'; @@ -106,10 +108,9 @@ function generateValidationSchema(substeps) { } function FlowStep(props) { - const { collapsed, onChange, onContinue, flowId } = props; + const { collapsed, onChange, onContinue, flowId, step } = props; const editorContext = React.useContext(EditorContext); const contextButtonRef = React.useRef(null); - const step = props.step; const [anchorEl, setAnchorEl] = React.useState(null); const isTrigger = step.type === 'trigger'; const isAction = step.type === 'action'; @@ -117,6 +118,10 @@ function FlowStep(props) { const [currentSubstep, setCurrentSubstep] = React.useState(0); const useAppsOptions = {}; + const stepTypeName = isTrigger + ? formatMessage('flowStep.triggerType') + : formatMessage('flowStep.actionType'); + if (isTrigger) { useAppsOptions.onlyWithTriggers = true; } @@ -183,6 +188,13 @@ function FlowStep(props) { } }; + const handleStepNameChange = async (name) => { + await onChange({ + ...step, + name, + }); + }; + const stepValidationSchema = React.useMemo( () => generateValidationSchema(substeps), [substeps], @@ -226,7 +238,7 @@ function FlowStep(props) { data-test="flow-step" >
- + -
- - {isTrigger - ? formatMessage('flowStep.triggerType') - : formatMessage('flowStep.actionType')} + + + + + {app?.name} - - {step.position}. {app?.name} - -
+ + {step.name} + +
{/* as there are no other actions besides "delete step", we hide the context menu. */} diff --git a/packages/web/src/components/FlowSubstepTitle/style.jsx b/packages/web/src/components/FlowSubstepTitle/style.jsx index c8c0c090..b885f6be 100644 --- a/packages/web/src/components/FlowSubstepTitle/style.jsx +++ b/packages/web/src/components/FlowSubstepTitle/style.jsx @@ -1,9 +1,11 @@ import { styled } from '@mui/material/styles'; import MuiListItemButton from '@mui/material/ListItemButton'; import MuiTypography from '@mui/material/Typography'; + export const ListItemButton = styled(MuiListItemButton)` justify-content: space-between; `; + export const Typography = styled(MuiTypography)` display: flex; align-items: center; diff --git a/packages/web/src/hooks/useUpdateStep.js b/packages/web/src/hooks/useUpdateStep.js index 65e92170..03548b56 100644 --- a/packages/web/src/hooks/useUpdateStep.js +++ b/packages/web/src/hooks/useUpdateStep.js @@ -6,19 +6,20 @@ export default function useUpdateStep() { const queryClient = useQueryClient(); const query = useMutation({ - mutationFn: async ({ id, appKey, key, connectionId, parameters }) => { + mutationFn: async ({ id, appKey, key, connectionId, name, parameters }) => { const { data } = await api.patch(`/v1/steps/${id}`, { appKey, key, connectionId, + name, parameters, }); return data; }, - onSuccess: () => { - queryClient.invalidateQueries({ + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['flows'], }); }, diff --git a/packages/web/src/styles/theme.js b/packages/web/src/styles/theme.js index 7c2257bc..1e05d062 100644 --- a/packages/web/src/styles/theme.js +++ b/packages/web/src/styles/theme.js @@ -158,6 +158,10 @@ export const defaultTheme = createTheme({ fontSize: referenceTheme.typography.pxToRem(16), }, }, + stepApp: { + fontSize: referenceTheme.typography.pxToRem(12), + color: '#5C5C5C', + }, }, components: { MuiAppBar: { @@ -211,6 +215,23 @@ export const defaultTheme = createTheme({ }), }, }, + MuiChip: { + variants: [ + { + props: { variant: 'stepType' }, + style: ({ theme }) => ({ + color: '#001F52', + fontSize: theme.typography.pxToRem(12), + border: '1px solid', + borderColor: alpha(theme.palette.primary.main, 0.3), + bgcolor: alpha( + theme.palette.primary.main, + theme.palette.action.selectedOpacity, + ), + }), + }, + ], + }, MuiContainer: { defaultProps: { maxWidth: 'xl', @@ -294,6 +315,7 @@ export const defaultTheme = createTheme({ }, }, }); + export const mationTheme = createTheme( deepmerge(defaultTheme, { palette: { @@ -315,4 +337,5 @@ export const mationTheme = createTheme( }, }), ); + export default defaultTheme; From 0b4af4b1b9e8ecc6bf5bd641248e2976bf723f31 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Wed, 8 Jan 2025 15:10:26 +0000 Subject: [PATCH 25/45] feat(EditableTypography): support disable state --- .../components/EditableTypography/index.jsx | 26 ++++++++----------- .../components/EditableTypography/style.js | 5 ++-- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/packages/web/src/components/EditableTypography/index.jsx b/packages/web/src/components/EditableTypography/index.jsx index 255ebbd3..f4c4b077 100644 --- a/packages/web/src/components/EditableTypography/index.jsx +++ b/packages/web/src/components/EditableTypography/index.jsx @@ -13,7 +13,7 @@ function EditableTypography(props) { iconPosition = 'start', iconSize = 'large', sx, - disabledEditing = false, + disabled = false, prefixValue = '', ...typographyProps } = props; @@ -21,10 +21,10 @@ function EditableTypography(props) { const [editing, setEditing] = React.useState(false); const handleClick = React.useCallback(() => { - if (disabledEditing) return; + if (disabled) return; setEditing((editing) => !editing); - }, [disabledEditing]); + }, [disabled]); const handleTextFieldClick = React.useCallback((event) => { event.stopPropagation(); @@ -33,8 +33,9 @@ function EditableTypography(props) { const handleTextFieldKeyDown = React.useCallback( async (event) => { const target = event.target; + const eventKey = event.key; - if (event.key === 'Enter') { + if (eventKey === 'Enter') { if (target.value !== children) { await onConfirm(target.value); } @@ -42,11 +43,11 @@ function EditableTypography(props) { setEditing(false); } - if (event.key === 'Escape') { + if (eventKey === 'Escape') { setEditing(false); } }, - [children], + [children, onConfirm], ); const handleTextFieldBlur = React.useCallback( @@ -84,19 +85,14 @@ function EditableTypography(props) { } return ( - - {iconPosition === 'start' && editing === false && ( + + {!disabled && iconPosition === 'start' && editing === false && ( )} {component} - {iconPosition === 'end' && editing === false && ( + {!disabled && iconPosition === 'end' && editing === false && ( )} @@ -105,7 +101,7 @@ function EditableTypography(props) { EditableTypography.propTypes = { children: PropTypes.string.isRequired, - disabledEditing: PropTypes.bool, + disabled: PropTypes.bool, iconPosition: PropTypes.oneOf(['start', 'end']), iconSize: PropTypes.oneOf(['small', 'large']), onConfirm: PropTypes.func, diff --git a/packages/web/src/components/EditableTypography/style.js b/packages/web/src/components/EditableTypography/style.js index e99cb6ea..4ed685b8 100644 --- a/packages/web/src/components/EditableTypography/style.js +++ b/packages/web/src/components/EditableTypography/style.js @@ -3,8 +3,7 @@ import MuiBox from '@mui/material/Box'; import MuiTextField from '@mui/material/TextField'; import { inputClasses } from '@mui/material/Input'; -const boxShouldForwardProp = (prop) => - !['editing', 'disabledEditing'].includes(prop); +const boxShouldForwardProp = (prop) => !['editing', 'disabled'].includes(prop); export const Box = styled(MuiBox, { shouldForwardProp: boxShouldForwardProp, @@ -15,7 +14,7 @@ export const Box = styled(MuiBox, { max-width: 90%; height: 33px; align-items: center; - ${({ disabledEditing }) => !disabledEditing && 'cursor: pointer;'} + ${({ disabled }) => !disabled && 'cursor: pointer;'} ${({ editing }) => editing && 'border-bottom: 1px dashed #000;'} `; From dadd35d791382d8644c8d55bb1a91599155e33e7 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Wed, 8 Jan 2025 15:10:46 +0000 Subject: [PATCH 26/45] feat(FlowStep): disable editing step name when flow is published --- packages/web/src/components/FlowStep/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/components/FlowStep/index.jsx b/packages/web/src/components/FlowStep/index.jsx index df7e8ec6..fec9f2d8 100644 --- a/packages/web/src/components/FlowStep/index.jsx +++ b/packages/web/src/components/FlowStep/index.jsx @@ -270,7 +270,7 @@ function FlowStep(props) { variant="body2" onConfirm={handleStepNameChange} prefixValue={`${step.position}. `} - disabledEditing={collapsed} + disabled={editorContext.readOnly || collapsed} > {step.name} From f6be5f12636dc173eeac98a928104cb197850621 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Wed, 8 Jan 2025 15:11:15 +0000 Subject: [PATCH 27/45] feat(Editor): use iniitial event name as step name when empty --- packages/web/src/components/Editor/index.jsx | 4 ++-- packages/web/src/components/EditorNew/EditorNew.jsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/web/src/components/Editor/index.jsx b/packages/web/src/components/Editor/index.jsx index 5de95d4e..9170d06d 100644 --- a/packages/web/src/components/Editor/index.jsx +++ b/packages/web/src/components/Editor/index.jsx @@ -27,8 +27,8 @@ function Editor(props) { connectionId: step.connection?.id, }; - if (step.name) { - payload.name = step.name; + if (step.name || step.keyLabel) { + payload.name = step.name || step.keyLabel; } if (step.appKey) { diff --git a/packages/web/src/components/EditorNew/EditorNew.jsx b/packages/web/src/components/EditorNew/EditorNew.jsx index 7d52e3cc..5e981906 100644 --- a/packages/web/src/components/EditorNew/EditorNew.jsx +++ b/packages/web/src/components/EditorNew/EditorNew.jsx @@ -90,7 +90,7 @@ const EditorNew = ({ flow }) => { connectionId: step.connection?.id, }; - if (step.name) { + if (step.name || step.keyLabel) { payload.name = step.name || step.keyLabel; } From 99c9d83c371ed49ecc7e8b0acb665a0dfc3285c0 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Thu, 9 Jan 2025 11:00:36 +0000 Subject: [PATCH 28/45] feat(ExecutionStep): show step name in execution history --- .../src/components/ExecutionStep/index.jsx | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/web/src/components/ExecutionStep/index.jsx b/packages/web/src/components/ExecutionStep/index.jsx index 91f61a77..a1772307 100644 --- a/packages/web/src/components/ExecutionStep/index.jsx +++ b/packages/web/src/components/ExecutionStep/index.jsx @@ -9,6 +9,7 @@ import Tab from '@mui/material/Tab'; import Typography from '@mui/material/Typography'; import Tooltip from '@mui/material/Tooltip'; import Box from '@mui/material/Box'; +import Chip from '@mui/material/Chip'; import TabPanel from 'components/TabPanel'; import SearchableJSONViewer from 'components/SearchableJSONViewer'; @@ -100,6 +101,10 @@ function ExecutionStep(props) { const hasError = !!executionStep.errorDetails; + const stepTypeName = isTrigger + ? formatMessage('flowStep.triggerType') + : formatMessage('flowStep.actionType'); + return (
@@ -119,13 +124,20 @@ function ExecutionStep(props) { - - {isTrigger && formatMessage('flowStep.triggerType')} - {isAction && formatMessage('flowStep.actionType')} + + + + {app?.name} - {step.position}. {app?.name} + {step.position}. {step.name} From 4f24c0881a2fb420f0dfadd60ca83c173742b32f Mon Sep 17 00:00:00 2001 From: "kasia.oczkowska" Date: Fri, 6 Dec 2024 11:52:44 +0000 Subject: [PATCH 29/45] feat: introduce inline error messages for create user and create role forms --- .../web/src/components/Container/index.jsx | 17 +- packages/web/src/helpers/errors.js | 18 ++ packages/web/src/locales/en.json | 5 + .../web/src/pages/CreateRole/index.ee.jsx | 169 +++++++++--- packages/web/src/pages/CreateUser/index.jsx | 244 ++++++++++++------ 5 files changed, 330 insertions(+), 123 deletions(-) create mode 100644 packages/web/src/helpers/errors.js diff --git a/packages/web/src/components/Container/index.jsx b/packages/web/src/components/Container/index.jsx index ffafaa14..ef75335b 100644 --- a/packages/web/src/components/Container/index.jsx +++ b/packages/web/src/components/Container/index.jsx @@ -1,10 +1,19 @@ import * as React from 'react'; +import PropTypes from 'prop-types'; import MuiContainer from '@mui/material/Container'; -export default function Container(props) { - return ; +export default function Container({ maxWidth = 'lg', ...props }) { + return ; } -Container.defaultProps = { - maxWidth: 'lg', +Container.propTypes = { + maxWidth: PropTypes.oneOf([ + 'xs', + 'sm', + 'md', + 'lg', + 'xl', + false, + PropTypes.string, + ]), }; diff --git a/packages/web/src/helpers/errors.js b/packages/web/src/helpers/errors.js new file mode 100644 index 00000000..dc73867d --- /dev/null +++ b/packages/web/src/helpers/errors.js @@ -0,0 +1,18 @@ +// Helpers to extract errors received from the API + +export const getGeneralErrorMessage = ({ error, fallbackMessage }) => { + if (!error) { + return; + } + + const errors = error?.response?.data?.errors; + const generalError = errors?.general; + + if (generalError && Array.isArray(generalError)) { + return generalError.join(' '); + } + + if (!errors) { + return error?.message || fallbackMessage; + } +}; diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index c9422557..bb95e979 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -226,6 +226,8 @@ "userForm.email": "Email", "userForm.role": "Role", "userForm.password": "Password", + "userForm.mandatoryInput": "{inputName} is required.", + "userForm.validateEmail": "Email must be valid.", "createUser.submit": "Create", "createUser.successfullyCreated": "The user has been created.", "createUser.invitationEmailInfo": "Invitation email will be sent if SMTP credentials are valid. Otherwise, you can share the invitation link manually: ", @@ -250,8 +252,11 @@ "createRolePage.title": "Create role", "roleForm.name": "Name", "roleForm.description": "Description", + "roleForm.mandatoryInput": "{inputName} is required.", "createRole.submit": "Create", "createRole.successfullyCreated": "The role has been created.", + "createRole.generalError": "Error while creating the role.", + "createRole.permissionsError": "Permissions are invalid.", "editRole.submit": "Update", "editRole.successfullyUpdated": "The role has been updated.", "roleList.name": "Name", diff --git a/packages/web/src/pages/CreateRole/index.ee.jsx b/packages/web/src/pages/CreateRole/index.ee.jsx index 99a66901..c2eb7d1a 100644 --- a/packages/web/src/pages/CreateRole/index.ee.jsx +++ b/packages/web/src/pages/CreateRole/index.ee.jsx @@ -1,10 +1,14 @@ import LoadingButton from '@mui/lab/LoadingButton'; import Grid from '@mui/material/Grid'; import Stack from '@mui/material/Stack'; +import Alert from '@mui/material/Alert'; +import AlertTitle from '@mui/material/AlertTitle'; import PermissionCatalogField from 'components/PermissionCatalogField/index.ee'; import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import * as React from 'react'; import { useNavigate } from 'react-router-dom'; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as yup from 'yup'; import Container from 'components/Container'; import Form from 'components/Form'; @@ -15,10 +19,45 @@ import { getComputedPermissionsDefaultValues, getPermissions, } from 'helpers/computePermissions.ee'; +import { getGeneralErrorMessage } from 'helpers/errors'; import useFormatMessage from 'hooks/useFormatMessage'; import useAdminCreateRole from 'hooks/useAdminCreateRole'; import usePermissionCatalog from 'hooks/usePermissionCatalog.ee'; +const getValidationSchema = (formatMessage) => { + const getMandatoryFieldMessage = (fieldTranslationId) => + formatMessage('roleForm.mandatoryInput', { + inputName: formatMessage(fieldTranslationId), + }); + + return yup.object().shape({ + name: yup + .string() + .trim() + .required(getMandatoryFieldMessage('roleForm.name')), + description: yup.string().trim(), + }); +}; + +const getPermissionsErrorMessage = (error) => { + const errors = error?.response?.data?.errors; + + if (errors) { + const permissionsErrors = Object.keys(errors) + .filter((key) => key.startsWith('permissions')) + .reduce((obj, key) => { + obj[key] = errors[key]; + return obj; + }, {}); + + if (Object.keys(permissionsErrors).length > 0) { + return JSON.stringify(permissionsErrors, null, 2); + } + } + + return null; +}; + export default function CreateRole() { const navigate = useNavigate(); const formatMessage = useFormatMessage(); @@ -42,7 +81,7 @@ export default function CreateRole() { [permissionCatalogData], ); - const handleRoleCreation = async (roleData) => { + const handleRoleCreation = async (roleData, e, setError) => { try { const permissions = getPermissions(roleData.computedPermissions); @@ -61,14 +100,38 @@ export default function CreateRole() { navigate(URLS.ROLES); } catch (error) { - const errors = Object.values(error.response.data.errors); + const errors = error?.response?.data?.errors; - for (const [errorMessage] of errors) { - enqueueSnackbar(errorMessage, { - variant: 'error', - SnackbarProps: { - 'data-test': 'snackbar-error', - }, + if (errors) { + const fieldNames = ['name', 'description']; + Object.entries(errors).forEach(([fieldName, fieldErrors]) => { + if (fieldNames.includes(fieldName) && Array.isArray(fieldErrors)) { + setError(fieldName, { + type: 'fieldRequestError', + message: fieldErrors.join(', '), + }); + } + }); + } + + const permissionError = getPermissionsErrorMessage(error); + + if (permissionError) { + setError('root.permissions', { + type: 'fieldRequestError', + message: permissionError, + }); + } + + const generalError = getGeneralErrorMessage({ + error, + fallbackMessage: formatMessage('createRole.generalError'), + }); + + if (generalError) { + setError('root.general', { + type: 'requestError', + message: generalError, }); } } @@ -84,39 +147,69 @@ export default function CreateRole() { - - - + ( + + - + - + - - {formatMessage('createRole.submit')} - - - + {errors?.root?.permissions && ( + + + {formatMessage('createRole.permissionsError')} + +
+                      {errors?.root?.permissions?.message}
+                    
+
+ )} + + {errors?.root?.general && ( + + {errors?.root?.general?.message} + + )} + + + {formatMessage('createRole.submit')} + +
+ )} + />
diff --git a/packages/web/src/pages/CreateUser/index.jsx b/packages/web/src/pages/CreateUser/index.jsx index ad96ba96..689791e5 100644 --- a/packages/web/src/pages/CreateUser/index.jsx +++ b/packages/web/src/pages/CreateUser/index.jsx @@ -3,9 +3,10 @@ import Grid from '@mui/material/Grid'; import Stack from '@mui/material/Stack'; import Alert from '@mui/material/Alert'; import MuiTextField from '@mui/material/TextField'; -import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import * as React from 'react'; import { useQueryClient } from '@tanstack/react-query'; +import * as yup from 'yup'; +import { yupResolver } from '@hookform/resolvers/yup'; import Can from 'components/Can'; import Container from 'components/Container'; @@ -16,50 +17,94 @@ import TextField from 'components/TextField'; import useFormatMessage from 'hooks/useFormatMessage'; import useRoles from 'hooks/useRoles.ee'; import useAdminCreateUser from 'hooks/useAdminCreateUser'; +import useCurrentUserAbility from 'hooks/useCurrentUserAbility'; +import { getGeneralErrorMessage } from 'helpers/errors'; function generateRoleOptions(roles) { return roles?.map(({ name: label, id: value }) => ({ label, value })); } +const getValidationSchema = (formatMessage, canUpdateRole) => { + const getMandatoryFieldMessage = (fieldTranslationId) => + formatMessage('userForm.mandatoryInput', { + inputName: formatMessage(fieldTranslationId), + }); + + return yup.object().shape({ + fullName: yup + .string() + .trim() + .required(getMandatoryFieldMessage('userForm.fullName')), + email: yup + .string() + .trim() + .email(formatMessage('userForm.validateEmail')) + .required(getMandatoryFieldMessage('userForm.email')), + ...(canUpdateRole + ? { + roleId: yup + .string() + .required(getMandatoryFieldMessage('userForm.role')), + } + : {}), + }); +}; + +const defaultValues = { + fullName: '', + email: '', + roleId: '', +}; + export default function CreateUser() { const formatMessage = useFormatMessage(); const { mutateAsync: createUser, isPending: isCreateUserPending, data: createdUser, + isSuccess: createUserSuccess, } = useAdminCreateUser(); const { data: rolesData, loading: isRolesLoading } = useRoles(); const roles = rolesData?.data; - const enqueueSnackbar = useEnqueueSnackbar(); const queryClient = useQueryClient(); + const currentUserAbility = useCurrentUserAbility(); + const canUpdateRole = currentUserAbility.can('update', 'Role'); - const handleUserCreation = async (userData) => { + const handleUserCreation = async (userData, e, setError) => { try { await createUser({ fullName: userData.fullName, email: userData.email, - roleId: userData.role?.id, + roleId: userData.roleId, }); queryClient.invalidateQueries({ queryKey: ['admin', 'users'] }); - - enqueueSnackbar(formatMessage('createUser.successfullyCreated'), { - variant: 'success', - persist: true, - SnackbarProps: { - 'data-test': 'snackbar-create-user-success', - }, - }); } catch (error) { - enqueueSnackbar(formatMessage('createUser.error'), { - variant: 'error', - persist: true, - SnackbarProps: { - 'data-test': 'snackbar-error', - }, + const errors = error?.response?.data?.errors; + + if (errors) { + const fieldNames = Object.keys(defaultValues); + Object.entries(errors).forEach(([fieldName, fieldErrors]) => { + if (fieldNames.includes(fieldName) && Array.isArray(fieldErrors)) { + setError(fieldName, { + type: 'fieldRequestError', + message: fieldErrors.join(', '), + }); + } + }); + } + + const generalError = getGeneralErrorMessage({ + error, + fallbackMessage: formatMessage('createUser.error'), }); - throw new Error('Failed while creating!'); + if (generalError) { + setError('root.general', { + type: 'requestError', + message: generalError, + }); + } } }; @@ -73,74 +118,111 @@ export default function CreateUser() { -
- - - - - - - ( + + ( - - )} - loading={isRolesLoading} + error={!!errors?.fullName} + helperText={errors?.fullName?.message} /> - - - {formatMessage('createUser.submit')} - + - {createdUser && ( - + ( + + )} + loading={isRolesLoading} + showHelperText={false} + /> + + + {errors?.root?.general && ( + + {errors?.root?.general?.message} + + )} + + {createUserSuccess && ( + + {formatMessage('createUser.successfullyCreated')} + + )} + + {createdUser && ( + + {formatMessage('createUser.invitationEmailInfo', { + link: () => ( + + {createdUser.data.acceptInvitationUrl} + + ), + })} + + )} + + - {formatMessage('createUser.invitationEmailInfo', { - link: () => ( - - {createdUser.data.acceptInvitationUrl} - - ), - })} - - )} - -
+ {formatMessage('createUser.submit')} + + + )} + />
From 8f5909d7b0e5aaf050fee854131a7ecd91381bb5 Mon Sep 17 00:00:00 2001 From: "Jakub P." Date: Tue, 10 Dec 2024 00:32:51 +0100 Subject: [PATCH 30/45] test: use new alert in create role and create user tests --- .../fixtures/admin/create-user-page.js | 18 +- .../tests/admin/manage-roles.spec.js | 154 +++--- .../tests/admin/manage-users.spec.js | 442 ++++++++---------- .../tests/flow-editor/create-flow.spec.js | 335 +++++++------ .../tests/my-profile/profile-updates.spec.js | 5 +- 5 files changed, 430 insertions(+), 524 deletions(-) diff --git a/packages/e2e-tests/fixtures/admin/create-user-page.js b/packages/e2e-tests/fixtures/admin/create-user-page.js index 135b38fb..ddf0f6e6 100644 --- a/packages/e2e-tests/fixtures/admin/create-user-page.js +++ b/packages/e2e-tests/fixtures/admin/create-user-page.js @@ -1,3 +1,5 @@ +const { expect } = require('@playwright/test'); + const { faker } = require('@faker-js/faker'); const { AuthenticatedPage } = require('../authenticated-page'); @@ -11,11 +13,17 @@ export class AdminCreateUserPage extends AuthenticatedPage { super(page); this.fullNameInput = page.getByTestId('full-name-input'); this.emailInput = page.getByTestId('email-input'); - this.roleInput = page.getByTestId('role.id-autocomplete'); + this.roleInput = page.getByTestId('roleId-autocomplete'); this.createButton = page.getByTestId('create-button'); this.pageTitle = page.getByTestId('create-user-title'); - this.invitationEmailInfoAlert = page.getByTestId('invitation-email-info-alert'); - this.acceptInvitationLink = page.getByTestId('invitation-email-info-alert').getByRole('link'); + this.invitationEmailInfoAlert = page.getByTestId( + 'invitation-email-info-alert' + ); + this.acceptInvitationLink = page + .getByTestId('invitation-email-info-alert') + .getByRole('link'); + this.createUserSuccessAlert = page.getByTestId('create-user-success-alert'); + this.fieldError = page.locator('p[id$="-helper-text"]'); } seed(seed) { @@ -28,4 +36,8 @@ export class AdminCreateUserPage extends AuthenticatedPage { email: faker.internet.email().toLowerCase(), }; } + + async expectCreateUserSuccessAlertToBeVisible() { + await expect(this.createUserSuccessAlert).toBeVisible(); + } } diff --git a/packages/e2e-tests/tests/admin/manage-roles.spec.js b/packages/e2e-tests/tests/admin/manage-roles.spec.js index 00299c5d..1e9a405f 100644 --- a/packages/e2e-tests/tests/admin/manage-roles.spec.js +++ b/packages/e2e-tests/tests/admin/manage-roles.spec.js @@ -35,9 +35,8 @@ test.describe('Role management page', () => { await adminCreateRolePage.closeSnackbar(); }); - let roleRow = await test.step( - 'Make sure role data is correct', - async () => { + let roleRow = + await test.step('Make sure role data is correct', async () => { const roleRow = await adminRolesPage.getRoleRowByName( 'Create Edit Test' ); @@ -48,8 +47,7 @@ test.describe('Role management page', () => { await expect(roleData.canEdit).toBe(true); await expect(roleData.canDelete).toBe(true); return roleRow; - } - ); + }); await test.step('Edit the role', async () => { await adminRolesPage.clickEditRole(roleRow); @@ -67,9 +65,8 @@ test.describe('Role management page', () => { await adminEditRolePage.closeSnackbar(); }); - roleRow = await test.step( - 'Make sure changes reflected on roles page', - async () => { + roleRow = + await test.step('Make sure changes reflected on roles page', async () => { await adminRolesPage.isMounted(); const roleRow = await adminRolesPage.getRoleRowByName( 'Create Update Test' @@ -81,8 +78,7 @@ test.describe('Role management page', () => { await expect(roleData.canEdit).toBe(true); await expect(roleData.canDelete).toBe(true); return roleRow; - } - ); + }); await test.step('Delete the role', async () => { await adminRolesPage.clickDeleteRole(roleRow); @@ -184,49 +180,39 @@ test.describe('Role management page', () => { await expect(snackbar.variant).toBe('success'); await adminCreateRolePage.closeSnackbar(); }); - await test.step( - 'Create a new user with the "Delete Role" role', - async () => { - await adminUsersPage.navigateTo(); - await adminUsersPage.createUserButton.click(); - await adminCreateUserPage.fullNameInput.fill('User Role Test'); - await adminCreateUserPage.emailInput.fill( - 'user-role-test@automatisch.io' - ); - await adminCreateUserPage.roleInput.click(); - await adminCreateUserPage.page - .getByRole('option', { name: 'Delete Role', exact: true }) - .click(); - await adminCreateUserPage.createButton.click(); - await adminCreateUserPage.snackbar.waitFor({ - state: 'attached', - }); - await adminCreateUserPage.invitationEmailInfoAlert.waitFor({ - state: 'attached', - }); - const snackbar = await adminUsersPage.getSnackbarData( - 'snackbar-create-user-success' - ); - await expect(snackbar.variant).toBe('success'); - await adminUsersPage.closeSnackbar(); - } - ); - await test.step( - 'Try to delete "Delete Role" role when new user has it', - async () => { - await adminRolesPage.navigateTo(); - const row = await adminRolesPage.getRoleRowByName('Delete Role'); - const modal = await adminRolesPage.clickDeleteRole(row); - await modal.deleteButton.click(); - await adminRolesPage.snackbar.waitFor({ - state: 'attached', - }); - const snackbar = await adminRolesPage.getSnackbarData('snackbar-delete-role-error'); - await expect(snackbar.variant).toBe('error'); - await adminRolesPage.closeSnackbar(); - await modal.close(); - } - ); + await test.step('Create a new user with the "Delete Role" role', async () => { + await adminUsersPage.navigateTo(); + await adminUsersPage.createUserButton.click(); + await adminCreateUserPage.fullNameInput.fill('User Role Test'); + await adminCreateUserPage.emailInput.fill( + 'user-role-test@automatisch.io' + ); + await adminCreateUserPage.roleInput.click(); + await adminCreateUserPage.page + .getByRole('option', { name: 'Delete Role', exact: true }) + .click(); + await adminCreateUserPage.createButton.click(); + await adminCreateUserPage.invitationEmailInfoAlert.waitFor({ + state: 'attached', + }); + await adminCreateUserPage.expectCreateUserSuccessAlertToBeVisible(); + }); + + await test.step('Try to delete "Delete Role" role when new user has it', async () => { + await adminRolesPage.navigateTo(); + const row = await adminRolesPage.getRoleRowByName('Delete Role'); + const modal = await adminRolesPage.clickDeleteRole(row); + await modal.deleteButton.click(); + await adminRolesPage.snackbar.waitFor({ + state: 'attached', + }); + const snackbar = await adminRolesPage.getSnackbarData( + 'snackbar-delete-role-error' + ); + await expect(snackbar.variant).toBe('error'); + await adminRolesPage.closeSnackbar(); + await modal.close(); + }); await test.step('Change the role the user has', async () => { await adminUsersPage.navigateTo(); await adminUsersPage.usersLoader.waitFor({ @@ -301,24 +287,16 @@ test.describe('Role management page', () => { .getByRole('option', { name: 'Cannot Delete Role' }) .click(); await adminCreateUserPage.createButton.click(); - await adminCreateUserPage.snackbar.waitFor({ - state: 'attached', - }); await adminCreateUserPage.invitationEmailInfoAlert.waitFor({ state: 'attached', }); - const snackbar = await adminCreateUserPage.getSnackbarData( - 'snackbar-create-user-success' - ); - await expect(snackbar.variant).toBe('success'); - await adminCreateUserPage.closeSnackbar(); + await adminCreateUserPage.expectCreateUserSuccessAlertToBeVisible(); }); await test.step('Delete this user', async () => { await adminUsersPage.navigateTo(); const row = await adminUsersPage.findUserPageWithEmail( 'user-delete-role-test@automatisch.io' ); - // await test.waitForTimeout(10000); const modal = await adminUsersPage.clickDeleteUser(row); await modal.deleteButton.click(); await adminUsersPage.snackbar.waitFor({ @@ -385,17 +363,10 @@ test('Accessibility of role management page', async ({ .getByRole('option', { name: 'Basic Test' }) .click(); await adminCreateUserPage.createButton.click(); - await adminCreateUserPage.snackbar.waitFor({ - state: 'attached', - }); await adminCreateUserPage.invitationEmailInfoAlert.waitFor({ state: 'attached', }); - const snackbar = await adminCreateUserPage.getSnackbarData( - 'snackbar-create-user-success' - ); - await expect(snackbar.variant).toBe('success'); - await adminCreateUserPage.closeSnackbar(); + await adminCreateUserPage.expectCreateUserSuccessAlertToBeVisible(); }); await test.step('Logout and login to the basic role user', async () => { @@ -409,42 +380,35 @@ test('Accessibility of role management page', async ({ await page.getByTestId('logout-item').click(); const acceptInvitationPage = new AcceptInvitation(page); - await acceptInvitationPage.open(acceptInvitatonToken); - await acceptInvitationPage.acceptInvitation('sample'); const loginPage = new LoginPage(page); - - // await loginPage.isMounted(); await loginPage.login('basic-role-test@automatisch.io', 'sample'); await expect(loginPage.loginButton).not.toBeVisible(); await expect(page).toHaveURL('/flows'); }); - await test.step( - 'Navigate to the admin settings page and make sure it is blank', - async () => { - const pageUrl = new URL(page.url()); - const url = `${pageUrl.origin}/admin-settings/users`; - await page.goto(url); - await page.waitForTimeout(750); - const isUnmounted = await page.evaluate(() => { - // eslint-disable-next-line no-undef - const root = document.querySelector('#root'); + await test.step('Navigate to the admin settings page and make sure it is blank', async () => { + const pageUrl = new URL(page.url()); + const url = `${pageUrl.origin}/admin-settings/users`; + await page.goto(url); + await page.waitForTimeout(750); + const isUnmounted = await page.evaluate(() => { + // eslint-disable-next-line no-undef + const root = document.querySelector('#root'); - if (root) { - // We have react query devtools only in dev env. - // In production, there is nothing in root. - // That's why `<= 1`. - return root.children.length <= 1; - } + if (root) { + // We have react query devtools only in dev env. + // In production, there is nothing in root. + // That's why `<= 1`. + return root.children.length <= 1; + } - return false; - }); - await expect(isUnmounted).toBe(true); - } - ); + return false; + }); + await expect(isUnmounted).toBe(true); + }); await test.step('Log back into the admin account', async () => { await page.goto('/'); diff --git a/packages/e2e-tests/tests/admin/manage-users.spec.js b/packages/e2e-tests/tests/admin/manage-users.spec.js index d6fc1507..af7f7083 100644 --- a/packages/e2e-tests/tests/admin/manage-users.spec.js +++ b/packages/e2e-tests/tests/admin/manage-users.spec.js @@ -5,281 +5,221 @@ const { test, expect } = require('../../fixtures/index'); * otherwise tests will fail since users are only *soft*-deleted */ test.describe('User management page', () => { - test.beforeEach(async ({ adminUsersPage }) => { await adminUsersPage.navigateTo(); await adminUsersPage.closeSnackbar(); }); - test( - 'User creation and deletion process', - async ({ adminCreateUserPage, adminEditUserPage, adminUsersPage }) => { - adminCreateUserPage.seed(9000); - const user = adminCreateUserPage.generateUser(); - await adminUsersPage.usersLoader.waitFor({ - state: 'detached' /* Note: state: 'visible' introduces flakiness + test('User creation and deletion process', async ({ + adminCreateUserPage, + adminEditUserPage, + adminUsersPage, + }) => { + adminCreateUserPage.seed(9000); + const user = adminCreateUserPage.generateUser(); + await adminUsersPage.usersLoader.waitFor({ + state: 'detached' /* Note: state: 'visible' introduces flakiness because visibility: hidden is used as part of the state transition in notistack, see https://github.com/iamhosseindhv/notistack/blob/122f47057eb7ce5a1abfe923316cf8475303e99a/src/transitions/Collapse/Collapse.tsx#L110 - */ + */, + }); + await test.step('Create a user', async () => { + await adminUsersPage.createUserButton.click(); + await adminCreateUserPage.fullNameInput.fill(user.fullName); + await adminCreateUserPage.emailInput.fill(user.email); + await adminCreateUserPage.roleInput.click(); + await adminCreateUserPage.page + .getByRole('option', { name: 'Admin' }) + .click(); + await adminCreateUserPage.createButton.click(); + await adminCreateUserPage.invitationEmailInfoAlert.waitFor({ + state: 'attached', }); - await test.step( - 'Create a user', - async () => { - await adminUsersPage.createUserButton.click(); - await adminCreateUserPage.fullNameInput.fill(user.fullName); - await adminCreateUserPage.emailInput.fill(user.email); - await adminCreateUserPage.roleInput.click(); - await adminCreateUserPage.page.getByRole( - 'option', { name: 'Admin' } - ).click(); - await adminCreateUserPage.createButton.click(); - await adminCreateUserPage.invitationEmailInfoAlert.waitFor({ - state: 'attached' - }); - const snackbar = await adminUsersPage.getSnackbarData( - 'snackbar-create-user-success' - ); - await expect(snackbar.variant).toBe('success'); - await adminUsersPage.navigateTo(); - await adminUsersPage.closeSnackbar(); - } + await adminCreateUserPage.expectCreateUserSuccessAlertToBeVisible(); + await adminUsersPage.navigateTo(); + }); + await test.step('Check the user exists with the expected properties', async () => { + await adminUsersPage.findUserPageWithEmail(user.email); + const userRow = await adminUsersPage.getUserRowByEmail(user.email); + const data = await adminUsersPage.getRowData(userRow); + await expect(data.email).toBe(user.email); + await expect(data.fullName).toBe(user.fullName); + await expect(data.role).toBe('Admin'); + }); + await test.step('Edit user info and make sure the edit works correctly', async () => { + await adminUsersPage.findUserPageWithEmail(user.email); + + let userRow = await adminUsersPage.getUserRowByEmail(user.email); + await adminUsersPage.clickEditUser(userRow); + await adminEditUserPage.waitForLoad(user.fullName); + const newUserInfo = adminEditUserPage.generateUser(); + await adminEditUserPage.fullNameInput.fill(newUserInfo.fullName); + await adminEditUserPage.updateButton.click(); + + const snackbar = await adminUsersPage.getSnackbarData( + 'snackbar-edit-user-success' ); - await test.step( - 'Check the user exists with the expected properties', - async () => { - await adminUsersPage.findUserPageWithEmail(user.email); - const userRow = await adminUsersPage.getUserRowByEmail(user.email); - const data = await adminUsersPage.getRowData(userRow); - await expect(data.email).toBe(user.email); - await expect(data.fullName).toBe(user.fullName); - await expect(data.role).toBe('Admin'); - } + await expect(snackbar.variant).toBe('success'); + await adminUsersPage.closeSnackbar(); + + await adminUsersPage.findUserPageWithEmail(user.email); + userRow = await adminUsersPage.getUserRowByEmail(user.email); + const rowData = await adminUsersPage.getRowData(userRow); + await expect(rowData.fullName).toBe(newUserInfo.fullName); + }); + await test.step('Delete user and check the page confirms this deletion', async () => { + await adminUsersPage.findUserPageWithEmail(user.email); + const userRow = await adminUsersPage.getUserRowByEmail(user.email); + await adminUsersPage.clickDeleteUser(userRow); + const modal = adminUsersPage.deleteUserModal; + await modal.deleteButton.click(); + + const snackbar = await adminUsersPage.getSnackbarData( + 'snackbar-delete-user-success' ); - await test.step( - 'Edit user info and make sure the edit works correctly', - async () => { - await adminUsersPage.findUserPageWithEmail(user.email); + await expect(snackbar.variant).toBe('success'); + await adminUsersPage.closeSnackbar(); + await expect(userRow).not.toBeVisible(false); + }); + }); - let userRow = await adminUsersPage.getUserRowByEmail(user.email); - await adminUsersPage.clickEditUser(userRow); - await adminEditUserPage.waitForLoad(user.fullName); - const newUserInfo = adminEditUserPage.generateUser(); - await adminEditUserPage.fullNameInput.fill(newUserInfo.fullName); - await adminEditUserPage.updateButton.click(); + test('Creating a user which has been deleted', async ({ + adminCreateUserPage, + adminUsersPage, + }) => { + adminCreateUserPage.seed(9100); + const testUser = adminCreateUserPage.generateUser(); - const snackbar = await adminUsersPage.getSnackbarData( - 'snackbar-edit-user-success' - ); - await expect(snackbar.variant).toBe('success'); - await adminUsersPage.closeSnackbar(); - - await adminUsersPage.findUserPageWithEmail(user.email); - userRow = await adminUsersPage.getUserRowByEmail(user.email); - const rowData = await adminUsersPage.getRowData(userRow); - await expect(rowData.fullName).toBe(newUserInfo.fullName); - } - ); - await test.step( - 'Delete user and check the page confirms this deletion', - async () => { - await adminUsersPage.findUserPageWithEmail(user.email); - const userRow = await adminUsersPage.getUserRowByEmail(user.email); - await adminUsersPage.clickDeleteUser(userRow); - const modal = adminUsersPage.deleteUserModal; - await modal.deleteButton.click(); - - const snackbar = await adminUsersPage.getSnackbarData( - 'snackbar-delete-user-success' - ); - await expect(snackbar.variant).toBe('success'); - await adminUsersPage.closeSnackbar(); - await expect(userRow).not.toBeVisible(false); - } - ); + await test.step('Create the test user', async () => { + await adminUsersPage.navigateTo(); + await adminUsersPage.createUserButton.click(); + await adminCreateUserPage.fullNameInput.fill(testUser.fullName); + await adminCreateUserPage.emailInput.fill(testUser.email); + await adminCreateUserPage.roleInput.click(); + await adminCreateUserPage.page + .getByRole('option', { name: 'Admin' }) + .click(); + await adminCreateUserPage.createButton.click(); + await adminCreateUserPage.expectCreateUserSuccessAlertToBeVisible(); }); - test( - 'Creating a user which has been deleted', - async ({ adminCreateUserPage, adminUsersPage }) => { - adminCreateUserPage.seed(9100); - const testUser = adminCreateUserPage.generateUser(); - - await test.step( - 'Create the test user', - async () => { - await adminUsersPage.navigateTo(); - await adminUsersPage.createUserButton.click(); - await adminCreateUserPage.fullNameInput.fill(testUser.fullName); - await adminCreateUserPage.emailInput.fill(testUser.email); - await adminCreateUserPage.roleInput.click(); - await adminCreateUserPage.page.getByRole( - 'option', { name: 'Admin' } - ).click(); - await adminCreateUserPage.createButton.click(); - const snackbar = await adminUsersPage.getSnackbarData( - 'snackbar-create-user-success' - ); - await expect(snackbar.variant).toBe('success'); - await adminUsersPage.closeSnackbar(); - } + await test.step('Delete the created user', async () => { + await adminUsersPage.navigateTo(); + await adminUsersPage.findUserPageWithEmail(testUser.email); + const userRow = await adminUsersPage.getUserRowByEmail(testUser.email); + await adminUsersPage.clickDeleteUser(userRow); + const modal = adminUsersPage.deleteUserModal; + await modal.deleteButton.click(); + const snackbar = await adminUsersPage.getSnackbarData( + 'snackbar-delete-user-success' ); + await expect(snackbar).not.toBeNull(); + await expect(snackbar.variant).toBe('success'); + await adminUsersPage.closeSnackbar(); + await expect(userRow).not.toBeVisible(false); + }); - await test.step( - 'Delete the created user', - async () => { - await adminUsersPage.navigateTo(); - await adminUsersPage.findUserPageWithEmail(testUser.email); - const userRow = await adminUsersPage.getUserRowByEmail(testUser.email); - await adminUsersPage.clickDeleteUser(userRow); - const modal = adminUsersPage.deleteUserModal; - await modal.deleteButton.click(); - const snackbar = await adminUsersPage.getSnackbarData( - 'snackbar-delete-user-success' - ); - await expect(snackbar).not.toBeNull(); - await expect(snackbar.variant).toBe('success'); - await adminUsersPage.closeSnackbar(); - await expect(userRow).not.toBeVisible(false); - } - ); + await test.step('Create the user again', async () => { + await adminUsersPage.createUserButton.click(); + await adminCreateUserPage.fullNameInput.fill(testUser.fullName); + await adminCreateUserPage.emailInput.fill(testUser.email); + await adminCreateUserPage.roleInput.click(); + await adminCreateUserPage.page + .getByRole('option', { name: 'Admin' }) + .click(); + await adminCreateUserPage.createButton.click(); + await expect(adminCreateUserPage.fieldError).toHaveCount(1); + }); + }); - await test.step( - 'Create the user again', - async () => { - await adminUsersPage.createUserButton.click(); - await adminCreateUserPage.fullNameInput.fill(testUser.fullName); - await adminCreateUserPage.emailInput.fill(testUser.email); - await adminCreateUserPage.roleInput.click(); - await adminCreateUserPage.page.getByRole( - 'option', { name: 'Admin' } - ).click(); - await adminCreateUserPage.createButton.click(); - const snackbar = await adminUsersPage.getSnackbarData('snackbar-error'); - await expect(snackbar.variant).toBe('error'); - await adminUsersPage.closeSnackbar(); - } - ); - } - ); + test('Creating a user which already exists', async ({ + adminCreateUserPage, + adminUsersPage, + page, + }) => { + adminCreateUserPage.seed(9200); + const testUser = adminCreateUserPage.generateUser(); - test( - 'Creating a user which already exists', - async ({ adminCreateUserPage, adminUsersPage, page }) => { - adminCreateUserPage.seed(9200); - const testUser = adminCreateUserPage.generateUser(); + await test.step('Create the test user', async () => { + await adminUsersPage.createUserButton.click(); + await adminCreateUserPage.fullNameInput.fill(testUser.fullName); + await adminCreateUserPage.emailInput.fill(testUser.email); + await adminCreateUserPage.roleInput.click(); + await adminCreateUserPage.page + .getByRole('option', { name: 'Admin' }) + .click(); + await adminCreateUserPage.createButton.click(); + await adminCreateUserPage.expectCreateUserSuccessAlertToBeVisible(); + }); - await test.step( - 'Create the test user', - async () => { - await adminUsersPage.createUserButton.click(); - await adminCreateUserPage.fullNameInput.fill(testUser.fullName); - await adminCreateUserPage.emailInput.fill(testUser.email); - await adminCreateUserPage.roleInput.click(); - await adminCreateUserPage.page.getByRole( - 'option', { name: 'Admin' } - ).click(); - await adminCreateUserPage.createButton.click(); - const snackbar = await adminUsersPage.getSnackbarData( - 'snackbar-create-user-success' - ); - await expect(snackbar.variant).toBe('success'); - await adminUsersPage.closeSnackbar(); - } - ); + await test.step('Create the user again', async () => { + await adminUsersPage.navigateTo(); + await adminUsersPage.createUserButton.click(); + await adminCreateUserPage.fullNameInput.fill(testUser.fullName); + await adminCreateUserPage.emailInput.fill(testUser.email); + const createUserPageUrl = page.url(); + await adminCreateUserPage.roleInput.click(); + await adminCreateUserPage.page + .getByRole('option', { name: 'Admin' }) + .click(); + await adminCreateUserPage.createButton.click(); - await test.step( - 'Create the user again', - async () => { - await adminUsersPage.navigateTo(); - await adminUsersPage.createUserButton.click(); - await adminCreateUserPage.fullNameInput.fill(testUser.fullName); - await adminCreateUserPage.emailInput.fill(testUser.email); - const createUserPageUrl = page.url(); - await adminCreateUserPage.roleInput.click(); - await adminCreateUserPage.page.getByRole( - 'option', { name: 'Admin' } - ).click(); - await adminCreateUserPage.createButton.click(); + await expect(page.url()).toBe(createUserPageUrl); + await expect(adminCreateUserPage.fieldError).toHaveCount(1); + }); + }); - await expect(page.url()).toBe(createUserPageUrl); - const snackbar = await adminUsersPage.getSnackbarData('snackbar-error'); - await expect(snackbar.variant).toBe('error'); - await adminUsersPage.closeSnackbar(); - } - ); - } - ); + test('Editing a user to have the same email as another user should not be allowed', async ({ + adminCreateUserPage, + adminEditUserPage, + adminUsersPage, + page, + }) => { + adminCreateUserPage.seed(9300); + const user1 = adminCreateUserPage.generateUser(); + const user2 = adminCreateUserPage.generateUser(); + await test.step('Create the first user', async () => { + await adminUsersPage.navigateTo(); + await adminUsersPage.createUserButton.click(); + await adminCreateUserPage.fullNameInput.fill(user1.fullName); + await adminCreateUserPage.emailInput.fill(user1.email); + await adminCreateUserPage.roleInput.click(); + await adminCreateUserPage.page + .getByRole('option', { name: 'Admin' }) + .click(); + await adminCreateUserPage.createButton.click(); + await adminCreateUserPage.expectCreateUserSuccessAlertToBeVisible(); + }); - test( - 'Editing a user to have the same email as another user should not be allowed', - async ({ - adminCreateUserPage, adminEditUserPage, adminUsersPage, page - }) => { - adminCreateUserPage.seed(9300); - const user1 = adminCreateUserPage.generateUser(); - const user2 = adminCreateUserPage.generateUser(); - await test.step( - 'Create the first user', - async () => { - await adminUsersPage.navigateTo(); - await adminUsersPage.createUserButton.click(); - await adminCreateUserPage.fullNameInput.fill(user1.fullName); - await adminCreateUserPage.emailInput.fill(user1.email); - await adminCreateUserPage.roleInput.click(); - await adminCreateUserPage.page.getByRole( - 'option', { name: 'Admin' } - ).click(); - await adminCreateUserPage.createButton.click(); - const snackbar = await adminUsersPage.getSnackbarData( - 'snackbar-create-user-success' - ); - await expect(snackbar.variant).toBe('success'); - await adminUsersPage.closeSnackbar(); - } - ); + await test.step('Create the second user', async () => { + await adminUsersPage.navigateTo(); + await adminUsersPage.createUserButton.click(); + await adminCreateUserPage.fullNameInput.fill(user2.fullName); + await adminCreateUserPage.emailInput.fill(user2.email); + await adminCreateUserPage.roleInput.click(); + await adminCreateUserPage.page + .getByRole('option', { name: 'Admin' }) + .click(); + await adminCreateUserPage.createButton.click(); + await adminCreateUserPage.expectCreateUserSuccessAlertToBeVisible(); + }); - await test.step( - 'Create the second user', - async () => { - await adminUsersPage.navigateTo(); - await adminUsersPage.createUserButton.click(); - await adminCreateUserPage.fullNameInput.fill(user2.fullName); - await adminCreateUserPage.emailInput.fill(user2.email); - await adminCreateUserPage.roleInput.click(); - await adminCreateUserPage.page.getByRole( - 'option', { name: 'Admin' } - ).click(); - await adminCreateUserPage.createButton.click(); - const snackbar = await adminUsersPage.getSnackbarData( - 'snackbar-create-user-success' - ); - await expect(snackbar.variant).toBe('success'); - await adminUsersPage.closeSnackbar(); - } - ); + await test.step('Try editing the second user to have the email of the first user', async () => { + await adminUsersPage.navigateTo(); + await adminUsersPage.findUserPageWithEmail(user2.email); + let userRow = await adminUsersPage.getUserRowByEmail(user2.email); + await adminUsersPage.clickEditUser(userRow); + await adminEditUserPage.waitForLoad(user2.fullName); + await adminEditUserPage.emailInput.fill(user1.email); + const editPageUrl = page.url(); + await adminEditUserPage.updateButton.click(); - await test.step( - 'Try editing the second user to have the email of the first user', - async () => { - await adminUsersPage.navigateTo(); - await adminUsersPage.findUserPageWithEmail(user2.email); - let userRow = await adminUsersPage.getUserRowByEmail(user2.email); - await adminUsersPage.clickEditUser(userRow); - await adminEditUserPage.waitForLoad(user2.fullName); - await adminEditUserPage.emailInput.fill(user1.email); - const editPageUrl = page.url(); - await adminEditUserPage.updateButton.click(); - - const snackbar = await adminUsersPage.getSnackbarData( - 'snackbar-error' - ); - await expect(snackbar.variant).toBe('error'); - await adminUsersPage.closeSnackbar(); - await expect(page.url()).toBe(editPageUrl); - } - ); - } - ); + const snackbar = await adminUsersPage.getSnackbarData('snackbar-error'); + await expect(snackbar.variant).toBe('error'); + await adminUsersPage.closeSnackbar(); + await expect(page.url()).toBe(editPageUrl); + }); + }); }); diff --git a/packages/e2e-tests/tests/flow-editor/create-flow.spec.js b/packages/e2e-tests/tests/flow-editor/create-flow.spec.js index 98114c27..6f46454a 100644 --- a/packages/e2e-tests/tests/flow-editor/create-flow.spec.js +++ b/packages/e2e-tests/tests/flow-editor/create-flow.spec.js @@ -7,198 +7,191 @@ test('Ensure creating a new flow works', async ({ page }) => { ); }); -test( - 'Create a new flow with a Scheduler step then an Ntfy step', - async ({ flowEditorPage, page }) => { - await test.step('create flow', async () => { - await test.step('navigate to new flow page', async () => { - 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}/ - ); +test('Create a new flow with a Scheduler step then an Ntfy step', async ({ + flowEditorPage, + page, +}) => { + await test.step('create flow', async () => { + await test.step('navigate to new flow page', async () => { + 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 test.step('has two steps by default', async () => { + await expect(page.getByTestId('flow-step')).toHaveCount(2); + }); + }); + + await test.step('setup Scheduler trigger', async () => { + await test.step('choose app and event substep', async () => { + await test.step('choose application', async () => { + await flowEditorPage.appAutocomplete.click(); + await page.getByRole('option', { name: 'Scheduler' }).click(); }); - - await test.step('has two steps by default', async () => { - await expect(page.getByTestId('flow-step')).toHaveCount(2); + + await test.step('choose and event', async () => { + await expect(flowEditorPage.eventAutocomplete).toBeVisible(); + await flowEditorPage.eventAutocomplete.click(); + await page.getByRole('option', { name: 'Every hour' }).click(); + }); + + await test.step('continue to next step', async () => { + await flowEditorPage.continueButton.click(); + }); + + await test.step('collapses the substep', async () => { + await expect(flowEditorPage.appAutocomplete).not.toBeVisible(); + await expect(flowEditorPage.eventAutocomplete).not.toBeVisible(); }); }); - await test.step('setup Scheduler trigger', async () => { - await test.step('choose app and event substep', async () => { - await test.step('choose application', async () => { - await flowEditorPage.appAutocomplete.click(); - await page - .getByRole('option', { name: 'Scheduler' }) - .click(); - }); - - await test.step('choose and event', async () => { - await expect(flowEditorPage.eventAutocomplete).toBeVisible(); - await flowEditorPage.eventAutocomplete.click(); - await page - .getByRole('option', { name: 'Every hour' }) - .click(); - }); - - await test.step('continue to next step', async () => { - await flowEditorPage.continueButton.click(); - }); - - await test.step('collapses the substep', async () => { - await expect(flowEditorPage.appAutocomplete).not.toBeVisible(); - await expect(flowEditorPage.eventAutocomplete).not.toBeVisible(); - }); + await test.step('set up a trigger', async () => { + await test.step('choose "yes" in "trigger on weekends?"', async () => { + await expect(flowEditorPage.trigger).toBeVisible(); + await flowEditorPage.trigger.click(); + await page.getByRole('option', { name: 'Yes' }).click(); }); - await test.step('set up a trigger', async () => { - await test.step('choose "yes" in "trigger on weekends?"', async () => { - await expect(flowEditorPage.trigger).toBeVisible(); - await flowEditorPage.trigger.click(); - await page.getByRole('option', { name: 'Yes' }).click(); - }); - - await test.step('continue to next step', async () => { - await flowEditorPage.continueButton.click(); - }); - - await test.step('collapses the substep', async () => { - await expect(flowEditorPage.trigger).not.toBeVisible(); - }); + await test.step('continue to next step', async () => { + await flowEditorPage.continueButton.click(); }); - await test.step('test trigger', async () => { - await test.step('show sample output', async () => { - await expect(flowEditorPage.testOutput).not.toBeVisible(); - await flowEditorPage.continueButton.click(); - await expect(flowEditorPage.testOutput).toBeVisible(); - await flowEditorPage.screenshot({ - path: 'Scheduler trigger test output.png', - }); - await flowEditorPage.continueButton.click(); - }); + await test.step('collapses the substep', async () => { + await expect(flowEditorPage.trigger).not.toBeVisible(); }); }); - await test.step('arrange Ntfy action', async () => { - await test.step('choose app and event substep', async () => { - await test.step('choose application', async () => { - await flowEditorPage.appAutocomplete.click(); - await page.getByRole('option', { name: 'Ntfy' }).click(); - }); - - await test.step('choose an event', async () => { - await expect(flowEditorPage.eventAutocomplete).toBeVisible(); - await flowEditorPage.eventAutocomplete.click(); - await page - .getByRole('option', { name: 'Send message' }) - .click(); - }); - - await test.step('continue to next step', async () => { - await flowEditorPage.continueButton.click(); - }); - - await test.step('collapses the substep', async () => { - await expect(flowEditorPage.appAutocomplete).not.toBeVisible(); - await expect(flowEditorPage.eventAutocomplete).not.toBeVisible(); - }); - }); - - await test.step('choose connection substep', async () => { - await test.step('choose connection list item', async () => { - await flowEditorPage.connectionAutocomplete.click(); - await page.getByRole('option').first().click(); - }); - - await test.step('continue to next step', async () => { - await flowEditorPage.continueButton.click(); - }); - - await test.step('collapses the substep', async () => { - await expect(flowEditorPage.connectionAutocomplete).not.toBeVisible(); - }); - }); - - await test.step('set up action substep', async () => { - await test.step('fill topic and message body', async () => { - await page - .getByTestId('parameters.topic-power-input') - .locator('[contenteditable]') - .fill('Topic'); - await page - .getByTestId('parameters.message-power-input') - .locator('[contenteditable]') - .fill('Message body'); - }); - - await test.step('continue to next step', async () => { - await flowEditorPage.continueButton.click(); - }); - - await test.step('collapses the substep', async () => { - await expect(flowEditorPage.connectionAutocomplete).not.toBeVisible(); - }); - }); - - await test.step('test trigger substep', async () => { - await test.step('show sample output', async () => { - await expect(flowEditorPage.testOutput).not.toBeVisible(); - await page - .getByTestId('flow-substep-continue-button') - .first() - .click(); - await expect(flowEditorPage.testOutput).toBeVisible(); - await flowEditorPage.screenshot({ - path: 'Ntfy action test output.png', - }); - await flowEditorPage.continueButton.click(); - }); - }); - }); - - await test.step('publish and unpublish', async () => { - await test.step('publish flow', async () => { - await expect(flowEditorPage.unpublishFlowButton).not.toBeVisible(); - await expect(flowEditorPage.publishFlowButton).toBeVisible(); - await flowEditorPage.publishFlowButton.click(); - await expect(flowEditorPage.publishFlowButton).not.toBeVisible(); - }); - - await test.step('shows read-only sticky snackbar', async () => { - await expect(flowEditorPage.infoSnackbar).toBeVisible(); + await test.step('test trigger', async () => { + await test.step('show sample output', async () => { + await expect(flowEditorPage.testOutput).not.toBeVisible(); + await flowEditorPage.continueButton.click(); + await expect(flowEditorPage.testOutput).toBeVisible(); await flowEditorPage.screenshot({ - path: 'Published flow.png', + path: 'Scheduler trigger test output.png', }); + await flowEditorPage.continueButton.click(); }); - - await test.step('unpublish from snackbar', async () => { + }); + }); + + await test.step('arrange Ntfy action', async () => { + await test.step('choose app and event substep', async () => { + await test.step('choose application', async () => { + await flowEditorPage.appAutocomplete.click(); + await page.getByRole('option', { name: 'Ntfy' }).click(); + }); + + await test.step('choose an event', async () => { + await expect(flowEditorPage.eventAutocomplete).toBeVisible(); + await flowEditorPage.eventAutocomplete.click(); + await page.getByRole('option', { name: 'Send message' }).click(); + }); + + await test.step('continue to next step', async () => { + await flowEditorPage.continueButton.click(); + }); + + await test.step('collapses the substep', async () => { + await expect(flowEditorPage.appAutocomplete).not.toBeVisible(); + await expect(flowEditorPage.eventAutocomplete).not.toBeVisible(); + }); + }); + + await test.step('choose connection substep', async () => { + await test.step('choose connection list item', async () => { + await flowEditorPage.connectionAutocomplete.click(); await page - .getByTestId('unpublish-flow-from-snackbar') + .getByRole('option') + .filter({ hasText: 'Add new connection' }) .click(); - await expect(flowEditorPage.infoSnackbar).not.toBeVisible(); }); - - await test.step('publish once again', async () => { - await expect(flowEditorPage.publishFlowButton).toBeVisible(); - await flowEditorPage.publishFlowButton.click(); - await expect(flowEditorPage.publishFlowButton).not.toBeVisible(); + + await test.step('continue to next step', async () => { + await page.getByTestId('create-connection-button').click(); }); - - await test.step('unpublish from layout top bar', async () => { - await expect(flowEditorPage.unpublishFlowButton).toBeVisible(); - await flowEditorPage.unpublishFlowButton.click(); - await expect(flowEditorPage.unpublishFlowButton).not.toBeVisible(); + + await test.step('collapses the substep', async () => { + await flowEditorPage.continueButton.click(); + await expect(flowEditorPage.connectionAutocomplete).not.toBeVisible(); + }); + }); + + await test.step('set up action substep', async () => { + await test.step('fill topic and message body', async () => { + await page + .getByTestId('parameters.topic-power-input') + .locator('[contenteditable]') + .fill('Topic'); + await page + .getByTestId('parameters.message-power-input') + .locator('[contenteditable]') + .fill('Message body'); + }); + + await test.step('continue to next step', async () => { + await flowEditorPage.continueButton.click(); + }); + + await test.step('collapses the substep', async () => { + await expect(flowEditorPage.connectionAutocomplete).not.toBeVisible(); + }); + }); + + await test.step('test trigger substep', async () => { + await test.step('show sample output', async () => { + await expect(flowEditorPage.testOutput).not.toBeVisible(); + await page.getByTestId('flow-substep-continue-button').first().click(); + await expect(flowEditorPage.testOutput).toBeVisible(); await flowEditorPage.screenshot({ - path: 'Unpublished flow.png', + path: 'Ntfy action test output.png', }); + await flowEditorPage.continueButton.click(); }); }); - - await test.step('in layout', async () => { - await test.step('can go back to flows page', async () => { - await page.getByTestId('editor-go-back-button').click(); - await expect(page).toHaveURL('/flows'); + }); + + await test.step('publish and unpublish', async () => { + await test.step('publish flow', async () => { + await expect(flowEditorPage.unpublishFlowButton).not.toBeVisible(); + await expect(flowEditorPage.publishFlowButton).toBeVisible(); + await flowEditorPage.publishFlowButton.click(); + await expect(flowEditorPage.publishFlowButton).not.toBeVisible(); + }); + + await test.step('shows read-only sticky snackbar', async () => { + await expect(flowEditorPage.infoSnackbar).toBeVisible(); + await flowEditorPage.screenshot({ + path: 'Published flow.png', }); }); - } -); \ No newline at end of file + + await test.step('unpublish from snackbar', async () => { + await page.getByTestId('unpublish-flow-from-snackbar').click(); + await expect(flowEditorPage.infoSnackbar).not.toBeVisible(); + }); + + await test.step('publish once again', async () => { + await expect(flowEditorPage.publishFlowButton).toBeVisible(); + await flowEditorPage.publishFlowButton.click(); + await expect(flowEditorPage.publishFlowButton).not.toBeVisible(); + }); + + await test.step('unpublish from layout top bar', async () => { + await expect(flowEditorPage.unpublishFlowButton).toBeVisible(); + await flowEditorPage.unpublishFlowButton.click(); + await expect(flowEditorPage.unpublishFlowButton).not.toBeVisible(); + await flowEditorPage.screenshot({ + path: 'Unpublished flow.png', + }); + }); + }); + + await test.step('in layout', async () => { + await test.step('can go back to flows page', async () => { + await page.getByTestId('editor-go-back-button').click(); + await expect(page).toHaveURL('/flows'); + }); + }); +}); diff --git a/packages/e2e-tests/tests/my-profile/profile-updates.spec.js b/packages/e2e-tests/tests/my-profile/profile-updates.spec.js index d77962e4..fc0ce7d0 100644 --- a/packages/e2e-tests/tests/my-profile/profile-updates.spec.js +++ b/packages/e2e-tests/tests/my-profile/profile-updates.spec.js @@ -33,10 +33,7 @@ publicTest.describe('My Profile', () => { .getByRole('option', { name: 'Admin' }) .click(); await adminCreateUserPage.createButton.click(); - const snackbar = await adminUsersPage.getSnackbarData( - 'snackbar-create-user-success' - ); - await expect(snackbar.variant).toBe('success'); + await adminCreateUserPage.expectCreateUserSuccessAlertToBeVisible(); }); await publicTest.step('copy invitation link', async () => { From 9ae347d130b7bd9d559e33ff9cc99438c951c2c1 Mon Sep 17 00:00:00 2001 From: "kasia.oczkowska" Date: Fri, 13 Dec 2024 11:23:40 +0000 Subject: [PATCH 31/45] fix: remove unnecessary function argument --- packages/web/src/pages/CreateRole/index.ee.jsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/web/src/pages/CreateRole/index.ee.jsx b/packages/web/src/pages/CreateRole/index.ee.jsx index c2eb7d1a..3bf6479e 100644 --- a/packages/web/src/pages/CreateRole/index.ee.jsx +++ b/packages/web/src/pages/CreateRole/index.ee.jsx @@ -151,9 +151,7 @@ export default function CreateRole() { onSubmit={handleRoleCreation} defaultValues={defaultValues} noValidate - resolver={yupResolver( - getValidationSchema(formatMessage, defaultValues), - )} + resolver={yupResolver(getValidationSchema(formatMessage))} automaticValidation={false} render={({ formState: { errors } }) => ( From ae74b899bd5dcd099b30b6ed340d7beabb5cfe1f Mon Sep 17 00:00:00 2001 From: "kasia.oczkowska" Date: Thu, 19 Dec 2024 13:53:59 +0000 Subject: [PATCH 32/45] refactor: use form's centralized error management --- packages/web/src/helpers/errors.js | 18 -------- packages/web/src/locales/en.json | 2 - .../web/src/pages/CreateRole/index.ee.jsx | 44 ++++--------------- packages/web/src/pages/CreateUser/index.jsx | 28 +----------- 4 files changed, 11 insertions(+), 81 deletions(-) delete mode 100644 packages/web/src/helpers/errors.js diff --git a/packages/web/src/helpers/errors.js b/packages/web/src/helpers/errors.js deleted file mode 100644 index dc73867d..00000000 --- a/packages/web/src/helpers/errors.js +++ /dev/null @@ -1,18 +0,0 @@ -// Helpers to extract errors received from the API - -export const getGeneralErrorMessage = ({ error, fallbackMessage }) => { - if (!error) { - return; - } - - const errors = error?.response?.data?.errors; - const generalError = errors?.general; - - if (generalError && Array.isArray(generalError)) { - return generalError.join(' '); - } - - if (!errors) { - return error?.message || fallbackMessage; - } -}; diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index bb95e979..a5fa024a 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -231,7 +231,6 @@ "createUser.submit": "Create", "createUser.successfullyCreated": "The user has been created.", "createUser.invitationEmailInfo": "Invitation email will be sent if SMTP credentials are valid. Otherwise, you can share the invitation link manually: ", - "createUser.error": "Error while creating the user.", "editUserPage.title": "Edit user", "editUser.status": "Status", "editUser.submit": "Update", @@ -255,7 +254,6 @@ "roleForm.mandatoryInput": "{inputName} is required.", "createRole.submit": "Create", "createRole.successfullyCreated": "The role has been created.", - "createRole.generalError": "Error while creating the role.", "createRole.permissionsError": "Permissions are invalid.", "editRole.submit": "Update", "editRole.successfullyUpdated": "The role has been updated.", diff --git a/packages/web/src/pages/CreateRole/index.ee.jsx b/packages/web/src/pages/CreateRole/index.ee.jsx index 3bf6479e..88ac1d82 100644 --- a/packages/web/src/pages/CreateRole/index.ee.jsx +++ b/packages/web/src/pages/CreateRole/index.ee.jsx @@ -19,7 +19,6 @@ import { getComputedPermissionsDefaultValues, getPermissions, } from 'helpers/computePermissions.ee'; -import { getGeneralErrorMessage } from 'helpers/errors'; import useFormatMessage from 'hooks/useFormatMessage'; import useAdminCreateRole from 'hooks/useAdminCreateRole'; import usePermissionCatalog from 'hooks/usePermissionCatalog.ee'; @@ -66,6 +65,7 @@ export default function CreateRole() { useAdminCreateRole(); const { data: permissionCatalogData, isLoading: isPermissionCatalogLoading } = usePermissionCatalog(); + const [permissionError, setPermissionError] = React.useState(null); const defaultValues = React.useMemo( () => ({ @@ -81,8 +81,9 @@ export default function CreateRole() { [permissionCatalogData], ); - const handleRoleCreation = async (roleData, e, setError) => { + const handleRoleCreation = async (roleData) => { try { + setPermissionError(null); const permissions = getPermissions(roleData.computedPermissions); await createRole({ @@ -100,40 +101,13 @@ export default function CreateRole() { navigate(URLS.ROLES); } catch (error) { - const errors = error?.response?.data?.errors; - - if (errors) { - const fieldNames = ['name', 'description']; - Object.entries(errors).forEach(([fieldName, fieldErrors]) => { - if (fieldNames.includes(fieldName) && Array.isArray(fieldErrors)) { - setError(fieldName, { - type: 'fieldRequestError', - message: fieldErrors.join(', '), - }); - } - }); - } - const permissionError = getPermissionsErrorMessage(error); - if (permissionError) { - setError('root.permissions', { - type: 'fieldRequestError', - message: permissionError, - }); + setPermissionError(permissionError); } - const generalError = getGeneralErrorMessage({ - error, - fallbackMessage: formatMessage('createRole.generalError'), - }); - - if (generalError) { - setError('root.general', { - type: 'requestError', - message: generalError, - }); - } + const errors = error?.response?.data?.errors; + throw errors || error; } }; @@ -178,18 +152,18 @@ export default function CreateRole() { - {errors?.root?.permissions && ( + {permissionError && ( {formatMessage('createRole.permissionsError')}
-                      {errors?.root?.permissions?.message}
+                      {permissionError}
                     
)} - {errors?.root?.general && ( + {errors?.root?.general && !permissionError && ( {errors?.root?.general?.message} diff --git a/packages/web/src/pages/CreateUser/index.jsx b/packages/web/src/pages/CreateUser/index.jsx index 689791e5..5eb04376 100644 --- a/packages/web/src/pages/CreateUser/index.jsx +++ b/packages/web/src/pages/CreateUser/index.jsx @@ -18,7 +18,6 @@ import useFormatMessage from 'hooks/useFormatMessage'; import useRoles from 'hooks/useRoles.ee'; import useAdminCreateUser from 'hooks/useAdminCreateUser'; import useCurrentUserAbility from 'hooks/useCurrentUserAbility'; -import { getGeneralErrorMessage } from 'helpers/errors'; function generateRoleOptions(roles) { return roles?.map(({ name: label, id: value }) => ({ label, value })); @@ -70,7 +69,7 @@ export default function CreateUser() { const currentUserAbility = useCurrentUserAbility(); const canUpdateRole = currentUserAbility.can('update', 'Role'); - const handleUserCreation = async (userData, e, setError) => { + const handleUserCreation = async (userData) => { try { await createUser({ fullName: userData.fullName, @@ -81,30 +80,7 @@ export default function CreateUser() { queryClient.invalidateQueries({ queryKey: ['admin', 'users'] }); } catch (error) { const errors = error?.response?.data?.errors; - - if (errors) { - const fieldNames = Object.keys(defaultValues); - Object.entries(errors).forEach(([fieldName, fieldErrors]) => { - if (fieldNames.includes(fieldName) && Array.isArray(fieldErrors)) { - setError(fieldName, { - type: 'fieldRequestError', - message: fieldErrors.join(', '), - }); - } - }); - } - - const generalError = getGeneralErrorMessage({ - error, - fallbackMessage: formatMessage('createUser.error'), - }); - - if (generalError) { - setError('root.general', { - type: 'requestError', - message: generalError, - }); - } + throw errors || error; } }; From 169c86a7480777eda26b76f82665f93fa2962e62 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Wed, 8 Jan 2025 11:43:37 +0300 Subject: [PATCH 33/45] feat: Implement initial logic of exporting flow --- packages/backend/src/helpers/export-flow.js | 46 +++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 packages/backend/src/helpers/export-flow.js diff --git a/packages/backend/src/helpers/export-flow.js b/packages/backend/src/helpers/export-flow.js new file mode 100644 index 00000000..9c4f6986 --- /dev/null +++ b/packages/backend/src/helpers/export-flow.js @@ -0,0 +1,46 @@ +import Crypto from 'crypto'; + +const exportFlow = async (flow) => { + const steps = await flow.$relatedQuery('steps'); + + const newFlowId = Crypto.randomUUID(); + const stepIdMap = Object.fromEntries( + steps.map((step) => [step.id, Crypto.randomUUID()]) + ); + + const exportedFlow = { + id: newFlowId, + name: flow.name, + steps: steps.map((step) => ({ + id: stepIdMap[step.id], + key: step.key, + name: step.name, + appKey: step.appKey, + type: step.type, + parameters: updateParameters(step.parameters, stepIdMap), + position: step.position, + webhookPath: step.webhookPath?.replace(flow.id, newFlowId), + })), + }; + + console.log(JSON.stringify(exportedFlow, null, 2)); + return exportedFlow; +}; + +const updateParameters = (parameters, stepIdMap) => { + if (!parameters) return parameters; + + const stringifiedParameters = JSON.stringify(parameters); + let updatedParameters = stringifiedParameters; + + Object.entries(stepIdMap).forEach(([oldStepId, newStepId]) => { + updatedParameters = updatedParameters.replace( + `{{step.${oldStepId}.`, + `{{step.${newStepId}.` + ); + }); + + return JSON.parse(updatedParameters); +}; + +export default exportFlow; From c180b98460636d42f56c0912f6be8219ab52e4ac Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Fri, 10 Jan 2025 17:21:43 +0300 Subject: [PATCH 34/45] feat: Complete export flow rest API endpoint --- packages/backend/package.json | 1 + .../controllers/api/v1/flows/export-flow.js | 9 + .../api/v1/flows/export-flow.test.js | 217 ++++++++++++++++++ packages/backend/src/helpers/authorization.js | 4 + packages/backend/src/helpers/export-flow.js | 1 - packages/backend/src/models/flow.js | 20 ++ packages/backend/src/models/flow.test.js | 64 ++++++ packages/backend/src/routes/api/v1/flows.js | 8 + .../mocks/rest/api/v1/flows/export-flow.js | 32 +++ packages/backend/yarn.lock | 25 +- 10 files changed, 362 insertions(+), 19 deletions(-) create mode 100644 packages/backend/src/controllers/api/v1/flows/export-flow.js create mode 100644 packages/backend/src/controllers/api/v1/flows/export-flow.test.js create mode 100644 packages/backend/test/mocks/rest/api/v1/flows/export-flow.js diff --git a/packages/backend/package.json b/packages/backend/package.json index 2686d597..e16585a7 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -69,6 +69,7 @@ "prettier": "^2.5.1", "raw-body": "^2.5.2", "showdown": "^2.1.0", + "slugify": "^1.6.6", "uuid": "^9.0.1", "winston": "^3.7.1", "xmlrpc": "^1.3.2" diff --git a/packages/backend/src/controllers/api/v1/flows/export-flow.js b/packages/backend/src/controllers/api/v1/flows/export-flow.js new file mode 100644 index 00000000..d3446dd9 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/flows/export-flow.js @@ -0,0 +1,9 @@ +export default async (request, response) => { + const flow = await request.currentUser.authorizedFlows + .findById(request.params.flowId) + .throwIfNotFound(); + + const { exportedFlowAsString, slug } = await flow.export(); + + response.status(201).attachment(slug).send(exportedFlowAsString); +}; diff --git a/packages/backend/src/controllers/api/v1/flows/export-flow.test.js b/packages/backend/src/controllers/api/v1/flows/export-flow.test.js new file mode 100644 index 00000000..1f72ade9 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/flows/export-flow.test.js @@ -0,0 +1,217 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +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 { createFlow } from '../../../../../test/factories/flow.js'; +import { createStep } from '../../../../../test/factories/step.js'; +import { createPermission } from '../../../../../test/factories/permission.js'; +import exportFlowMock from '../../../../../test/mocks/rest/api/v1/flows/export-flow.js'; + +describe('POST /api/v1/flows/:flowId/export', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should export the flow data of the current user', async () => { + const currentUserFlow = await createFlow({ userId: currentUser.id }); + + const triggerStep = await createStep({ + flowId: currentUserFlow.id, + type: 'trigger', + appKey: 'webhook', + key: 'catchRawWebhook', + name: 'Catch raw webhook', + parameters: { + workSynchronously: true, + }, + position: 1, + webhookPath: `/webhooks/flows/${currentUserFlow.id}/sync`, + }); + + const actionStep = await createStep({ + flowId: currentUserFlow.id, + type: 'action', + appKey: 'formatter', + key: 'text', + name: 'Text', + parameters: { + input: `hello {{step.${triggerStep.id}.query.sample}} deneme`, + transform: 'capitalize', + }, + position: 2, + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .post(`/api/v1/flows/${currentUserFlow.id}/export`) + .set('Authorization', token) + .expect(201); + + // Test headers for file attachment + expect(response.headers['content-disposition']).toContain( + 'attachment; filename="name-your-flow.json"' + ); + expect(response.headers['content-type']).toBe( + 'application/json; charset=utf-8' + ); + + const expectedFileStructure = await exportFlowMock(currentUserFlow, [ + triggerStep, + actionStep, + ]); + + expect(response.body).toStrictEqual(expectedFileStructure); + }); + + it('should export 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, + type: 'trigger', + appKey: 'webhook', + key: 'catchRawWebhook', + name: 'Catch raw webhook', + parameters: { + workSynchronously: true, + }, + position: 1, + webhookPath: `/webhooks/flows/${anotherUserFlow.id}/sync`, + }); + + const actionStep = await createStep({ + flowId: anotherUserFlow.id, + type: 'action', + appKey: 'formatter', + key: 'text', + name: 'Text', + parameters: { + input: `hello {{step.${triggerStep.id}.query.sample}} deneme`, + transform: 'capitalize', + }, + position: 2, + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .post(`/api/v1/flows/${anotherUserFlow.id}/export`) + .set('Authorization', token) + .expect(201); + + expect(response.headers['content-disposition']).toStrictEqual( + 'attachment; filename="name-your-flow.json"' + ); + expect(response.headers['content-type']).toStrictEqual( + 'application/json; charset=utf-8' + ); + + const expectedFileStructure = await exportFlowMock(anotherUserFlow, [ + triggerStep, + actionStep, + ]); + + expect(response.body).toStrictEqual(expectedFileStructure); + }); + + it('should return not found response for not existing flow UUID', async () => { + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const notExistingFlowUUID = Crypto.randomUUID(); + + await request(app) + .post(`/api/v1/flows/${notExistingFlowUUID}/export`) + .set('Authorization', token) + .expect(404); + }); + + it('should return not found response for unauthorized flow', async () => { + const anotherUser = await createUser(); + const anotherUserFlow = await createFlow({ userId: anotherUser.id }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await request(app) + .post(`/api/v1/flows/${anotherUserFlow.id}/export`) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await request(app) + .post('/api/v1/flows/invalidFlowUUID/export') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/helpers/authorization.js b/packages/backend/src/helpers/authorization.js index c9f6329f..13718283 100644 --- a/packages/backend/src/helpers/authorization.js +++ b/packages/backend/src/helpers/authorization.js @@ -113,6 +113,10 @@ const authorizationList = { action: 'create', subject: 'Flow', }, + 'POST /api/v1/flows/:flowId/export': { + action: 'update', + subject: 'Flow', + }, 'POST /api/v1/flows/:flowId/steps': { action: 'update', subject: 'Flow', diff --git a/packages/backend/src/helpers/export-flow.js b/packages/backend/src/helpers/export-flow.js index 9c4f6986..05b238dc 100644 --- a/packages/backend/src/helpers/export-flow.js +++ b/packages/backend/src/helpers/export-flow.js @@ -23,7 +23,6 @@ const exportFlow = async (flow) => { })), }; - console.log(JSON.stringify(exportedFlow, null, 2)); return exportedFlow; }; diff --git a/packages/backend/src/models/flow.js b/packages/backend/src/models/flow.js index 56744396..e9d32811 100644 --- a/packages/backend/src/models/flow.js +++ b/packages/backend/src/models/flow.js @@ -1,4 +1,5 @@ import { ValidationError } from 'objection'; +import slugify from 'slugify'; import Base from './base.js'; import Step from './step.js'; import User from './user.js'; @@ -7,6 +8,7 @@ import ExecutionStep from './execution-step.js'; import globalVariable from '../helpers/global-variable.js'; import logger from '../helpers/logger.js'; import Telemetry from '../helpers/telemetry/index.js'; +import exportFlow from '../helpers/export-flow.js'; import flowQueue from '../queues/flow.js'; import { REMOVE_AFTER_30_DAYS_OR_150_JOBS, @@ -426,6 +428,24 @@ class Flow extends Base { } } + slugifyNameAsFilename() { + const slug = slugify(this.name, { + lower: true, + strict: true, + replacement: '-', + }); + + return `${slug}.json`; + } + + async export() { + const exportedFlow = await exportFlow(this); + const exportedFlowAsString = JSON.stringify(exportedFlow, null, 2); + const slug = this.slugifyNameAsFilename(); + + return { exportedFlowAsString, slug }; + } + async $beforeUpdate(opt, queryContext) { await super.$beforeUpdate(opt, queryContext); diff --git a/packages/backend/src/models/flow.test.js b/packages/backend/src/models/flow.test.js index 7faefa17..0329d6b6 100644 --- a/packages/backend/src/models/flow.test.js +++ b/packages/backend/src/models/flow.test.js @@ -10,6 +10,7 @@ import { createFlow } from '../../test/factories/flow.js'; import { createStep } from '../../test/factories/step.js'; import { createExecution } from '../../test/factories/execution.js'; import { createExecutionStep } from '../../test/factories/execution-step.js'; +import * as exportFlow from '../helpers/export-flow.js'; describe('Flow model', () => { it('tableName should return correct name', () => { @@ -506,6 +507,69 @@ describe('Flow model', () => { }); }); + describe('slugifyNameAsFilename', () => { + it('should generate a slug file name from flow name', async () => { + const flow = await createFlow({ + name: 'My Flow Name', + }); + + const slug = flow.slugifyNameAsFilename(); + expect(slug).toBe('my-flow-name.json'); + }); + }); + + describe('export', () => { + it('should call slugifyNameAsFilename method', async () => { + const flow = await createFlow({ + name: 'My Flow Name', + }); + + const slugifyNameAsFilenameSpy = vi + .spyOn(flow, 'slugifyNameAsFilename') + .mockImplementation(() => 'my-flow-name.json'); + + await flow.export(); + + expect(slugifyNameAsFilenameSpy).toHaveBeenCalledOnce(); + }); + + it('should call exportFlow method', async () => { + const flow = await createFlow(); + + const exportFlowSpy = vi + .spyOn(exportFlow, 'default') + .mockImplementation(() => {}); + + await flow.export(); + + expect(exportFlowSpy).toHaveBeenCalledOnce(); + }); + + it('should return exportedFlowAsString and slug', async () => { + const flow = await createFlow(); + + const exportedFlowAsString = { + name: 'My Flow Name', + }; + + const slug = 'slug'; + + vi.spyOn(exportFlow, 'default').mockReturnValue(exportedFlowAsString); + vi.spyOn(flow, 'slugifyNameAsFilename').mockReturnValue(slug); + + const expectedExportedFlowAsString = JSON.stringify( + exportedFlowAsString, + null, + 2 + ); + + expect(await flow.export()).toStrictEqual({ + exportedFlowAsString: expectedExportedFlowAsString, + slug: 'slug', + }); + }); + }); + describe('throwIfHavingLessThanTwoSteps', () => { it('should throw validation error with less than two steps', async () => { const flow = await createFlow(); diff --git a/packages/backend/src/routes/api/v1/flows.js b/packages/backend/src/routes/api/v1/flows.js index 8b507b82..10b19e74 100644 --- a/packages/backend/src/routes/api/v1/flows.js +++ b/packages/backend/src/routes/api/v1/flows.js @@ -9,6 +9,7 @@ import createFlowAction from '../../../controllers/api/v1/flows/create-flow.js'; import createStepAction from '../../../controllers/api/v1/flows/create-step.js'; import deleteFlowAction from '../../../controllers/api/v1/flows/delete-flow.js'; import duplicateFlowAction from '../../../controllers/api/v1/flows/duplicate-flow.js'; +import exportFlowAction from '../../../controllers/api/v1/flows/export-flow.js'; const router = Router(); @@ -17,6 +18,13 @@ router.get('/:flowId', authenticateUser, authorizeUser, getFlowAction); router.post('/', authenticateUser, authorizeUser, createFlowAction); router.patch('/:flowId', authenticateUser, authorizeUser, updateFlowAction); +router.post( + '/:flowId/export', + authenticateUser, + authorizeUser, + exportFlowAction +); + router.patch( '/:flowId/status', authenticateUser, diff --git a/packages/backend/test/mocks/rest/api/v1/flows/export-flow.js b/packages/backend/test/mocks/rest/api/v1/flows/export-flow.js new file mode 100644 index 00000000..e235a6c7 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/flows/export-flow.js @@ -0,0 +1,32 @@ +import { expect } from 'vitest'; + +const duplicateFlowMock = async (flow, steps = []) => { + const data = { + id: expect.any(String), + name: flow.name, + }; + + if (steps.length) { + data.steps = steps.map((step) => { + const computedStep = { + id: expect.any(String), + key: step.key, + name: step.name, + appKey: step.appKey, + type: step.type, + parameters: expect.any(Object), + position: step.position, + }; + + if (step.type === 'trigger') { + computedStep.webhookPath = expect.stringContaining('/webhooks/flows/'); + } + + return computedStep; + }); + } + + return data; +}; + +export default duplicateFlowMock; diff --git a/packages/backend/yarn.lock b/packages/backend/yarn.lock index bf899f12..184e2e40 100644 --- a/packages/backend/yarn.lock +++ b/packages/backend/yarn.lock @@ -4177,6 +4177,11 @@ simple-update-notifier@^1.0.7: dependencies: semver "~7.0.0" +slugify@^1.6.6: + version "1.6.6" + resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.6.tgz#2d4ac0eacb47add6af9e04d3be79319cbcc7924b" + integrity sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw== + smart-buffer@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" @@ -4261,16 +4266,7 @@ 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": - 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: +"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: 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== @@ -4302,14 +4298,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"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: +"strip-ansi-cjs@npm:strip-ansi@^6.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== From 51291889cdae7e0f463deebbba6a0d4685771d83 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Mon, 13 Jan 2025 12:13:46 +0100 Subject: [PATCH 35/45] chore: Remove slugify library --- packages/backend/package.json | 1 - packages/backend/yarn.lock | 5 ----- 2 files changed, 6 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index e16585a7..2686d597 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -69,7 +69,6 @@ "prettier": "^2.5.1", "raw-body": "^2.5.2", "showdown": "^2.1.0", - "slugify": "^1.6.6", "uuid": "^9.0.1", "winston": "^3.7.1", "xmlrpc": "^1.3.2" diff --git a/packages/backend/yarn.lock b/packages/backend/yarn.lock index 184e2e40..718eff58 100644 --- a/packages/backend/yarn.lock +++ b/packages/backend/yarn.lock @@ -4177,11 +4177,6 @@ simple-update-notifier@^1.0.7: dependencies: semver "~7.0.0" -slugify@^1.6.6: - version "1.6.6" - resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.6.tgz#2d4ac0eacb47add6af9e04d3be79319cbcc7924b" - integrity sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw== - smart-buffer@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" From f15d1ac7b15a9941e2b79ed56418b740777fa16b Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Mon, 13 Jan 2025 12:14:16 +0100 Subject: [PATCH 36/45] refactor: Only return JSON for flow export --- .../controllers/api/v1/flows/export-flow.js | 6 ++- .../api/v1/flows/export-flow.test.js | 23 ++------- packages/backend/src/models/flow.js | 17 +------ packages/backend/src/models/flow.test.js | 51 +------------------ 4 files changed, 11 insertions(+), 86 deletions(-) diff --git a/packages/backend/src/controllers/api/v1/flows/export-flow.js b/packages/backend/src/controllers/api/v1/flows/export-flow.js index d3446dd9..5a1faac9 100644 --- a/packages/backend/src/controllers/api/v1/flows/export-flow.js +++ b/packages/backend/src/controllers/api/v1/flows/export-flow.js @@ -1,9 +1,11 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + export default async (request, response) => { const flow = await request.currentUser.authorizedFlows .findById(request.params.flowId) .throwIfNotFound(); - const { exportedFlowAsString, slug } = await flow.export(); + const exportedFlow = await flow.export(); - response.status(201).attachment(slug).send(exportedFlowAsString); + return renderObject(response, exportedFlow, { status: 201 }); }; diff --git a/packages/backend/src/controllers/api/v1/flows/export-flow.test.js b/packages/backend/src/controllers/api/v1/flows/export-flow.test.js index 1f72ade9..1c648e64 100644 --- a/packages/backend/src/controllers/api/v1/flows/export-flow.test.js +++ b/packages/backend/src/controllers/api/v1/flows/export-flow.test.js @@ -67,20 +67,12 @@ describe('POST /api/v1/flows/:flowId/export', () => { .set('Authorization', token) .expect(201); - // Test headers for file attachment - expect(response.headers['content-disposition']).toContain( - 'attachment; filename="name-your-flow.json"' - ); - expect(response.headers['content-type']).toBe( - 'application/json; charset=utf-8' - ); - - const expectedFileStructure = await exportFlowMock(currentUserFlow, [ + const expectedPayload = await exportFlowMock(currentUserFlow, [ triggerStep, actionStep, ]); - expect(response.body).toStrictEqual(expectedFileStructure); + expect(response.body).toStrictEqual(expectedPayload); }); it('should export the flow data of another user', async () => { @@ -132,19 +124,12 @@ describe('POST /api/v1/flows/:flowId/export', () => { .set('Authorization', token) .expect(201); - expect(response.headers['content-disposition']).toStrictEqual( - 'attachment; filename="name-your-flow.json"' - ); - expect(response.headers['content-type']).toStrictEqual( - 'application/json; charset=utf-8' - ); - - const expectedFileStructure = await exportFlowMock(anotherUserFlow, [ + const expectedPayload = await exportFlowMock(anotherUserFlow, [ triggerStep, actionStep, ]); - expect(response.body).toStrictEqual(expectedFileStructure); + expect(response.body).toStrictEqual(expectedPayload); }); it('should return not found response for not existing flow UUID', async () => { diff --git a/packages/backend/src/models/flow.js b/packages/backend/src/models/flow.js index e9d32811..22be9030 100644 --- a/packages/backend/src/models/flow.js +++ b/packages/backend/src/models/flow.js @@ -1,5 +1,4 @@ import { ValidationError } from 'objection'; -import slugify from 'slugify'; import Base from './base.js'; import Step from './step.js'; import User from './user.js'; @@ -428,22 +427,8 @@ class Flow extends Base { } } - slugifyNameAsFilename() { - const slug = slugify(this.name, { - lower: true, - strict: true, - replacement: '-', - }); - - return `${slug}.json`; - } - async export() { - const exportedFlow = await exportFlow(this); - const exportedFlowAsString = JSON.stringify(exportedFlow, null, 2); - const slug = this.slugifyNameAsFilename(); - - return { exportedFlowAsString, slug }; + return await exportFlow(this); } async $beforeUpdate(opt, queryContext) { diff --git a/packages/backend/src/models/flow.test.js b/packages/backend/src/models/flow.test.js index 0329d6b6..cbaae474 100644 --- a/packages/backend/src/models/flow.test.js +++ b/packages/backend/src/models/flow.test.js @@ -507,65 +507,18 @@ describe('Flow model', () => { }); }); - describe('slugifyNameAsFilename', () => { - it('should generate a slug file name from flow name', async () => { - const flow = await createFlow({ - name: 'My Flow Name', - }); - - const slug = flow.slugifyNameAsFilename(); - expect(slug).toBe('my-flow-name.json'); - }); - }); - describe('export', () => { - it('should call slugifyNameAsFilename method', async () => { - const flow = await createFlow({ - name: 'My Flow Name', - }); - - const slugifyNameAsFilenameSpy = vi - .spyOn(flow, 'slugifyNameAsFilename') - .mockImplementation(() => 'my-flow-name.json'); - - await flow.export(); - - expect(slugifyNameAsFilenameSpy).toHaveBeenCalledOnce(); - }); - - it('should call exportFlow method', async () => { - const flow = await createFlow(); - - const exportFlowSpy = vi - .spyOn(exportFlow, 'default') - .mockImplementation(() => {}); - - await flow.export(); - - expect(exportFlowSpy).toHaveBeenCalledOnce(); - }); - - it('should return exportedFlowAsString and slug', async () => { + it('should return exportedFlow', async () => { const flow = await createFlow(); const exportedFlowAsString = { name: 'My Flow Name', }; - const slug = 'slug'; - vi.spyOn(exportFlow, 'default').mockReturnValue(exportedFlowAsString); - vi.spyOn(flow, 'slugifyNameAsFilename').mockReturnValue(slug); - - const expectedExportedFlowAsString = JSON.stringify( - exportedFlowAsString, - null, - 2 - ); expect(await flow.export()).toStrictEqual({ - exportedFlowAsString: expectedExportedFlowAsString, - slug: 'slug', + name: 'My Flow Name', }); }); }); From 7d621c07f11bd7b4e4862e18b368b2f6835546cc Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Mon, 13 Jan 2025 12:14:48 +0100 Subject: [PATCH 37/45] fix: Rename duplicate flow mock as export flow mock --- .../test/mocks/rest/api/v1/flows/export-flow.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/backend/test/mocks/rest/api/v1/flows/export-flow.js b/packages/backend/test/mocks/rest/api/v1/flows/export-flow.js index e235a6c7..c7a1ef6e 100644 --- a/packages/backend/test/mocks/rest/api/v1/flows/export-flow.js +++ b/packages/backend/test/mocks/rest/api/v1/flows/export-flow.js @@ -1,6 +1,6 @@ import { expect } from 'vitest'; -const duplicateFlowMock = async (flow, steps = []) => { +const exportFlowMock = async (flow, steps = []) => { const data = { id: expect.any(String), name: flow.name, @@ -26,7 +26,16 @@ const duplicateFlowMock = async (flow, steps = []) => { }); } - return data; + return { + data: data, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Object', + }, + }; }; -export default duplicateFlowMock; +export default exportFlowMock; From ec148012619f45646605a67a372b40bc6ac4a453 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Mon, 13 Jan 2025 12:30:15 +0000 Subject: [PATCH 38/45] feat(web): add exporting flow functionality --- packages/web/package.json | 1 + .../web/src/components/EditorLayout/index.jsx | 50 ++++++++++++++++--- .../src/components/FlowContextMenu/index.jsx | 41 +++++++++++++-- packages/web/src/hooks/useDeleteFlow.js | 4 +- .../web/src/hooks/useDownloadJsonAsFile.js | 31 ++++++++++++ packages/web/src/hooks/useExportFlow.js | 15 ++++++ packages/web/src/locales/en.json | 4 ++ packages/web/yarn.lock | 5 ++ 8 files changed, 138 insertions(+), 13 deletions(-) create mode 100644 packages/web/src/hooks/useDownloadJsonAsFile.js create mode 100644 packages/web/src/hooks/useExportFlow.js diff --git a/packages/web/package.json b/packages/web/package.json index cf1eb72c..b8ab799b 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -37,6 +37,7 @@ "slate": "^0.94.1", "slate-history": "^0.93.0", "slate-react": "^0.94.2", + "slugify": "^1.6.6", "uuid": "^9.0.0", "web-vitals": "^1.0.1", "yup": "^0.32.11" diff --git a/packages/web/src/components/EditorLayout/index.jsx b/packages/web/src/components/EditorLayout/index.jsx index c8878b5c..0d72f0bc 100644 --- a/packages/web/src/components/EditorLayout/index.jsx +++ b/packages/web/src/components/EditorLayout/index.jsx @@ -10,25 +10,31 @@ import Snackbar from '@mui/material/Snackbar'; import { ReactFlowProvider } from 'reactflow'; import { EditorProvider } from 'contexts/Editor'; -import EditableTypography from 'components/EditableTypography'; -import Container from 'components/Container'; -import Editor from 'components/Editor'; -import Can from 'components/Can'; -import useFormatMessage from 'hooks/useFormatMessage'; -import * as URLS from 'config/urls'; import { TopBar } from './style'; +import * as URLS from 'config/urls'; +import Can from 'components/Can'; +import Container from 'components/Container'; +import EditableTypography from 'components/EditableTypography'; +import Editor from 'components/Editor'; +import EditorNew from 'components/EditorNew/EditorNew'; import useFlow from 'hooks/useFlow'; +import useFormatMessage from 'hooks/useFormatMessage'; import useUpdateFlow from 'hooks/useUpdateFlow'; import useUpdateFlowStatus from 'hooks/useUpdateFlowStatus'; -import EditorNew from 'components/EditorNew/EditorNew'; +import useExportFlow from 'hooks/useExportFlow'; +import useDownloadJsonAsFile from 'hooks/useDownloadJsonAsFile'; +import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; const useNewFlowEditor = process.env.REACT_APP_USE_NEW_FLOW_EDITOR === 'true'; export default function EditorLayout() { const { flowId } = useParams(); const formatMessage = useFormatMessage(); + const enqueueSnackbar = useEnqueueSnackbar(); const { mutateAsync: updateFlow } = useUpdateFlow(flowId); const { mutateAsync: updateFlowStatus } = useUpdateFlowStatus(flowId); + const { mutateAsync: exportFlow } = useExportFlow(flowId); + const downloadJsonAsFile = useDownloadJsonAsFile(); const { data, isLoading: isFlowLoading } = useFlow(flowId); const flow = data?.data; @@ -38,6 +44,19 @@ export default function EditorLayout() { }); }; + const onExportFlow = async (name) => { + const flowExport = await exportFlow(); + + downloadJsonAsFile({ + contents: flowExport.data, + name: flowExport.data.name, + }); + + enqueueSnackbar(formatMessage('flowEditor.flowSuccessfullyExported'), { + variant: 'success', + }); + }; + return ( <> - + + + {(allowed) => ( + + )} + + {(allowed) => ( From 59770781366737117bdcf7a7892c205b160f05db Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Mon, 13 Jan 2025 13:31:42 +0000 Subject: [PATCH 40/45] feat(EditableTypography): make icon size and position consistent --- .../src/components/EditableTypography/index.jsx | 14 ++++---------- packages/web/src/components/EditorLayout/index.jsx | 1 + 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/web/src/components/EditableTypography/index.jsx b/packages/web/src/components/EditableTypography/index.jsx index f4c4b077..693bed1f 100644 --- a/packages/web/src/components/EditableTypography/index.jsx +++ b/packages/web/src/components/EditableTypography/index.jsx @@ -10,9 +10,8 @@ function EditableTypography(props) { const { children, onConfirm = noop, - iconPosition = 'start', - iconSize = 'large', sx, + iconColor = 'inherit', disabled = false, prefixValue = '', ...typographyProps @@ -86,14 +85,10 @@ function EditableTypography(props) { return ( - {!disabled && iconPosition === 'start' && editing === false && ( - - )} - {component} - {!disabled && iconPosition === 'end' && editing === false && ( - + {!disabled && editing === false && ( + )} ); @@ -102,8 +97,7 @@ function EditableTypography(props) { EditableTypography.propTypes = { children: PropTypes.string.isRequired, disabled: PropTypes.bool, - iconPosition: PropTypes.oneOf(['start', 'end']), - iconSize: PropTypes.oneOf(['small', 'large']), + iconColor: PropTypes.oneOf(['action', 'inherit']), onConfirm: PropTypes.func, prefixValue: PropTypes.string, sx: PropTypes.object, diff --git a/packages/web/src/components/EditorLayout/index.jsx b/packages/web/src/components/EditorLayout/index.jsx index c8878b5c..9c5a4faa 100644 --- a/packages/web/src/components/EditorLayout/index.jsx +++ b/packages/web/src/components/EditorLayout/index.jsx @@ -72,6 +72,7 @@ export default function EditorLayout() { variant="body1" onConfirm={onFlowNameUpdate} noWrap + iconColor="action" sx={{ display: 'flex', flex: 1, maxWidth: '50vw', ml: 2 }} > {flow?.name} From 418452c1220b9210c6fca1516a64ac4f853473d2 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Mon, 13 Jan 2025 15:57:37 +0000 Subject: [PATCH 41/45] chore(test): add missing test files in coverage --- packages/backend/vitest.config.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/backend/vitest.config.js b/packages/backend/vitest.config.js index e75ea51d..08e185f9 100644 --- a/packages/backend/vitest.config.js +++ b/packages/backend/vitest.config.js @@ -13,7 +13,15 @@ export default defineConfig({ reportsDirectory: './coverage', reporter: ['text', 'lcov'], all: true, - include: ['**/src/models/**', '**/src/controllers/**'], + include: [ + '**/src/controllers/**', + '**/src/helpers/authentication.test.js', + '**/src/helpers/axios-with-proxy.test.js', + '**/src/helpers/compute-parameters.test.js', + '**/src/helpers/user-ability.test.js', + '**/src/models/**', + '**/src/serializers/**', + ], exclude: [ '**/src/controllers/webhooks/**', '**/src/controllers/paddle/**', From 07a19a5676a56b4f886617c22ef8da9bd140fa99 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Thu, 19 Dec 2024 15:47:16 +0000 Subject: [PATCH 42/45] refactor: remove obsolete dependency and its usage --- packages/web/package.json | 1 - .../web/src/hooks/useAuthenticateApp.ee.js | 19 ------------------- packages/web/yarn.lock | 5 ----- 3 files changed, 25 deletions(-) diff --git a/packages/web/package.json b/packages/web/package.json index cf1eb72c..501d1ccc 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -83,7 +83,6 @@ "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/hooks/useAuthenticateApp.ee.js b/packages/web/src/hooks/useAuthenticateApp.ee.js index 061b8e88..64ff578c 100644 --- a/packages/web/src/hooks/useAuthenticateApp.ee.js +++ b/packages/web/src/hooks/useAuthenticateApp.ee.js @@ -13,7 +13,6 @@ 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) { @@ -143,24 +142,6 @@ 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/yarn.lock b/packages/web/yarn.lock index 8023fb30..6da1c2b8 100644 --- a/packages/web/yarn.lock +++ b/packages/web/yarn.lock @@ -2126,11 +2126,6 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz#427d5549943a9c6fce808e39ea64dbe60d4047f1" integrity sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA== -"@simbathesailor/use-what-changed@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@simbathesailor/use-what-changed/-/use-what-changed-2.0.0.tgz#7f82d78f92c8588b5fadd702065dde93bd781403" - integrity sha512-ulBNrPSvfho9UN6zS2fii3AsdEcp2fMaKeqUZZeCNPaZbB6aXyTUhpEN9atjMAbu/eyK3AY8L4SYJUG62Ekocw== - "@sinclair/typebox@^0.24.1": version "0.24.51" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f" From e36fe2f0b488591287168b62d3ce4d92fca0fafa Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Mon, 13 Jan 2025 16:05:46 +0000 Subject: [PATCH 43/45] chore(test): update coverage thresholds --- packages/backend/vitest.config.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/backend/vitest.config.js b/packages/backend/vitest.config.js index 08e185f9..9c2ef104 100644 --- a/packages/backend/vitest.config.js +++ b/packages/backend/vitest.config.js @@ -28,10 +28,10 @@ export default defineConfig({ ], thresholds: { autoUpdate: true, - statements: 99.44, - branches: 97.78, - functions: 99.1, - lines: 99.44, + statements: 99.4, + branches: 97.77, + functions: 99.16, + lines: 99.4, }, }, }, From e4b31c7f528902cdcf240146e50cd59d892b2345 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Mon, 13 Jan 2025 16:11:29 +0000 Subject: [PATCH 44/45] refactor(webhook): remove singleton webhook logic --- .../apps/twilio/triggers/receive-sms/index.js | 3 -- .../handler-by-connection-id-and-ref-value.js | 38 ------------------- packages/backend/src/models/step.js | 18 +-------- packages/backend/src/routes/webhooks.js | 9 ----- packages/web/src/propTypes/propTypes.js | 4 -- 5 files changed, 1 insertion(+), 71 deletions(-) delete mode 100644 packages/backend/src/controllers/webhooks/handler-by-connection-id-and-ref-value.js diff --git a/packages/backend/src/apps/twilio/triggers/receive-sms/index.js b/packages/backend/src/apps/twilio/triggers/receive-sms/index.js index d5224ca1..eac75b1a 100644 --- a/packages/backend/src/apps/twilio/triggers/receive-sms/index.js +++ b/packages/backend/src/apps/twilio/triggers/receive-sms/index.js @@ -35,9 +35,6 @@ export default defineTrigger({ }, ], - useSingletonWebhook: true, - singletonWebhookRefValueParameter: 'phoneNumberSid', - async run($) { const dataItem = { raw: $.request.body, diff --git a/packages/backend/src/controllers/webhooks/handler-by-connection-id-and-ref-value.js b/packages/backend/src/controllers/webhooks/handler-by-connection-id-and-ref-value.js deleted file mode 100644 index 2f5c611f..00000000 --- a/packages/backend/src/controllers/webhooks/handler-by-connection-id-and-ref-value.js +++ /dev/null @@ -1,38 +0,0 @@ -import path from 'node:path'; - -import Connection from '../../models/connection.js'; -import logger from '../../helpers/logger.js'; -import handler from '../../helpers/webhook-handler.js'; - -export default async (request, response) => { - const computedRequestPayload = { - headers: request.headers, - body: request.body, - query: request.query, - params: request.params, - }; - logger.debug(`Handling incoming webhook request at ${request.originalUrl}.`); - logger.debug(JSON.stringify(computedRequestPayload, null, 2)); - - const { connectionId } = request.params; - - const connection = await Connection.query() - .findById(connectionId) - .throwIfNotFound(); - - if (!(await connection.verifyWebhook(request))) { - return response.sendStatus(401); - } - - const triggerSteps = await connection - .$relatedQuery('triggerSteps') - .where('webhook_path', path.join(request.baseUrl, request.path)); - - if (triggerSteps.length === 0) return response.sendStatus(404); - - for (const triggerStep of triggerSteps) { - await handler(triggerStep.flowId, request, response); - } - - response.sendStatus(204); -}; diff --git a/packages/backend/src/models/step.js b/packages/backend/src/models/step.js index 7c31e0a6..41c53373 100644 --- a/packages/backend/src/models/step.js +++ b/packages/backend/src/models/step.js @@ -1,5 +1,4 @@ import { URL } from 'node:url'; -import get from 'lodash.get'; import Base from './base.js'; import App from './app.js'; import Flow from './flow.js'; @@ -109,25 +108,10 @@ class Step extends Base { if (!triggerCommand) return null; - const { useSingletonWebhook, singletonWebhookRefValueParameter, type } = - triggerCommand; - - const isWebhook = type === 'webhook'; + const isWebhook = triggerCommand.type === 'webhook'; if (!isWebhook) return null; - if (singletonWebhookRefValueParameter) { - const parameterValue = get( - this.parameters, - singletonWebhookRefValueParameter - ); - return `/webhooks/connections/${this.connectionId}/${parameterValue}`; - } - - if (useSingletonWebhook) { - return `/webhooks/connections/${this.connectionId}`; - } - if (this.parameters.workSynchronously) { return `/webhooks/flows/${this.flowId}/sync`; } diff --git a/packages/backend/src/routes/webhooks.js b/packages/backend/src/routes/webhooks.js index 98cadef0..cd2f359b 100644 --- a/packages/backend/src/routes/webhooks.js +++ b/packages/backend/src/routes/webhooks.js @@ -4,7 +4,6 @@ import multer from 'multer'; import appConfig from '../config/app.js'; import webhookHandlerByFlowId from '../controllers/webhooks/handler-by-flow-id.js'; import webhookHandlerSyncByFlowId from '../controllers/webhooks/handler-sync-by-flow-id.js'; -import webhookHandlerByConnectionIdAndRefValue from '../controllers/webhooks/handler-by-connection-id-and-ref-value.js'; const router = Router(); const upload = multer(); @@ -39,14 +38,6 @@ function createRouteHandler(path, handler) { .post(wrappedHandler); } -createRouteHandler( - '/connections/:connectionId/:refValue', - webhookHandlerByConnectionIdAndRefValue -); -createRouteHandler( - '/connections/:connectionId', - webhookHandlerByConnectionIdAndRefValue -); createRouteHandler('/flows/:flowId/sync', webhookHandlerSyncByFlowId); createRouteHandler('/flows/:flowId', webhookHandlerByFlowId); createRouteHandler('/:flowId', webhookHandlerByFlowId); diff --git a/packages/web/src/propTypes/propTypes.js b/packages/web/src/propTypes/propTypes.js index bb594912..77acf9ff 100644 --- a/packages/web/src/propTypes/propTypes.js +++ b/packages/web/src/propTypes/propTypes.js @@ -123,8 +123,6 @@ export const RawTriggerPropType = PropTypes.shape({ showWebhookUrl: PropTypes.bool, pollInterval: PropTypes.number, description: PropTypes.string, - useSingletonWebhook: PropTypes.bool, - singletonWebhookRefValueParameter: PropTypes.string, getInterval: PropTypes.func, run: PropTypes.func, testRun: PropTypes.func, @@ -140,8 +138,6 @@ export const TriggerPropType = PropTypes.shape({ showWebhookUrl: PropTypes.bool, pollInterval: PropTypes.number, description: PropTypes.string, - useSingletonWebhook: PropTypes.bool, - singletonWebhookRefValueParameter: PropTypes.string, getInterval: PropTypes.func, run: PropTypes.func, testRun: PropTypes.func, From 82ba4603393110104d1ae23c4c7d09cfa968565e Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Tue, 14 Jan 2025 10:26:25 +0100 Subject: [PATCH 45/45] docs: Add REDIS_DB env variable to configuration --- packages/docs/pages/advanced/configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/docs/pages/advanced/configuration.md b/packages/docs/pages/advanced/configuration.md index a6635034..aa461568 100644 --- a/packages/docs/pages/advanced/configuration.md +++ b/packages/docs/pages/advanced/configuration.md @@ -35,6 +35,7 @@ Please be careful with the `ENCRYPTION_KEY` and `WEBHOOK_SECRET_KEY` environment | `APP_SECRET_KEY` | string | | Secret Key to authenticate the user | | `REDIS_HOST` | string | `redis` | Redis Host | | `REDIS_PORT` | number | `6379` | Redis Port | +| `REDIS_DB` | number | | Redis Database | | `REDIS_USERNAME` | string | | Redis Username | | `REDIS_PASSWORD` | string | | Redis Password | | `REDIS_TLS` | boolean | `false` | Redis TLS |