Compare commits

..

1 Commits

Author SHA1 Message Date
kasia.oczkowska
b48b2592d5 feat: show api error message when logging in fails 2024-11-13 14:46:21 +00:00
23 changed files with 2150 additions and 1815 deletions

View File

@@ -34,6 +34,7 @@ cd $CURRENT_DIR
echo "Installing and linking dependencies..."
yarn
yarn lerna bootstrap
echo "Migrating database..."
cd packages/backend

View File

@@ -38,7 +38,7 @@ jobs:
cache-dependency-path: yarn.lock
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile
- run: yarn --frozen-lockfile && yarn lerna bootstrap
- run: cd packages/backend && yarn start
env:
ENCRYPTION_KEY: sample_encryption_key
@@ -58,7 +58,7 @@ jobs:
cache-dependency-path: yarn.lock
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile
- run: yarn --frozen-lockfile && yarn lerna bootstrap
- run: cd packages/backend && yarn start:worker
env:
ENCRYPTION_KEY: sample_encryption_key
@@ -78,7 +78,7 @@ jobs:
cache-dependency-path: yarn.lock
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile
- run: yarn --frozen-lockfile && yarn lerna bootstrap
- run: cd packages/web && yarn build
env:
CI: false

View File

@@ -59,7 +59,7 @@ jobs:
with:
node-version: 18
- name: Install dependencies
run: yarn
run: yarn && yarn lerna bootstrap
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
- name: Build Automatisch web

View File

@@ -1,11 +1,13 @@
{
"packages": ["packages/*"],
"version": "0.14.0",
"packages": [
"packages/*"
],
"version": "0.10.0",
"npmClient": "yarn",
"useWorkspaces": true,
"command": {
"add": {
"exact": true
}
},
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
}

View File

@@ -23,7 +23,7 @@
"eslint": "^8.13.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"lerna": "^8.0.0",
"lerna": "^4.0.0",
"prettier": "^2.5.1"
},
"publishConfig": {

View File

@@ -8,7 +8,7 @@ export default {
key: 'instanceUrl',
label: 'WordPress instance URL',
type: 'string',
required: true,
required: false,
readOnly: false,
value: null,
placeholder: null,

View File

@@ -52,7 +52,7 @@ const appConfig = {
isDev: appEnv === 'development',
isTest: appEnv === 'test',
isProd: appEnv === 'production',
version: '0.14.0',
version: '0.13.1',
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
postgresSchema: process.env.POSTGRES_SCHEMA || 'public',
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),

View File

@@ -10,7 +10,7 @@ describe('GET /api/v1/automatisch/version', () => {
const expectedPayload = {
data: {
version: '0.14.0',
version: '0.13.1',
},
meta: {
count: 1,

View File

@@ -1,9 +1,8 @@
import { vi, describe, it, expect } from 'vitest';
import { describe, it, expect } from 'vitest';
import SamlAuthProvider from '../models/saml-auth-provider.ee';
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
import Identity from './identity.ee';
import Base from './base';
import appConfig from '../config/app';
describe('SamlAuthProvider model', () => {
it('tableName should return correct name', () => {
@@ -46,39 +45,4 @@ describe('SamlAuthProvider model', () => {
expect(virtualAttributes).toStrictEqual(expectedAttributes);
});
it('loginUrl should return the URL of login', () => {
const samlAuthProvider = new SamlAuthProvider();
samlAuthProvider.issuer = 'sample-issuer';
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
'https://automatisch.io'
);
expect(samlAuthProvider.loginUrl).toStrictEqual(
'https://automatisch.io/login/saml/sample-issuer'
);
});
it('loginCallbackUrl should return the URL of login callback', () => {
const samlAuthProvider = new SamlAuthProvider();
samlAuthProvider.issuer = 'sample-issuer';
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
'https://automatisch.io'
);
expect(samlAuthProvider.loginCallBackUrl).toStrictEqual(
'https://automatisch.io/login/saml/sample-issuer/callback'
);
});
it('remoteLogoutUrl should return the URL from entrypoint', () => {
const samlAuthProvider = new SamlAuthProvider();
samlAuthProvider.entryPoint = 'https://example.com/saml/logout';
expect(samlAuthProvider.remoteLogoutUrl).toStrictEqual(
'https://example.com/saml/logout'
);
});
});

View File

@@ -580,28 +580,4 @@ describe('User model', () => {
expect(refetchedUser.invitationTokenSentAt).toBe(null);
expect(refetchedUser.status).toBe('active');
});
describe('updatePassword', () => {
it('should update password when the given current password matches with the user password', async () => {
const user = await createUser({ password: 'sample-password' });
const updatedUser = await user.updatePassword({
currentPassword: 'sample-password',
password: 'new-password',
});
expect(await updatedUser.login('new-password')).toBe(true);
});
it('should throw validation error when the given current password does not match with the user password', async () => {
const user = await createUser({ password: 'sample-password' });
await expect(
user.updatePassword({
currentPassword: 'wrong-password',
password: 'new-password',
})
).rejects.toThrowError('currentPassword: is incorrect.');
});
});
});

View File

@@ -112,7 +112,7 @@ export default function ResetPasswordForm() {
<Alert
data-test="accept-invitation-form-error"
severity="error"
sx={{ mt: 1 }}
sx={{ mt: 1, fontWeight: 500 }}
>
{formatMessage('acceptInvitationForm.invalidToken')}
</Alert>

View File

@@ -126,7 +126,7 @@ function AddAppConnection(props) {
</DialogTitle>
{authDocUrl && (
<Alert severity="info">
<Alert severity="info" sx={{ fontWeight: 300 }}>
{formatMessage('addAppConnection.callToDocs', {
appName: name,
docsLink: generateExternalLink(authDocUrl),
@@ -138,7 +138,7 @@ function AddAppConnection(props) {
<Alert
data-test="add-connection-error"
severity="error"
sx={{ mt: 1, wordBreak: 'break-all' }}
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
>
{!errorDetails && errorMessage}
{errorDetails && (

View File

@@ -32,7 +32,10 @@ function AdminApplicationAuthClientDialog(props) {
<Dialog open={true} onClose={onClose}>
<DialogTitle>{title}</DialogTitle>
{error && (
<Alert severity="error" sx={{ mt: 1, wordBreak: 'break-all' }}>
<Alert
severity="error"
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
>
{error.message}
</Alert>
)}

View File

@@ -188,7 +188,7 @@ function InstallationForm() {
)}
/>
{install.isSuccess && (
<Alert data-test="success-alert" severity="success" sx={{ mt: 3 }}>
<Alert data-test="success-alert" severity="success" sx={{ mt: 3, fontWeight: 500 }}>
{formatMessage('installationForm.success', {
link: (str) => (
<Link

View File

@@ -11,19 +11,16 @@ import Form from 'components/Form';
import TextField from 'components/TextField';
import useFormatMessage from 'hooks/useFormatMessage';
import useCreateAccessToken from 'hooks/useCreateAccessToken';
import { Alert } from '@mui/material';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
function LoginForm() {
const isCloud = useCloud();
const navigate = useNavigate();
const formatMessage = useFormatMessage();
const enqueueSnackbar = useEnqueueSnackbar();
const authentication = useAuthentication();
const {
mutateAsync: createAccessToken,
isPending: loading,
error,
isError,
} = useCreateAccessToken();
const { mutateAsync: createAccessToken, isPending: loading } =
useCreateAccessToken();
React.useEffect(() => {
if (authentication.isAuthenticated) {
@@ -40,19 +37,23 @@ function LoginForm() {
});
const { token } = data;
authentication.updateToken(token);
} catch {}
};
} catch (error) {
const errors = error?.response?.data?.errors
? Object.values(error.response.data.errors)
: [];
const renderError = () => {
const errors = error?.response?.data?.errors?.general || [
formatMessage('loginForm.error'),
];
return errors.map((error) => (
<Alert severity="error" sx={{ mt: 2 }}>
{error}
</Alert>
));
if (errors.length) {
for (const [error] of errors) {
enqueueSnackbar(error, {
variant: 'error',
});
}
} else {
enqueueSnackbar(error?.message || formatMessage('loginForm.error'), {
variant: 'error',
});
}
}
};
return (
@@ -105,8 +106,6 @@ function LoginForm() {
</Link>
)}
{isError && renderError()}
<LoadingButton
type="submit"
variant="contained"

View File

@@ -7,7 +7,7 @@ import FormControl from '@mui/material/FormControl';
import SearchIcon from '@mui/icons-material/Search';
import useFormatMessage from 'hooks/useFormatMessage';
export default function SearchInput({ onChange, defaultValue = '' }) {
export default function SearchInput({ onChange }) {
const formatMessage = useFormatMessage();
return (
<FormControl variant="outlined" fullWidth>
@@ -16,7 +16,6 @@ export default function SearchInput({ onChange, defaultValue = '' }) {
</InputLabel>
<OutlinedInput
defaultValue={defaultValue}
id="search-input"
type="text"
size="medium"
@@ -35,5 +34,4 @@ export default function SearchInput({ onChange, defaultValue = '' }) {
SearchInput.propTypes = {
onChange: PropTypes.func,
defaultValue: PropTypes.string,
};

View File

@@ -84,7 +84,10 @@ function TestSubstep(props) {
}}
>
{hasError && (
<Alert severity="error" sx={{ mb: 2, width: '100%' }}>
<Alert
severity="error"
sx={{ mb: 2, fontWeight: 500, width: '100%' }}
>
<pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>
{JSON.stringify(errorDetails, null, 2)}
</pre>
@@ -101,11 +104,13 @@ function TestSubstep(props) {
severity="warning"
sx={{ mb: 1, width: '100%' }}
>
<AlertTitle>
<AlertTitle sx={{ fontWeight: 700 }}>
{formatMessage('flowEditor.noTestDataTitle')}
</AlertTitle>
<Box>{formatMessage('flowEditor.noTestDataMessage')}</Box>
<Box sx={{ fontWeight: 400 }}>
{formatMessage('flowEditor.noTestDataMessage')}
</Box>
</Alert>
)}

View File

@@ -124,6 +124,7 @@ export default function CreateUser() {
<Alert
severity="info"
color="primary"
sx={{ fontWeight: '500' }}
data-test="invitation-email-info-alert"
>
{formatMessage('createUser.invitationEmailInfo', {

View File

@@ -42,9 +42,13 @@ export default function Execution() {
<Grid container item sx={{ mt: 2, mb: [2, 5] }} rowGap={3}>
{!isExecutionStepsLoading && !data?.pages?.[0].data.length && (
<Alert severity="warning" sx={{ flex: 1 }}>
<AlertTitle>{formatMessage('execution.noDataTitle')}</AlertTitle>
<AlertTitle sx={{ fontWeight: 700 }}>
{formatMessage('execution.noDataTitle')}
</AlertTitle>
<Box>{formatMessage('execution.noDataMessage')}</Box>
<Box sx={{ fontWeight: 400 }}>
{formatMessage('execution.noDataMessage')}
</Box>
</Alert>
)}

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { Link, useSearchParams } from 'react-router-dom';
import debounce from 'lodash/debounce';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
@@ -23,18 +23,13 @@ import useLazyFlows from 'hooks/useLazyFlows';
export default function Flows() {
const formatMessage = useFormatMessage();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const page = parseInt(searchParams.get('page') || '', 10) || 1;
const flowName = searchParams.get('flowName') || '';
const [isLoading, setIsLoading] = React.useState(true);
const [flowName, setFlowName] = React.useState('');
const [isLoading, setIsLoading] = React.useState(false);
const currentUserAbility = useCurrentUserAbility();
const {
data,
mutate: fetchFlows,
isSuccess,
} = useLazyFlows(
const { data, mutate: fetchFlows } = useLazyFlows(
{ flowName, page },
{
onSettled: () => {
@@ -43,36 +38,6 @@ export default function Flows() {
},
);
const flows = data?.data || [];
const pageInfo = data?.meta;
const hasFlows = flows?.length;
const navigateToLastPage = isSuccess && !hasFlows && page > 1;
const onSearchChange = React.useCallback((event) => {
setSearchParams({ flowName: event.target.value });
}, []);
const getPathWithSearchParams = (page, flowName) => {
const searchParams = new URLSearchParams();
if (page > 1) {
searchParams.set('page', page);
}
if (flowName) {
searchParams.set('flowName', flowName);
}
return { search: searchParams.toString() };
};
const onDuplicateFlow = () => {
if (pageInfo?.currentPage > 1) {
navigate(getPathWithSearchParams(1, flowName));
} else {
fetchFlows();
}
};
const fetchData = React.useMemo(
() => debounce(fetchFlows, 300),
[fetchFlows],
@@ -89,14 +54,21 @@ export default function Flows() {
}, [fetchData, flowName, page]);
React.useEffect(
function redirectToLastPage() {
if (navigateToLastPage) {
navigate(getPathWithSearchParams(pageInfo.totalPages, flowName));
}
function resetPageOnSearch() {
// reset search params which only consists of `page`
setSearchParams({});
},
[navigateToLastPage],
[flowName],
);
const flows = data?.data || [];
const pageInfo = data?.meta;
const hasFlows = flows?.length;
const onSearchChange = React.useCallback((event) => {
setFlowName(event.target.value);
}, []);
return (
<Box sx={{ py: 3 }}>
<Container>
@@ -106,7 +78,7 @@ export default function Flows() {
</Grid>
<Grid item xs={12} sm="auto" order={{ xs: 2, sm: 1 }}>
<SearchInput onChange={onSearchChange} defaultValue={flowName} />
<SearchInput onChange={onSearchChange} />
</Grid>
<Grid
@@ -139,7 +111,7 @@ export default function Flows() {
</Grid>
<Divider sx={{ mt: [2, 0], mb: 2 }} />
{(isLoading || navigateToLastPage) && (
{isLoading && (
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
)}
{!isLoading &&
@@ -147,11 +119,11 @@ export default function Flows() {
<FlowRow
key={flow.id}
flow={flow}
onDuplicateFlow={onDuplicateFlow}
onDuplicateFlow={fetchFlows}
onDeleteFlow={fetchFlows}
/>
))}
{!isLoading && !navigateToLastPage && !hasFlows && (
{!isLoading && !hasFlows && (
<NoResultFound
text={formatMessage('flows.noFlows')}
{...(currentUserAbility.can('create', 'Flow') && {
@@ -159,23 +131,23 @@ export default function Flows() {
})}
/>
)}
{!isLoading &&
!navigateToLastPage &&
pageInfo &&
pageInfo.totalPages > 1 && (
<Pagination
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
page={pageInfo?.currentPage}
count={pageInfo?.totalPages}
renderItem={(item) => (
<PaginationItem
component={Link}
to={getPathWithSearchParams(item.page, flowName)}
{...item}
/>
)}
/>
)}
{!isLoading && pageInfo && pageInfo.totalPages > 1 && (
<Pagination
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
page={pageInfo?.currentPage}
count={pageInfo?.totalPages}
onChange={(event, page) =>
setSearchParams({ page: page.toString() })
}
renderItem={(item) => (
<PaginationItem
component={Link}
to={`${item.page === 1 ? '' : `?page=${item.page}`}`}
{...item}
/>
)}
/>
)}
</Container>
</Box>
);

View File

@@ -266,8 +266,8 @@ function ProfileSettings() {
</Grid>
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
<Alert variant="outlined" severity="error">
<AlertTitle>
<Alert variant="outlined" severity="error" sx={{ fontWeight: 500 }}>
<AlertTitle sx={{ fontWeight: 700 }}>
{formatMessage('profileSettings.deleteMyAccount')}
</AlertTitle>

View File

@@ -278,20 +278,6 @@ export const defaultTheme = createTheme({
}),
},
},
MuiAlert: {
styleOverrides: {
root: ({ theme }) => ({
fontWeight: theme.typography.fontWeightRegular,
}),
},
},
MuiAlertTitle: {
styleOverrides: {
root: ({ theme }) => ({
fontWeight: theme.typography.fontWeightBold,
}),
},
},
},
});
export const mationTheme = createTheme(

3678
yarn.lock

File diff suppressed because it is too large Load Diff