Compare commits

..

7 Commits

Author SHA1 Message Date
Ali BARIN
db966eae2d feat(auth): support preventing users from updating their profile 2024-08-23 11:09:28 +00:00
Ali BARIN
53f63996bd Merge pull request #2024 from kuba618/AUT-1186
test: add simple webhook flow test
2024-08-22 11:16:22 +02:00
Ali BARIN
4fedf77991 Merge pull request #2026 from automatisch/aut-1187
refactor(compute-parameters): rewrite logic
2024-08-22 10:18:49 +02:00
Ali BARIN
34331d8763 refactor(compute-parameters) rename functions and variables 2024-08-21 11:35:09 +00:00
Jakub P.
2c21b7762c test: add simple webhook flow test 2024-08-20 19:17:08 +02:00
Ömer Faruk Aydın
7f9c2b687f Merge pull request #2022 from automatisch/use-objects-as-variables
feat(PowerInput): support whole objects as variables
2024-08-19 14:21:52 +03:00
Ali BARIN
b452ed648c feat(PowerInput): support whole objects as variables 2024-08-16 13:22:31 +00:00
26 changed files with 494 additions and 135 deletions

View File

@@ -16,7 +16,6 @@ import trimWhitespace from './transformers/trim-whitespace.js';
import useDefaultValue from './transformers/use-default-value.js'; import useDefaultValue from './transformers/use-default-value.js';
import parseStringifiedJson from './transformers/parse-stringified-json.js'; import parseStringifiedJson from './transformers/parse-stringified-json.js';
import createUuid from './transformers/create-uuid.js'; import createUuid from './transformers/create-uuid.js';
import stringifyJson from './transformers/stringify-json.js';
const transformers = { const transformers = {
base64ToString, base64ToString,
@@ -35,7 +34,6 @@ const transformers = {
useDefaultValue, useDefaultValue,
parseStringifiedJson, parseStringifiedJson,
createUuid, createUuid,
stringifyJson,
}; };
export default defineAction({ export default defineAction({
@@ -65,7 +63,6 @@ export default defineAction({
{ label: 'Extract Number', value: 'extractNumber' }, { label: 'Extract Number', value: 'extractNumber' },
{ label: 'Lowercase', value: 'lowercase' }, { label: 'Lowercase', value: 'lowercase' },
{ label: 'Parse stringified JSON', value: 'parseStringifiedJson' }, { label: 'Parse stringified JSON', value: 'parseStringifiedJson' },
{ label: 'Stringify JSON', value: 'stringifyJson' },
{ label: 'Pluralize', value: 'pluralize' }, { label: 'Pluralize', value: 'pluralize' },
{ label: 'Replace', value: 'replace' }, { label: 'Replace', value: 'replace' },
{ label: 'String to Base64', value: 'stringToBase64' }, { label: 'String to Base64', value: 'stringToBase64' },

View File

@@ -1,7 +0,0 @@
const stringifyJson = ($) => {
const input = $.step.parameters.input;
return JSON.stringify(input);
};
export default stringifyJson;

View File

@@ -13,7 +13,6 @@ import encodeUri from './text/encode-uri.js';
import trimWhitespace from './text/trim-whitespace.js'; import trimWhitespace from './text/trim-whitespace.js';
import useDefaultValue from './text/use-default-value.js'; import useDefaultValue from './text/use-default-value.js';
import parseStringifiedJson from './text/parse-stringified-json.js'; import parseStringifiedJson from './text/parse-stringified-json.js';
import stringifyJson from './text/stringify-json.js';
import performMathOperation from './numbers/perform-math-operation.js'; import performMathOperation from './numbers/perform-math-operation.js';
import randomNumber from './numbers/random-number.js'; import randomNumber from './numbers/random-number.js';
import formatNumber from './numbers/format-number.js'; import formatNumber from './numbers/format-number.js';
@@ -41,7 +40,6 @@ const options = {
formatPhoneNumber, formatPhoneNumber,
formatDateTime, formatDateTime,
parseStringifiedJson, parseStringifiedJson,
stringifyJson,
}; };
export default { export default {

View File

@@ -1,4 +1,4 @@
const parseStringifiedJson = [ const useDefaultValue = [
{ {
label: 'Input', label: 'Input',
key: 'input', key: 'input',
@@ -9,4 +9,4 @@ const parseStringifiedJson = [
}, },
]; ];
export default parseStringifiedJson; export default useDefaultValue;

View File

@@ -1,12 +0,0 @@
const stringifyJson = [
{
label: 'Input',
key: 'input',
type: 'string',
required: true,
description: 'JSON to stringify.',
variables: true,
},
];
export default stringifyJson;

View File

@@ -0,0 +1,12 @@
export async function up(knex) {
await knex('config').insert({
key: 'userManagement.preventUsersFromUpdatingTheirProfile',
value: {
data: false
}
});
};
export async function down(knex) {
await knex('config').where({ key: 'userManagement.preventUsersFromUpdatingTheirProfile' }).delete();
};

View File

@@ -2,67 +2,137 @@ import get from 'lodash.get';
const variableRegExp = /({{step\.[\da-zA-Z-]+(?:\.[^.}{]+)+}})/g; const variableRegExp = /({{step\.[\da-zA-Z-]+(?:\.[^.}{]+)+}})/g;
export default function computeParameters(parameters, executionSteps) { function getParameterEntries(parameters) {
const entries = Object.entries(parameters); return Object.entries(parameters);
return entries.reduce((result, [key, value]) => { }
if (typeof value === 'string') {
const parts = value.split(variableRegExp);
const computedValue = parts function computeParameterEntries(parameterEntries, executionSteps) {
.map((part) => { const defaultComputedParameters = {};
const isVariable = part.match(variableRegExp); return parameterEntries.reduce((result, [key, value]) => {
const parameterComputedValue = computeParameter(value, executionSteps);
if (isVariable) {
const stepIdAndKeyPath = part.replace(/{{step.|}}/g, '');
const [stepId, ...keyPaths] = stepIdAndKeyPath.split('.');
const keyPath = keyPaths.join('.');
const executionStep = executionSteps.find((executionStep) => {
return executionStep.stepId === stepId;
});
const data = executionStep?.dataOut;
const dataValue = get(data, keyPath);
// Covers both arrays and objects
if (typeof dataValue === 'object') {
return JSON.stringify(dataValue);
}
return dataValue;
}
return part;
}).join('');
// challenge the input to see if it is stringifies object or array
try {
const parsedValue = JSON.parse(computedValue);
if (typeof parsedValue === 'number') {
throw new Error('Use original unparsed value.');
}
return {
...result,
[key]: parsedValue,
};
} catch (error) {
return {
...result,
[key]: computedValue,
};
}
}
if (Array.isArray(value)) {
return {
...result,
[key]: value.map((item) => computeParameters(item, executionSteps)),
};
}
return { return {
...result, ...result,
[key]: value, [key]: parameterComputedValue,
}; }
}, {}); }, defaultComputedParameters);
}
function computeParameter(value, executionSteps) {
if (typeof value === 'string') {
const computedStringParameter = computeStringParameter(value, executionSteps);
return computedStringParameter;
}
if (Array.isArray(value)) {
const computedArrayParameter = computeArrayParameter(value, executionSteps);
return computedArrayParameter;
}
return value;
}
function splitByVariable(stringValue) {
const parts = stringValue.split(variableRegExp);
return parts;
}
function isVariable(stringValue) {
return stringValue.match(variableRegExp);
}
function splitVariableByStepIdAndKeyPath(variableValue) {
const stepIdAndKeyPath = variableValue.replace(/{{step.|}}/g, '');
const [stepId, ...keyPaths] = stepIdAndKeyPath.split('.');
const keyPath = keyPaths.join('.');
return {
stepId,
keyPath
}
}
function getVariableStepId(variableValue) {
const { stepId } = splitVariableByStepIdAndKeyPath(variableValue);
return stepId;
}
function getVariableKeyPath(variableValue) {
const { keyPath } = splitVariableByStepIdAndKeyPath(variableValue);
return keyPath
}
function getVariableExecutionStep(variableValue, executionSteps) {
const stepId = getVariableStepId(variableValue);
const executionStep = executionSteps.find((executionStep) => {
return executionStep.stepId === stepId;
});
return executionStep;
}
function computeVariable(variable, executionSteps) {
const keyPath = getVariableKeyPath(variable);
const executionStep = getVariableExecutionStep(variable, executionSteps);
const data = executionStep?.dataOut;
const computedVariable = get(data, keyPath);
/**
* Inline both arrays and objects. Otherwise, variables resolving to
* them would be resolved as `[object Object]` or lose their shape.
*/
if (typeof computedVariable === 'object') {
return JSON.stringify(computedVariable);
}
return computedVariable;
}
function autoParseComputedVariable(computedVariable) {
// challenge the input to see if it is stringified object or array
try {
const parsedValue = JSON.parse(computedVariable);
if (typeof parsedValue === 'number') {
throw new Error('Use original unparsed value.');
}
return parsedValue;
} catch (error) {
return computedVariable;
}
}
function computeStringParameter(stringValue, executionSteps) {
const parts = splitByVariable(stringValue);
const computedValue = parts
.map((part) => {
const variable = isVariable(part);
if (variable) {
return computeVariable(part, executionSteps);
}
return part;
})
.join('');
const autoParsedValue = autoParseComputedVariable(computedValue);
return autoParsedValue;
}
function computeArrayParameter(arrayValue, executionSteps) {
return arrayValue.map((item) => computeParameters(item, executionSteps));
}
export default function computeParameters(parameters, executionSteps) {
const parameterEntries = getParameterEntries(parameters);
return computeParameterEntries(parameterEntries, executionSteps);
} }

View File

@@ -1,4 +1,6 @@
const { AuthenticatedPage } = require('./authenticated-page'); const { AuthenticatedPage } = require('./authenticated-page');
const { expect } = require('@playwright/test');
const axios = require('axios');
export class FlowEditorPage extends AuthenticatedPage { export class FlowEditorPage extends AuthenticatedPage {
screenshotPath = '/flow-editor'; screenshotPath = '/flow-editor';
@@ -9,17 +11,74 @@ export class FlowEditorPage extends AuthenticatedPage {
constructor(page) { constructor(page) {
super(page); super(page);
this.page = page;
this.appAutocomplete = this.page.getByTestId('choose-app-autocomplete'); this.appAutocomplete = this.page.getByTestId('choose-app-autocomplete');
this.eventAutocomplete = this.page.getByTestId('choose-event-autocomplete'); this.eventAutocomplete = this.page.getByTestId('choose-event-autocomplete');
this.continueButton = this.page.getByTestId('flow-substep-continue-button'); this.continueButton = this.page.getByTestId('flow-substep-continue-button');
this.testAndContinueButton = this.page.getByText('Test & Continue');
this.connectionAutocomplete = this.page.getByTestId( this.connectionAutocomplete = this.page.getByTestId(
'choose-connection-autocomplete' 'choose-connection-autocomplete'
); );
this.testOuput = this.page.getByTestId('flow-test-substep-output'); this.testOutput = this.page.getByTestId('flow-test-substep-output');
this.hasNoOutput = this.page.getByTestId('flow-test-substep-no-output');
this.unpublishFlowButton = this.page.getByTestId('unpublish-flow-button'); this.unpublishFlowButton = this.page.getByTestId('unpublish-flow-button');
this.publishFlowButton = this.page.getByTestId('publish-flow-button'); this.publishFlowButton = this.page.getByTestId('publish-flow-button');
this.infoSnackbar = this.page.getByTestId('flow-cannot-edit-info-snackbar'); this.infoSnackbar = this.page.getByTestId('flow-cannot-edit-info-snackbar');
this.trigger = this.page.getByLabel('Trigger on weekends?'); this.trigger = this.page.getByLabel('Trigger on weekends?');
this.stepCircularLoader = this.page.getByTestId('step-circular-loader'); this.stepCircularLoader = this.page.getByTestId('step-circular-loader');
this.flowName = this.page.getByTestId('editableTypography');
this.flowNameInput = this.page
.getByTestId('editableTypographyInput')
.locator('input');
}
async createWebhookTrigger(workSynchronously) {
await this.appAutocomplete.click();
await this.page.getByRole('option', { name: 'Webhook' }).click();
await expect(this.eventAutocomplete).toBeVisible();
await this.eventAutocomplete.click();
await this.page.getByRole('option', { name: 'Catch raw webhook' }).click();
await this.continueButton.click();
await this.page
.getByTestId('parameters.workSynchronously-autocomplete')
.click();
await this.page
.getByRole('option', { name: workSynchronously ? 'Yes' : 'No' })
.click();
await this.continueButton.click();
const webhookUrl = this.page.locator('input[name="webhookUrl"]');
if (workSynchronously) {
await expect(webhookUrl).toHaveValue(/sync/);
} else {
await expect(webhookUrl).not.toHaveValue(/sync/);
}
const triggerResponse = await axios.get(await webhookUrl.inputValue());
await expect(triggerResponse.status).toBe(204);
await expect(this.testOutput).not.toBeVisible();
await this.testAndContinueButton.click();
await expect(this.testOutput).toBeVisible();
await expect(this.hasNoOutput).not.toBeVisible();
await this.continueButton.click();
return await webhookUrl.inputValue();
}
async chooseAppAndEvent(appName, eventName) {
await this.appAutocomplete.click();
await this.page.getByRole('option', { name: appName }).click();
await expect(this.eventAutocomplete).toBeVisible();
await this.eventAutocomplete.click();
await this.page.getByRole('option', { name: eventName }).click();
await this.continueButton.click();
}
async testAndContinue() {
await this.continueButton.last().click();
await expect(this.testOutput).toBeVisible();
await this.continueButton.click();
} }
} }

View File

@@ -0,0 +1,82 @@
const { test, expect } = require('../../fixtures/index');
const axios = require('axios');
test.describe('Webhook flow', () => {
test.beforeEach(async ({ page }) => {
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(page.getByTestId('flow-step')).toHaveCount(2);
});
test('Create a new flow with a sync Webhook step then a Webhook step', async ({
flowEditorPage,
page,
}) => {
await flowEditorPage.flowName.click();
await flowEditorPage.flowNameInput.fill('syncWebhook');
const syncWebhookUrl = await flowEditorPage.createWebhookTrigger(true);
await flowEditorPage.chooseAppAndEvent('Webhook', 'Respond with');
await expect(flowEditorPage.continueButton.last()).not.toBeEnabled();
await page
.getByTestId('parameters.statusCode-power-input')
.locator('[contenteditable]')
.fill('200');
await flowEditorPage.clickAway();
await expect(flowEditorPage.continueButton.last()).not.toBeEnabled();
await page
.getByTestId('parameters.body-power-input')
.locator('[contenteditable]')
.fill('response from webhook');
await flowEditorPage.clickAway();
await expect(flowEditorPage.continueButton).toBeEnabled();
await flowEditorPage.continueButton.click();
await flowEditorPage.testAndContinue();
await flowEditorPage.publishFlowButton.click();
const response = await axios.get(syncWebhookUrl);
await expect(response.status).toBe(200);
await expect(response.data).toBe('response from webhook');
});
test('Create a new flow with an async Webhook step then a Webhook step', async ({
flowEditorPage,
page,
}) => {
await flowEditorPage.flowName.click();
await flowEditorPage.flowNameInput.fill('asyncWebhook');
const asyncWebhookUrl = await flowEditorPage.createWebhookTrigger(false);
await flowEditorPage.chooseAppAndEvent('Webhook', 'Respond with');
await expect(flowEditorPage.continueButton.last()).not.toBeEnabled();
await page
.getByTestId('parameters.statusCode-power-input')
.locator('[contenteditable]')
.fill('200');
await flowEditorPage.clickAway();
await expect(flowEditorPage.continueButton.last()).not.toBeEnabled();
await page
.getByTestId('parameters.body-power-input')
.locator('[contenteditable]')
.fill('response from webhook');
await flowEditorPage.clickAway();
await expect(flowEditorPage.continueButton).toBeEnabled();
await flowEditorPage.continueButton.click();
await flowEditorPage.testAndContinue();
await flowEditorPage.publishFlowButton.click();
const response = await axios.get(asyncWebhookUrl);
await expect(response.status).toBe(204);
await expect(response.data).toBe('');
});
});

View File

@@ -2,7 +2,6 @@ const { test, expect } = require('../../fixtures/index');
test('Ensure creating a new flow works', async ({ page }) => { test('Ensure creating a new flow works', async ({ page }) => {
await page.getByTestId('create-flow-button').click(); await page.getByTestId('create-flow-button').click();
await expect(page).toHaveURL(/\/editor\/create/);
await expect(page).toHaveURL( await expect(page).toHaveURL(
/\/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}/ /\/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}/
); );
@@ -69,9 +68,9 @@ test(
await test.step('test trigger', async () => { await test.step('test trigger', async () => {
await test.step('show sample output', async () => { await test.step('show sample output', async () => {
await expect(flowEditorPage.testOuput).not.toBeVisible(); await expect(flowEditorPage.testOutput).not.toBeVisible();
await flowEditorPage.continueButton.click(); await flowEditorPage.continueButton.click();
await expect(flowEditorPage.testOuput).toBeVisible(); await expect(flowEditorPage.testOutput).toBeVisible();
await flowEditorPage.screenshot({ await flowEditorPage.screenshot({
path: 'Scheduler trigger test output.png', path: 'Scheduler trigger test output.png',
}); });
@@ -143,12 +142,12 @@ test(
await test.step('test trigger substep', async () => { await test.step('test trigger substep', async () => {
await test.step('show sample output', async () => { await test.step('show sample output', async () => {
await expect(flowEditorPage.testOuput).not.toBeVisible(); await expect(flowEditorPage.testOutput).not.toBeVisible();
await page await page
.getByTestId('flow-substep-continue-button') .getByTestId('flow-substep-continue-button')
.first() .first()
.click(); .click();
await expect(flowEditorPage.testOuput).toBeVisible(); await expect(flowEditorPage.testOutput).toBeVisible();
await flowEditorPage.screenshot({ await flowEditorPage.screenshot({
path: 'Ntfy action test output.png', path: 'Ntfy action test output.png',
}); });

View File

@@ -7,6 +7,7 @@ import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle'; import DialogTitle from '@mui/material/DialogTitle';
import * as React from 'react'; import * as React from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { AppPropType } from 'propTypes/propTypes'; import { AppPropType } from 'propTypes/propTypes';
import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee'; import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee';
@@ -17,7 +18,6 @@ import useFormatMessage from 'hooks/useFormatMessage';
import { generateExternalLink } from 'helpers/translationValues'; import { generateExternalLink } from 'helpers/translationValues';
import { Form } from './style'; import { Form } from './style';
import useAppAuth from 'hooks/useAppAuth'; import useAppAuth from 'hooks/useAppAuth';
import { useQueryClient } from '@tanstack/react-query';
function AddAppConnection(props) { function AddAppConnection(props) {
const { application, connectionId, onClose } = props; const { application, connectionId, onClose } = props;

View File

@@ -41,6 +41,7 @@ function EditableTypography(props) {
if (editing) { if (editing) {
component = ( component = (
<TextField <TextField
data-test="editableTypographyInput"
onClick={handleTextFieldClick} onClick={handleTextFieldClick}
onKeyDown={handleTextFieldKeyDown} onKeyDown={handleTextFieldKeyDown}
onBlur={handleTextFieldBlur} onBlur={handleTextFieldBlur}

View File

@@ -101,6 +101,7 @@ export default function EditorLayout() {
{!isFlowLoading && ( {!isFlowLoading && (
<EditableTypography <EditableTypography
data-test="editableTypography"
variant="body1" variant="body1"
onConfirm={onFlowNameUpdate} onConfirm={onFlowNameUpdate}
noWrap noWrap

View File

@@ -12,6 +12,7 @@ import TextField from 'components/TextField';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import useCreateAccessToken from 'hooks/useCreateAccessToken'; import useCreateAccessToken from 'hooks/useCreateAccessToken';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
import usePreventUsersFromUpdatingTheirProfile from 'hooks/usePreventUsersFromUpdatingTheirProfile';
function LoginForm() { function LoginForm() {
const isCloud = useCloud(); const isCloud = useCloud();
@@ -19,6 +20,7 @@ function LoginForm() {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const enqueueSnackbar = useEnqueueSnackbar(); const enqueueSnackbar = useEnqueueSnackbar();
const authentication = useAuthentication(); const authentication = useAuthentication();
const preventUsersFromUpdatingTheirProfile = usePreventUsersFromUpdatingTheirProfile();
const { mutateAsync: createAccessToken, isPending: loading } = const { mutateAsync: createAccessToken, isPending: loading } =
useCreateAccessToken(); useCreateAccessToken();
@@ -84,7 +86,7 @@ function LoginForm() {
sx={{ mb: 1 }} sx={{ mb: 1 }}
/> />
{isCloud && ( {isCloud && !preventUsersFromUpdatingTheirProfile && (
<Link <Link
component={RouterLink} component={RouterLink}
to={URLS.FORGOT_PASSWORD} to={URLS.FORGOT_PASSWORD}

View File

@@ -25,25 +25,40 @@ const process = ({ data, parentKey, index, parentLabel = '' }) => {
sampleValue: JSON.stringify(sampleValue), sampleValue: JSON.stringify(sampleValue),
}; };
const arrayItems = sampleValue.flatMap((item, index) => const arrayItems = sampleValue.flatMap((item, index) => {
process({ const itemItself = {
label: `${label}.${index}`,
value: `${value}.${index}`,
sampleValue: JSON.stringify(item),
};
const itemEntries = process({
data: item, data: item,
parentKey: value, parentKey: value,
index, index,
parentLabel: label, parentLabel: label,
}), });
);
// TODO: remove spreading return [itemItself].concat(itemEntries);
return [arrayItself, ...arrayItems]; });
return [arrayItself].concat(arrayItems);
} }
if (typeof sampleValue === 'object' && sampleValue !== null) { if (typeof sampleValue === 'object' && sampleValue !== null) {
return process({ const objectItself = {
label,
value,
sampleValue: JSON.stringify(sampleValue),
};
const objectEntries = process({
data: sampleValue, data: sampleValue,
parentKey: value, parentKey: value,
parentLabel: label, parentLabel: label,
}); });
return [objectItself].concat(objectEntries);
} }
return [ return [

View File

@@ -40,6 +40,10 @@ function Switch(props) {
<FormControlLabel <FormControlLabel
className={className} className={className}
{...FormControlLabelProps} {...FormControlLabelProps}
componentsProps={{
typography: {
display: 'flex',
}}}
control={ control={
<MuiSwitch <MuiSwitch
{...switchProps} {...switchProps}

View File

@@ -118,7 +118,7 @@ function TestSubstep(props) {
)} )}
{hasNoOutput && ( {hasNoOutput && (
<Alert severity="warning" sx={{ mb: 1, width: '100%' }}> <Alert data-test="flow-test-substep-no-output" severity="warning" sx={{ mb: 1, width: '100%' }}>
<AlertTitle sx={{ fontWeight: 700 }}> <AlertTitle sx={{ fontWeight: 700 }}>
{formatMessage('flowEditor.noTestDataTitle')} {formatMessage('flowEditor.noTestDataTitle')}
</AlertTitle> </AlertTitle>

View File

@@ -0,0 +1,13 @@
import useAutomatischConfig from 'hooks/useAutomatischConfig';
export default function usePreventUsersFromUpdatingTheirProfile() {
const { data, isSuccess } =
useAutomatischConfig();
const automatischConfig = data?.data;
const preventUsersFromUpdatingTheirProfile = isSuccess ? automatischConfig['userManagement.preventUsersFromUpdatingTheirProfile'] : false;
console.log('preventUsersFromUpdatingTheirProfile', preventUsersFromUpdatingTheirProfile, automatischConfig)
return preventUsersFromUpdatingTheirProfile;
}

View File

@@ -259,20 +259,26 @@
"userInterfacePage.primaryLightColorFieldLabel": "Primary light color", "userInterfacePage.primaryLightColorFieldLabel": "Primary light color",
"userInterfacePage.svgDataFieldLabel": "Logo SVG code", "userInterfacePage.svgDataFieldLabel": "Logo SVG code",
"userInterfacePage.submit": "Update", "userInterfacePage.submit": "Update",
"authenticationPage.title": "Single Sign-On with SAML", "authenticationPage.title": "Authentication",
"authenticationForm.active": "Active", "authenticationConfig.title": "User management",
"authenticationForm.name": "Name", "authenticationConfig.userManagementPreventUsersFromUpdatingTheirProfile": "Prevent users from updating their profile",
"authenticationForm.certificate": "Certificate", "authenticationConfig.userManagementPreventUsersFromUpdatingTheirProfileTooltip": "This will prevent users from updating their full name, email and password. This is useful when you want to manage users from your own identity provider.",
"authenticationForm.signatureAlgorithm": "Signature algorithm", "authenticationConfig.save": "Save",
"authenticationForm.issuer": "Issuer", "authenticationConfig.successfullySaved": "The configuration has been saved.",
"authenticationForm.entryPoint": "Entry point", "samlAuthenticationPage.title": "Single Sign-On with SAML",
"authenticationForm.firstnameAttributeName": "Firstname attribute name", "samlAuthenticationForm.active": "Active",
"authenticationForm.surnameAttributeName": "Surname attribute name", "samlAuthenticationForm.name": "Name",
"authenticationForm.emailAttributeName": "Email attribute name", "samlAuthenticationForm.certificate": "Certificate",
"authenticationForm.roleAttributeName": "Role attribute name", "samlAuthenticationForm.signatureAlgorithm": "Signature algorithm",
"authenticationForm.defaultRole": "Default role", "samlAuthenticationForm.issuer": "Issuer",
"authenticationForm.successfullySaved": "The provider has been saved.", "samlAuthenticationForm.entryPoint": "Entry point",
"authenticationForm.save": "Save", "samlAuthenticationForm.firstnameAttributeName": "Firstname attribute name",
"samlAuthenticationForm.surnameAttributeName": "Surname attribute name",
"samlAuthenticationForm.emailAttributeName": "Email attribute name",
"samlAuthenticationForm.roleAttributeName": "Role attribute name",
"samlAuthenticationForm.defaultRole": "Default role",
"samlAuthenticationForm.successfullySaved": "The provider has been saved.",
"samlAuthenticationForm.save": "Save",
"roleMappingsForm.title": "Role mappings", "roleMappingsForm.title": "Role mappings",
"roleMappingsForm.remoteRoleName": "Remote role name", "roleMappingsForm.remoteRoleName": "Remote role name",
"roleMappingsForm.role": "Role", "roleMappingsForm.role": "Role",

View File

@@ -0,0 +1,98 @@
import { useMutation } from '@apollo/client';
import LoadingButton from '@mui/lab/LoadingButton';
import Stack from '@mui/material/Stack';
import Switch from 'components/Switch';
import Typography from '@mui/material/Typography';
import Tooltip from '@mui/material/Tooltip';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import Form from 'components/Form';
import useFormatMessage from 'hooks/useFormatMessage';
import useAutomatischConfig from 'hooks/useAutomatischConfig';
import { UPDATE_CONFIG } from 'graphql/mutations/update-config.ee';
import { useQueryClient } from '@tanstack/react-query';
function AuthenticationConfig() {
const formatMessage = useFormatMessage();
const enqueueSnackbar = useEnqueueSnackbar();
const queryClient = useQueryClient();
const { data, isLoading: isAutomatischConfigLoading } =
useAutomatischConfig();
const automatischConfig = data?.data;
const [
updateConfig,
{ loading: updateConfigLoading },
] = useMutation(UPDATE_CONFIG);
const handleSubmit = async (values) => {
try {
await updateConfig({
variables: {
input: {
'userManagement.preventUsersFromUpdatingTheirProfile': values.userManagement.preventUsersFromUpdatingTheirProfile,
},
},
});
await queryClient.invalidateQueries({
queryKey: ['automatisch', 'config'],
});
enqueueSnackbar(formatMessage('authenticationConfig.successfullySaved'), {
variant: 'success',
SnackbarProps: {
'data-test': 'snackbar-update-role-mappings-success',
},
});
} catch (error) {
throw new Error('Failed while saving!');
}
};
if (isAutomatischConfigLoading) {
return null;
}
return (
<>
<Typography variant="h4">
{formatMessage('authenticationConfig.title')}
</Typography>
<Form defaultValues={automatischConfig} onSubmit={handleSubmit}>
<Stack direction="column" spacing={2}>
<Switch
name="userManagement.preventUsersFromUpdatingTheirProfile"
label={<>
{formatMessage('authenticationConfig.userManagementPreventUsersFromUpdatingTheirProfile')}
<Tooltip
title={formatMessage('authenticationConfig.userManagementPreventUsersFromUpdatingTheirProfileTooltip')}
sx={{ ml: 1 }}
>
<InfoOutlinedIcon />
</Tooltip>
</>}
/>
<LoadingButton
type="submit"
variant="contained"
color="primary"
sx={{ boxShadow: 2 }}
loading={updateConfigLoading}
>
{formatMessage('authenticationConfig.save')}
</LoadingButton>
</Stack>
</Form>
</>
);
}
AuthenticationConfig.propTypes = {
};
export default AuthenticationConfig;

View File

@@ -1,7 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useMutation } from '@apollo/client'; import { useMutation } from '@apollo/client';
import LoadingButton from '@mui/lab/LoadingButton'; import LoadingButton from '@mui/lab/LoadingButton';
import Divider from '@mui/material/Divider';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
@@ -85,7 +84,6 @@ function RoleMappings({ provider, providerLoading }) {
return ( return (
<> <>
<Divider sx={{ pt: 2 }} />
<Typography variant="h3"> <Typography variant="h3">
{formatMessage('roleMappingsForm.title')} {formatMessage('roleMappingsForm.title')}
</Typography> </Typography>

View File

@@ -75,7 +75,7 @@ function SamlConfiguration({ provider, providerLoading }) {
}, },
}); });
enqueueSnackbar(formatMessage('authenticationForm.successfullySaved'), { enqueueSnackbar(formatMessage('samlAuthenticationForm.successfullySaved'), {
variant: 'success', variant: 'success',
SnackbarProps: { SnackbarProps: {
'data-test': 'snackbar-save-saml-provider-success', 'data-test': 'snackbar-save-saml-provider-success',
@@ -98,18 +98,18 @@ function SamlConfiguration({ provider, providerLoading }) {
<Stack direction="column" gap={2}> <Stack direction="column" gap={2}>
<Switch <Switch
name="active" name="active"
label={formatMessage('authenticationForm.active')} label={formatMessage('samlAuthenticationForm.active')}
/> />
<TextField <TextField
required={true} required={true}
name="name" name="name"
label={formatMessage('authenticationForm.name')} label={formatMessage('samlAuthenticationForm.name')}
fullWidth fullWidth
/> />
<TextField <TextField
required={true} required={true}
name="certificate" name="certificate"
label={formatMessage('authenticationForm.certificate')} label={formatMessage('samlAuthenticationForm.certificate')}
fullWidth fullWidth
multiline multiline
/> />
@@ -126,44 +126,44 @@ function SamlConfiguration({ provider, providerLoading }) {
renderInput={(params) => ( renderInput={(params) => (
<MuiTextField <MuiTextField
{...params} {...params}
label={formatMessage('authenticationForm.signatureAlgorithm')} label={formatMessage('samlAuthenticationForm.signatureAlgorithm')}
/> />
)} )}
/> />
<TextField <TextField
required={true} required={true}
name="issuer" name="issuer"
label={formatMessage('authenticationForm.issuer')} label={formatMessage('samlAuthenticationForm.issuer')}
fullWidth fullWidth
/> />
<TextField <TextField
required={true} required={true}
name="entryPoint" name="entryPoint"
label={formatMessage('authenticationForm.entryPoint')} label={formatMessage('samlAuthenticationForm.entryPoint')}
fullWidth fullWidth
/> />
<TextField <TextField
required={true} required={true}
name="firstnameAttributeName" name="firstnameAttributeName"
label={formatMessage('authenticationForm.firstnameAttributeName')} label={formatMessage('samlAuthenticationForm.firstnameAttributeName')}
fullWidth fullWidth
/> />
<TextField <TextField
required={true} required={true}
name="surnameAttributeName" name="surnameAttributeName"
label={formatMessage('authenticationForm.surnameAttributeName')} label={formatMessage('samlAuthenticationForm.surnameAttributeName')}
fullWidth fullWidth
/> />
<TextField <TextField
required={true} required={true}
name="emailAttributeName" name="emailAttributeName"
label={formatMessage('authenticationForm.emailAttributeName')} label={formatMessage('samlAuthenticationForm.emailAttributeName')}
fullWidth fullWidth
/> />
<TextField <TextField
required={true} required={true}
name="roleAttributeName" name="roleAttributeName"
label={formatMessage('authenticationForm.roleAttributeName')} label={formatMessage('samlAuthenticationForm.roleAttributeName')}
fullWidth fullWidth
/> />
<ControlledAutocomplete <ControlledAutocomplete
@@ -175,7 +175,7 @@ function SamlConfiguration({ provider, providerLoading }) {
renderInput={(params) => ( renderInput={(params) => (
<MuiTextField <MuiTextField
{...params} {...params}
label={formatMessage('authenticationForm.defaultRole')} label={formatMessage('samlAuthenticationForm.defaultRole')}
/> />
)} )}
loading={isRolesLoading} loading={isRolesLoading}
@@ -187,7 +187,7 @@ function SamlConfiguration({ provider, providerLoading }) {
sx={{ boxShadow: 2 }} sx={{ boxShadow: 2 }}
loading={loading} loading={loading}
> >
{formatMessage('authenticationForm.save')} {formatMessage('samlAuthenticationForm.save')}
</LoadingButton> </LoadingButton>
</Stack> </Stack>
</Form> </Form>

View File

@@ -2,11 +2,14 @@ import Grid from '@mui/material/Grid';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import PageTitle from 'components/PageTitle'; import PageTitle from 'components/PageTitle';
import Container from 'components/Container'; import Container from 'components/Container';
import Divider from '@mui/material/Divider';
import useFormatMessage from 'hooks/useFormatMessage'; import useFormatMessage from 'hooks/useFormatMessage';
import useSamlAuthProvider from 'hooks/useSamlAuthProvider'; import useSamlAuthProvider from 'hooks/useSamlAuthProvider';
import AuthenticationConfig from './AuthenticationConfig';
import SamlConfiguration from './SamlConfiguration'; import SamlConfiguration from './SamlConfiguration';
import RoleMappings from './RoleMappings'; import RoleMappings from './RoleMappings';
import useAdminSamlAuthProviders from 'hooks/useAdminSamlAuthProviders.ee'; import useAdminSamlAuthProviders from 'hooks/useAdminSamlAuthProviders.ee';
function AuthenticationPage() { function AuthenticationPage() {
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
@@ -16,20 +19,37 @@ function AuthenticationPage() {
const { data, isLoading: isProviderLoading } = useSamlAuthProvider({ const { data, isLoading: isProviderLoading } = useSamlAuthProvider({
samlAuthProviderId, samlAuthProviderId,
}); });
const provider = data?.data; const provider = data?.data;
return ( return (
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}> <Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
<Grid container item xs={12} sm={10} md={9}> <Grid container item xs={12} sm={10} md={9}>
<Grid container item xs={12} sx={{ mb: [2, 5] }}> <Grid container item xs={12}>
<PageTitle>{formatMessage('authenticationPage.title')}</PageTitle> <PageTitle>{formatMessage('authenticationPage.title')}</PageTitle>
</Grid> </Grid>
<Grid item xs={12} sx={{ pt: 5, pb: 5 }}>
<Stack spacing={5}>
<AuthenticationConfig />
<Divider />
</Stack>
</Grid>
<Grid container item xs={12}>
<PageTitle variant="h4">{formatMessage('samlAuthenticationPage.title')}</PageTitle>
</Grid>
<Grid item xs={12} sx={{ pt: 5, pb: 5 }}> <Grid item xs={12} sx={{ pt: 5, pb: 5 }}>
<Stack spacing={5}> <Stack spacing={5}>
<SamlConfiguration <SamlConfiguration
provider={provider} provider={provider}
providerLoading={isProviderLoading} providerLoading={isProviderLoading}
/> />
<Divider />
<RoleMappings <RoleMappings
provider={provider} provider={provider}
providerLoading={isProviderLoading} providerLoading={isProviderLoading}

View File

@@ -10,6 +10,7 @@ export default function Login() {
<Container maxWidth="sm"> <Container maxWidth="sm">
<Stack direction="column" gap={2}> <Stack direction="column" gap={2}>
<LoginForm /> <LoginForm />
<SsoProviders /> <SsoProviders />
</Stack> </Stack>
</Container> </Container>

View File

@@ -29,12 +29,14 @@ import adminSettingsRoutes from './adminSettingsRoutes';
import Notifications from 'pages/Notifications'; import Notifications from 'pages/Notifications';
import useAutomatischConfig from 'hooks/useAutomatischConfig'; import useAutomatischConfig from 'hooks/useAutomatischConfig';
import useAuthentication from 'hooks/useAuthentication'; import useAuthentication from 'hooks/useAuthentication';
import usePreventUsersFromUpdatingTheirProfile from 'hooks/usePreventUsersFromUpdatingTheirProfile';
import useAutomatischInfo from 'hooks/useAutomatischInfo'; import useAutomatischInfo from 'hooks/useAutomatischInfo';
import Installation from 'pages/Installation'; import Installation from 'pages/Installation';
function Routes() { function Routes() {
const { data: automatischInfo, isSuccess } = useAutomatischInfo(); const { data: automatischInfo, isSuccess } = useAutomatischInfo();
const { data: configData } = useAutomatischConfig(); const { data: configData } = useAutomatischConfig();
const preventUsersFromUpdatingTheirProfile = usePreventUsersFromUpdatingTheirProfile();
const { isAuthenticated } = useAuthentication(); const { isAuthenticated } = useAuthentication();
const config = configData?.data; const config = configData?.data;
@@ -134,14 +136,14 @@ function Routes() {
} }
/> />
<Route {preventUsersFromUpdatingTheirProfile === false && <Route
path={URLS.FORGOT_PASSWORD} path={URLS.FORGOT_PASSWORD}
element={ element={
<PublicLayout> <PublicLayout>
<ForgotPassword /> <ForgotPassword />
</PublicLayout> </PublicLayout>
} }
/> />}
<Route <Route
path={URLS.RESET_PASSWORD} path={URLS.RESET_PASSWORD}

View File

@@ -95,11 +95,11 @@ export const defaultTheme = createTheme({
}, },
}, },
h4: { h4: {
fontSize: referenceTheme.typography.pxToRem(32), fontSize: referenceTheme.typography.pxToRem(28),
lineHeight: 1.3, lineHeight: 1.3,
fontWeight: 700, fontWeight: 700,
[referenceTheme.breakpoints.down('sm')]: { [referenceTheme.breakpoints.down('sm')]: {
fontSize: referenceTheme.typography.pxToRem(16), fontSize: referenceTheme.typography.pxToRem(22),
}, },
}, },
h5: { h5: {