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
82 changed files with 17846 additions and 20084 deletions

View File

@@ -5,11 +5,8 @@ BACKEND_PORT=3000
WEB_PORT=3001
echo "Configuring backend environment variables..."
cd packages/backend
rm -rf .env
echo "
PORT=$BACKEND_PORT
WEB_APP_URL=http://localhost:$WEB_PORT
@@ -24,35 +21,24 @@ WEBHOOK_SECRET_KEY=sample_webhook_secret_key
APP_SECRET_KEY=sample_app_secret_key
REDIS_HOST=redis
SERVE_WEB_APP_SEPARATELY=true" >> .env
echo "Installing backend dependencies..."
yarn
cd $CURRENT_DIR
echo "Configuring web environment variables..."
cd packages/web
rm -rf .env
echo "
PORT=$WEB_PORT
REACT_APP_BACKEND_URL=http://localhost:$BACKEND_PORT
" >> .env
echo "Installing web dependencies..."
yarn
cd $CURRENT_DIR
echo "Installing and linking dependencies..."
yarn
yarn lerna bootstrap
echo "Migrating database..."
cd packages/backend
yarn db:migrate
yarn db:seed:user
echo "Done!"
echo "Done!"

View File

@@ -1,52 +0,0 @@
name: release-tag
on:
push:
branches:
- 'main'
jobs:
release-image:
runs-on: ubuntu-latest
env:
DOCKER_ORG: groot
DOCKER_LATEST: latest
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@v2
with: # replace it with your local IP
config-inline: |
[registry."git.send.nrw"]
http = true
insecure = true
- name: Login to DockerHub
uses: docker/login-action@v2
with:
registry: git.send.nrw # replace it with your local IP
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Get Meta
id: meta
run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v4
with:
context: ./docker
file: ./docker/Dockerfile.compose
entrypoint: ./docker/compose-entrypoint.sh
platforms: |
linux/amd64
push: true
tags: | # replace it with your local IP and tags
git.send.nrw/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
git.send.nrw/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}

View File

@@ -41,11 +41,8 @@ jobs:
with:
node-version: 18
- name: Install dependencies
run: yarn
working-directory: packages/backend
run: cd packages/backend && yarn
- name: Copy .env-example.test file to .env.test
run: cp .env-example.test .env.test
working-directory: packages/backend
run: cd packages/backend && cp .env-example.test .env.test
- name: Run tests
run: yarn test:coverage
working-directory: packages/backend
run: cd packages/backend && yarn test

View File

@@ -18,13 +18,11 @@ jobs:
with:
node-version: '18'
cache: 'yarn'
cache-dependency-path: packages/backend/yarn.lock
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
working-directory: packages/backend
- run: yarn lint
working-directory: packages/backend
- run: cd packages/backend && yarn lint
- run: echo "🍏 This job's status is ${{ job.status }}."
start-backend-server:
runs-on: ubuntu-latest
@@ -37,13 +35,11 @@ jobs:
with:
node-version: '18'
cache: 'yarn'
cache-dependency-path: packages/backend/yarn.lock
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
working-directory: packages/backend
- run: yarn start
working-directory: packages/backend
- run: yarn --frozen-lockfile && yarn lerna bootstrap
- run: cd packages/backend && yarn start
env:
ENCRYPTION_KEY: sample_encryption_key
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
@@ -59,13 +55,11 @@ jobs:
with:
node-version: '18'
cache: 'yarn'
cache-dependency-path: packages/backend/yarn.lock
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
working-directory: packages/backend
- run: yarn start:worker
working-directory: packages/backend
- run: yarn --frozen-lockfile && yarn lerna bootstrap
- run: cd packages/backend && yarn start:worker
env:
ENCRYPTION_KEY: sample_encryption_key
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
@@ -81,13 +75,11 @@ jobs:
with:
node-version: '18'
cache: 'yarn'
cache-dependency-path: packages/web/yarn.lock
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
working-directory: packages/web
- run: yarn build
working-directory: packages/web
- run: yarn --frozen-lockfile && yarn lerna bootstrap
- run: cd packages/web && yarn build
env:
CI: false
- run: echo "🍏 This job's status is ${{ job.status }}."

View File

@@ -3,13 +3,12 @@ on:
push:
branches:
- main
# TODO: Add pull request after optimizing the total excecution time of the test suite.
# pull_request:
# paths:
# - 'packages/backend/**'
# - 'packages/e2e-tests/**'
# - 'packages/web/**'
# - '!packages/backend/src/apps/**'
pull_request:
paths:
- 'packages/backend/**'
- 'packages/e2e-tests/**'
- 'packages/web/**'
- '!packages/backend/src/apps/**'
workflow_dispatch:
env:
@@ -59,21 +58,13 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install web dependencies
run: yarn
working-directory: ./packages/web
- name: Install backend dependencies
run: yarn
working-directory: ./packages/backend
- name: Install e2e-tests dependencies
run: yarn
working-directory: ./packages/e2e-tests
- name: Install dependencies
run: yarn && yarn lerna bootstrap
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
working-directory: ./packages/e2e-tests
- name: Build Automatisch web
run: yarn build
working-directory: ./packages/web
run: yarn build
env:
# Keep this until we clean up warnings in build processes
CI: false

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@ logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)

View File

@@ -1,7 +1,10 @@
version: '3.9'
services:
main:
image: git.send.nrw/groot/automatisch:latest
build:
context: ./docker
dockerfile: Dockerfile.compose
entrypoint: /compose-entrypoint.sh
ports:
- '3000:3000'
depends_on:
@@ -25,7 +28,10 @@ services:
volumes:
- automatisch_storage:/automatisch/storage
worker:
image: git.send.nrw/groot/automatisch:latest
build:
context: ./docker
dockerfile: Dockerfile.compose
entrypoint: /compose-entrypoint.sh
depends_on:
- main
environment:

View File

@@ -11,12 +11,10 @@ WORKDIR /automatisch
# copy the app, note .dockerignore
COPY . /automatisch
RUN cd packages/web && yarn
RUN yarn
RUN cd packages/web && yarn build
RUN cd packages/backend && yarn --production
RUN \
rm -rf /usr/local/share/.cache/ && \
apk del build-dependencies

13
lerna.json Normal file
View File

@@ -0,0 +1,13 @@
{
"packages": [
"packages/*"
],
"version": "0.10.0",
"npmClient": "yarn",
"useWorkspaces": true,
"command": {
"add": {
"exact": true
}
}
}

32
package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "@automatisch/root",
"license": "See LICENSE file",
"private": true,
"scripts": {
"start": "lerna run --stream --parallel --scope=@*/{web,backend} dev",
"start:web": "lerna run --stream --scope=@*/web dev",
"start:backend": "lerna run --stream --scope=@*/backend dev",
"build:docs": "cd ./packages/docs && yarn install && yarn build"
},
"workspaces": {
"packages": [
"packages/*"
],
"nohoist": [
"**/babel-loader",
"**/webpack",
"**/@automatisch/web",
"**/ajv"
]
},
"devDependencies": {
"eslint": "^8.13.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"lerna": "^4.0.0",
"prettier": "^2.5.1"
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -12,7 +12,6 @@
"pretest": "APP_ENV=test node ./test/setup/prepare-test-env.js",
"test": "APP_ENV=test vitest run",
"test:watch": "APP_ENV=test vitest watch",
"test:coverage": "yarn test --coverage",
"lint": "eslint .",
"db:create": "node ./bin/database/create.js",
"db:seed:user": "node ./bin/database/seed-user.js",
@@ -24,7 +23,6 @@
"dependencies": {
"@bull-board/express": "^3.10.1",
"@casl/ability": "^6.5.0",
"@faker-js/faker": "^9.2.0",
"@node-saml/passport-saml": "^4.0.4",
"@rudderstack/rudder-sdk-node": "^1.1.2",
"@sentry/node": "^7.42.0",
@@ -38,9 +36,6 @@
"crypto-js": "^4.1.1",
"debug": "~2.6.9",
"dotenv": "^10.0.0",
"eslint": "^8.13.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"express": "~4.18.2",
"express-async-errors": "^3.1.1",
"express-basic-auth": "^1.2.1",
@@ -66,7 +61,6 @@
"pg": "^8.7.1",
"php-serialize": "^4.0.2",
"pluralize": "^8.0.0",
"prettier": "^2.5.1",
"raw-body": "^2.5.2",
"showdown": "^2.1.0",
"uuid": "^9.0.1",
@@ -98,11 +92,10 @@
"url": "https://github.com/automatisch/automatisch/issues"
},
"devDependencies": {
"@vitest/coverage-v8": "^2.1.5",
"node-gyp": "^10.1.0",
"nodemon": "^2.0.13",
"supertest": "^6.3.3",
"vitest": "^2.1.5"
"vitest": "^1.1.3"
},
"publishConfig": {
"access": "public"

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

@@ -7,7 +7,7 @@ export default async (request, response) => {
.throwIfNotFound();
const roleMappings = await samlAuthProvider
.$relatedQuery('roleMappings')
.$relatedQuery('samlAuthProvidersRoleMappings')
.orderBy('remote_role_name', 'asc');
renderObject(response, roleMappings);

View File

@@ -8,14 +8,15 @@ export default async (request, response) => {
.findById(samlAuthProviderId)
.throwIfNotFound();
const roleMappings = await samlAuthProvider.updateRoleMappings(
roleMappingsParams(request)
);
const samlAuthProvidersRoleMappings =
await samlAuthProvider.updateRoleMappings(
samlAuthProvidersRoleMappingsParams(request)
);
renderObject(response, roleMappings);
renderObject(response, samlAuthProvidersRoleMappings);
};
const roleMappingsParams = (request) => {
const samlAuthProvidersRoleMappingsParams = (request) => {
const roleMappings = request.body;
return roleMappings.map(({ roleId, remoteRoleName }) => ({

View File

@@ -6,7 +6,7 @@ import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by
import { createRole } from '../../../../../../test/factories/role.js';
import { createUser } from '../../../../../../test/factories/user.js';
import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js';
import { createRoleMapping } from '../../../../../../test/factories/role-mapping.js';
import { createSamlAuthProvidersRoleMapping } from '../../../../../../test/factories/saml-auth-providers-role-mapping.js';
import createRoleMappingsMock from '../../../../../../test/mocks/rest/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js';
import * as license from '../../../../../helpers/license.ee.js';
@@ -21,12 +21,12 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi
samlAuthProvider = await createSamlAuthProvider();
await createRoleMapping({
await createSamlAuthProvidersRoleMapping({
samlAuthProviderId: samlAuthProvider.id,
remoteRoleName: 'Viewer',
});
await createRoleMapping({
await createSamlAuthProvidersRoleMapping({
samlAuthProviderId: samlAuthProvider.id,
remoteRoleName: 'Editor',
});
@@ -64,7 +64,7 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi
it('should delete role mappings when given empty role mappings', async () => {
const existingRoleMappings = await samlAuthProvider.$relatedQuery(
'roleMappings'
'samlAuthProvidersRoleMappings'
);
expect(existingRoleMappings.length).toBe(2);
@@ -149,4 +149,34 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi
.send(roleMappings)
.expect(404);
});
it('should not delete existing role mapping when error thrown', async () => {
const roleMappings = [
{
roleId: userRole.id,
remoteRoleName: {
invalid: 'data',
},
},
];
const roleMappingsBeforeRequest = await samlAuthProvider.$relatedQuery(
'samlAuthProvidersRoleMappings'
);
await request(app)
.patch(
`/api/v1/admin/saml-auth-providers/${samlAuthProvider.id}/role-mappings`
)
.set('Authorization', token)
.send(roleMappings)
.expect(422);
const roleMappingsAfterRequest = await samlAuthProvider.$relatedQuery(
'samlAuthProvidersRoleMappings'
);
expect(roleMappingsBeforeRequest).toStrictEqual(roleMappingsAfterRequest);
expect(roleMappingsAfterRequest.length).toBe(2);
});
});

View File

@@ -87,14 +87,14 @@ describe('GET /api/v1/apps/:appKey/connections', () => {
it('should return not found response for invalid connection UUID', async () => {
await createPermission({
action: 'read',
action: 'update',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await request(app)
.get('/api/v1/apps/invalid-connection-id/connections')
.get('/api/v1/connections/invalid-connection-id/connections')
.set('Authorization', token)
.expect(404);
});

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

@@ -193,7 +193,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
const notExistingStepUUID = Crypto.randomUUID();
await request(app)
.post(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`)
.get(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`)
.set('Authorization', token)
.expect(404);
});
@@ -216,7 +216,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
const step = await createStep({ appKey: null });
await request(app)
.post(`/api/v1/steps/${step.id}/dynamic-data`)
.get(`/api/v1/steps/${step.id}/dynamic-data`)
.set('Authorization', token)
.expect(404);
});

View File

@@ -118,7 +118,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
const notExistingStepUUID = Crypto.randomUUID();
await request(app)
.post(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`)
.get(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`)
.set('Authorization', token)
.expect(404);
});
@@ -138,11 +138,10 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
conditions: [],
});
const step = await createStep();
await step.$query().patch({ appKey: null });
const step = await createStep({ appKey: null });
await request(app)
.post(`/api/v1/steps/${step.id}/dynamic-fields`)
.get(`/api/v1/steps/${step.id}/dynamic-fields`)
.set('Authorization', token)
.expect(404);
});

View File

@@ -1,52 +0,0 @@
export async function up(knex) {
await knex.schema.createTable('role_mappings', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table
.uuid('saml_auth_provider_id')
.references('id')
.inTable('saml_auth_providers');
table.uuid('role_id').references('id').inTable('roles');
table.string('remote_role_name').notNullable();
table.unique(['saml_auth_provider_id', 'remote_role_name']);
table.timestamps(true, true);
});
const existingRoleMappings = await knex('saml_auth_providers_role_mappings');
if (existingRoleMappings.length) {
await knex('role_mappings').insert(existingRoleMappings);
}
return await knex.schema.dropTable('saml_auth_providers_role_mappings');
}
export async function down(knex) {
await knex.schema.createTable(
'saml_auth_providers_role_mappings',
(table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table
.uuid('saml_auth_provider_id')
.references('id')
.inTable('saml_auth_providers');
table.uuid('role_id').references('id').inTable('roles');
table.string('remote_role_name').notNullable();
table.unique(['saml_auth_provider_id', 'remote_role_name']);
table.timestamps(true, true);
}
);
const existingRoleMappings = await knex('role_mappings');
if (existingRoleMappings.length) {
await knex('saml_auth_providers_role_mappings').insert(
existingRoleMappings
);
}
return await knex.schema.dropTable('role_mappings');
}

View File

@@ -30,7 +30,7 @@ const findOrCreateUserBySamlIdentity = async (
: [mappedUser.role];
const samlAuthProviderRoleMapping = await samlAuthProvider
.$relatedQuery('roleMappings')
.$relatedQuery('samlAuthProvidersRoleMappings')
.whereIn('remote_role_name', mappedRoles)
.limit(1)
.first();

View File

@@ -1,46 +0,0 @@
import { describe, expect, it } from 'vitest';
import userAbility from './user-ability.js';
describe('userAbility', () => {
it('should return PureAbility instantiated with user permissions', () => {
const user = {
permissions: [
{
subject: 'Flow',
action: 'read',
conditions: ['isCreator'],
},
],
role: {
name: 'User',
},
};
const ability = userAbility(user);
expect(ability.rules).toStrictEqual(user.permissions);
});
it('should return permission-less PureAbility for user with no role', () => {
const user = {
permissions: [
{
subject: 'Flow',
action: 'read',
conditions: ['isCreator'],
},
],
role: null,
};
const ability = userAbility(user);
expect(ability.rules).toStrictEqual([]);
});
it('should return permission-less PureAbility for user with no permissions', () => {
const user = { permissions: null, role: { name: 'User' } };
const ability = userAbility(user);
expect(ability.rules).toStrictEqual([]);
});
});

View File

@@ -1,30 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`RoleMapping model > jsonSchema should have the correct schema 1`] = `
{
"properties": {
"id": {
"format": "uuid",
"type": "string",
},
"remoteRoleName": {
"minLength": 1,
"type": "string",
},
"roleId": {
"format": "uuid",
"type": "string",
},
"samlAuthProviderId": {
"format": "uuid",
"type": "string",
},
},
"required": [
"samlAuthProviderId",
"roleId",
"remoteRoleName",
],
"type": "object",
}
`;

View File

@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`RoleMapping model > jsonSchema should have the correct schema 1`] = `
exports[`SamlAuthProvidersRoleMapping model > jsonSchema should have the correct schema 1`] = `
{
"properties": {
"id": {

View File

@@ -5,7 +5,7 @@ import appConfig from '../config/app.js';
import axios from '../helpers/axios-with-proxy.js';
import Base from './base.js';
import Identity from './identity.ee.js';
import RoleMapping from './role-mapping.ee.js';
import SamlAuthProvidersRoleMapping from './saml-auth-providers-role-mapping.ee.js';
class SamlAuthProvider extends Base {
static tableName = 'saml_auth_providers';
@@ -53,12 +53,12 @@ class SamlAuthProvider extends Base {
to: 'saml_auth_providers.id',
},
},
roleMappings: {
samlAuthProvidersRoleMappings: {
relation: Base.HasManyRelation,
modelClass: RoleMapping,
modelClass: SamlAuthProvidersRoleMapping,
join: {
from: 'saml_auth_providers.id',
to: 'role_mappings.saml_auth_provider_id',
to: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
},
},
});
@@ -133,22 +133,27 @@ class SamlAuthProvider extends Base {
}
async updateRoleMappings(roleMappings) {
await this.$relatedQuery('roleMappings').delete();
return await SamlAuthProvider.transaction(async (trx) => {
await this.$relatedQuery('samlAuthProvidersRoleMappings', trx).delete();
if (isEmpty(roleMappings)) {
return [];
}
if (isEmpty(roleMappings)) {
return [];
}
const roleMappingsData = roleMappings.map((roleMapping) => ({
...roleMapping,
samlAuthProviderId: this.id,
}));
const samlAuthProvidersRoleMappingsData = roleMappings.map(
(samlAuthProvidersRoleMapping) => ({
...samlAuthProvidersRoleMapping,
samlAuthProviderId: this.id,
})
);
const newRoleMappings = await RoleMapping.query().insertAndFetch(
roleMappingsData
);
const samlAuthProvidersRoleMappings =
await SamlAuthProvidersRoleMapping.query(trx).insertAndFetch(
samlAuthProvidersRoleMappingsData
);
return newRoleMappings;
return samlAuthProvidersRoleMappings;
});
}
}

View File

@@ -1,14 +1,8 @@
import { vi, beforeEach, describe, it, expect } from 'vitest';
import { v4 as uuidv4 } from 'uuid';
import { describe, it, expect } from 'vitest';
import SamlAuthProvider from '../models/saml-auth-provider.ee';
import RoleMapping from '../models/role-mapping.ee';
import axios from '../helpers/axios-with-proxy.js';
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
import Identity from './identity.ee';
import Base from './base';
import appConfig from '../config/app';
import { createSamlAuthProvider } from '../../test/factories/saml-auth-provider.ee.js';
import { createRoleMapping } from '../../test/factories/role-mapping.js';
import { createRole } from '../../test/factories/role.js';
describe('SamlAuthProvider model', () => {
it('tableName should return correct name', () => {
@@ -31,12 +25,12 @@ describe('SamlAuthProvider model', () => {
to: 'saml_auth_providers.id',
},
},
roleMappings: {
samlAuthProvidersRoleMappings: {
relation: Base.HasManyRelation,
modelClass: RoleMapping,
modelClass: SamlAuthProvidersRoleMapping,
join: {
from: 'saml_auth_providers.id',
to: 'role_mappings.saml_auth_provider_id',
to: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
},
},
};
@@ -51,181 +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'
);
});
it('config should return the correct configuration object', () => {
const samlAuthProvider = new SamlAuthProvider();
samlAuthProvider.certificate = 'sample-certificate';
samlAuthProvider.signatureAlgorithm = 'sha256';
samlAuthProvider.entryPoint = 'https://example.com/saml';
samlAuthProvider.issuer = 'sample-issuer';
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
'https://automatisch.io'
);
const expectedConfig = {
callbackUrl: 'https://automatisch.io/login/saml/sample-issuer/callback',
cert: 'sample-certificate',
entryPoint: 'https://example.com/saml',
issuer: 'sample-issuer',
signatureAlgorithm: 'sha256',
logoutUrl: 'https://example.com/saml',
};
expect(samlAuthProvider.config).toStrictEqual(expectedConfig);
});
it('generateLogoutRequestBody should return a correctly encoded SAML logout request', () => {
vi.mock('uuid', () => ({
v4: vi.fn(),
}));
const samlAuthProvider = new SamlAuthProvider();
samlAuthProvider.entryPoint = 'https://example.com/saml';
samlAuthProvider.issuer = 'sample-issuer';
const mockUuid = '123e4567-e89b-12d3-a456-426614174000';
uuidv4.mockReturnValue(mockUuid);
const sessionId = 'test-session-id';
const logoutRequest = samlAuthProvider.generateLogoutRequestBody(sessionId);
const expectedLogoutRequest = `
<samlp:LogoutRequest
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
ID="${mockUuid}"
Version="2.0"
IssueInstant="${new Date().toISOString()}"
Destination="https://example.com/saml">
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">sample-issuer</saml:Issuer>
<samlp:SessionIndex>test-session-id</samlp:SessionIndex>
</samlp:LogoutRequest>
`;
const expectedEncodedRequest = Buffer.from(expectedLogoutRequest).toString(
'base64'
);
expect(logoutRequest).toBe(expectedEncodedRequest);
});
it('terminateRemoteSession should send the correct POST request and return the response', async () => {
vi.mock('../helpers/axios-with-proxy.js', () => ({
default: {
post: vi.fn(),
},
}));
const samlAuthProvider = new SamlAuthProvider();
samlAuthProvider.entryPoint = 'https://example.com/saml';
samlAuthProvider.generateLogoutRequestBody = vi
.fn()
.mockReturnValue('mockEncodedLogoutRequest');
const sessionId = 'test-session-id';
const mockResponse = { data: 'Logout Successful' };
axios.post.mockResolvedValue(mockResponse);
const response = await samlAuthProvider.terminateRemoteSession(sessionId);
expect(samlAuthProvider.generateLogoutRequestBody).toHaveBeenCalledWith(
sessionId
);
expect(axios.post).toHaveBeenCalledWith(
'https://example.com/saml',
'SAMLRequest=mockEncodedLogoutRequest',
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
expect(response).toBe(mockResponse);
});
describe('updateRoleMappings', () => {
let samlAuthProvider;
beforeEach(async () => {
samlAuthProvider = await createSamlAuthProvider();
});
it('should remove all existing role mappings', async () => {
await createRoleMapping({
samlAuthProviderId: samlAuthProvider.id,
remoteRoleName: 'Admin',
});
await createRoleMapping({
samlAuthProviderId: samlAuthProvider.id,
remoteRoleName: 'User',
});
await samlAuthProvider.updateRoleMappings([]);
const roleMappings = await samlAuthProvider.$relatedQuery('roleMappings');
expect(roleMappings).toStrictEqual([]);
});
it('should return the updated role mappings when new ones are provided', async () => {
const adminRole = await createRole({ name: 'Admin' });
const userRole = await createRole({ name: 'User' });
const newRoleMappings = [
{ remoteRoleName: 'Admin', roleId: adminRole.id },
{ remoteRoleName: 'User', roleId: userRole.id },
];
const result = await samlAuthProvider.updateRoleMappings(newRoleMappings);
const refetchedRoleMappings = await samlAuthProvider.$relatedQuery(
'roleMappings'
);
expect(result).toStrictEqual(refetchedRoleMappings);
});
});
});

View File

@@ -1,8 +1,8 @@
import Base from './base.js';
import SamlAuthProvider from './saml-auth-provider.ee.js';
class RoleMapping extends Base {
static tableName = 'role_mappings';
class SamlAuthProvidersRoleMapping extends Base {
static tableName = 'saml_auth_providers_role_mappings';
static jsonSchema = {
type: 'object',
@@ -21,11 +21,11 @@ class RoleMapping extends Base {
relation: Base.BelongsToOneRelation,
modelClass: SamlAuthProvider,
join: {
from: 'role_mappings.saml_auth_provider_id',
from: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
to: 'saml_auth_providers.id',
},
},
});
}
export default RoleMapping;
export default SamlAuthProvidersRoleMapping;

View File

@@ -1,26 +1,28 @@
import { describe, it, expect } from 'vitest';
import RoleMapping from './role-mapping.ee';
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
import SamlAuthProvider from './saml-auth-provider.ee';
import Base from './base';
describe('RoleMapping model', () => {
describe('SamlAuthProvidersRoleMapping model', () => {
it('tableName should return correct name', () => {
expect(RoleMapping.tableName).toBe('role_mappings');
expect(SamlAuthProvidersRoleMapping.tableName).toBe(
'saml_auth_providers_role_mappings'
);
});
it('jsonSchema should have the correct schema', () => {
expect(RoleMapping.jsonSchema).toMatchSnapshot();
expect(SamlAuthProvidersRoleMapping.jsonSchema).toMatchSnapshot();
});
it('relationMappings should return correct associations', () => {
const relationMappings = RoleMapping.relationMappings();
const relationMappings = SamlAuthProvidersRoleMapping.relationMappings();
const expectedRelations = {
samlAuthProvider: {
relation: Base.BelongsToOneRelation,
modelClass: SamlAuthProvider,
join: {
from: 'role_mappings.saml_auth_provider_id',
from: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
to: 'saml_auth_providers.id',
},
},

View File

@@ -212,10 +212,6 @@ class User extends Base {
return `${appConfig.webAppUrl}/accept-invitation?token=${this.invitationToken}`;
}
get ability() {
return userAbility(this);
}
static async authenticate(email, password) {
const user = await User.query().findOne({
email: email?.toLowerCase() || null,
@@ -411,7 +407,7 @@ class User extends Base {
}
}
startTrialPeriod() {
async startTrialPeriod() {
this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
}
@@ -587,6 +583,62 @@ class User extends Base {
return user;
}
async $beforeInsert(queryContext) {
await super.$beforeInsert(queryContext);
this.email = this.email.toLowerCase();
await this.generateHash();
if (appConfig.isCloud) {
await this.startTrialPeriod();
}
}
async $beforeUpdate(opt, queryContext) {
await super.$beforeUpdate(opt, queryContext);
if (this.email) {
this.email = this.email.toLowerCase();
}
await this.generateHash();
}
async $afterInsert(queryContext) {
await super.$afterInsert(queryContext);
if (appConfig.isCloud) {
await this.$relatedQuery('usageData').insert({
userId: this.id,
consumedTaskCount: 0,
nextResetAt: DateTime.now().plus({ days: 30 }).toISODate(),
});
}
}
async $afterFind() {
if (await hasValidLicense()) return this;
if (Array.isArray(this.permissions)) {
this.permissions = this.permissions.filter((permission) => {
const restrictedSubjects = [
'App',
'Role',
'SamlAuthProvider',
'Config',
];
return !restrictedSubjects.includes(permission.subject);
});
}
return this;
}
get ability() {
return userAbility(this);
}
can(action, subject) {
const can = this.ability.can(action, subject);
@@ -602,68 +654,12 @@ class User extends Base {
return conditionMap;
}
lowercaseEmail() {
if (this.email) {
this.email = this.email.toLowerCase();
}
}
cannot(action, subject) {
const cannot = this.ability.cannot(action, subject);
async createUsageData() {
if (appConfig.isCloud) {
return await this.$relatedQuery('usageData').insertAndFetch({
userId: this.id,
consumedTaskCount: 0,
nextResetAt: DateTime.now().plus({ days: 30 }).toISODate(),
});
}
}
if (cannot) throw new NotAuthorizedError();
async omitEnterprisePermissionsWithoutValidLicense() {
if (await hasValidLicense()) {
return this;
}
if (Array.isArray(this.permissions)) {
this.permissions = this.permissions.filter((permission) => {
const restrictedSubjects = [
'App',
'Role',
'SamlAuthProvider',
'Config',
];
return !restrictedSubjects.includes(permission.subject);
});
}
}
async $beforeInsert(queryContext) {
await super.$beforeInsert(queryContext);
this.lowercaseEmail();
await this.generateHash();
if (appConfig.isCloud) {
this.startTrialPeriod();
}
}
async $beforeUpdate(opt, queryContext) {
await super.$beforeUpdate(opt, queryContext);
this.lowercaseEmail();
await this.generateHash();
}
async $afterInsert(queryContext) {
await super.$afterInsert(queryContext);
await this.createUsageData();
}
async $afterFind() {
await this.omitEnterprisePermissionsWithoutValidLicense();
return cannot;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,31 @@
import { generateQueue } from './queue.js';
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);
process.on('SIGTERM', async () => {
await actionQueue.close();
});
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);
});
const actionQueue = generateQueue('action');
export default actionQueue;

View File

@@ -1,4 +1,31 @@
import { generateQueue } from './queue.js';
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);
process.on('SIGTERM', async () => {
await deleteUserQueue.close();
});
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);
});
const deleteUserQueue = generateQueue('delete-user');
export default deleteUserQueue;

View File

@@ -1,4 +1,31 @@
import { generateQueue } from './queue.js';
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);
process.on('SIGTERM', async () => {
await emailQueue.close();
});
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);
});
const emailQueue = generateQueue('email');
export default emailQueue;

View File

@@ -1,4 +1,31 @@
import { generateQueue } from './queue.js';
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);
process.on('SIGTERM', async () => {
await flowQueue.close();
});
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);
});
const flowQueue = generateQueue('flow');
export default flowQueue;

View File

@@ -1,21 +0,0 @@
import appConfig from '../config/app.js';
import actionQueue from './action.js';
import emailQueue from './email.js';
import flowQueue from './flow.js';
import triggerQueue from './trigger.js';
import deleteUserQueue from './delete-user.ee.js';
import removeCancelledSubscriptionsQueue from './remove-cancelled-subscriptions.ee.js';
const queues = [
actionQueue,
emailQueue,
flowQueue,
triggerQueue,
deleteUserQueue,
];
if (appConfig.isCloud) {
queues.push(removeCancelledSubscriptionsQueue);
}
export default queues;

View File

@@ -1,44 +0,0 @@
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,
},
});
};

View File

@@ -1,8 +1,44 @@
import { generateQueue } from './queue.js';
import process from 'process';
import { Queue } from 'bullmq';
import redisConfig from '../config/redis.js';
import logger from '../helpers/logger.js';
const removeCancelledSubscriptionsQueue = generateQueue(
const CONNECTION_REFUSED = 'ECONNREFUSED';
const redisConnection = {
connection: redisConfig,
};
const removeCancelledSubscriptionsQueue = new Queue(
'remove-cancelled-subscriptions',
{ runDaily: true }
redisConnection
);
process.on('SIGTERM', async () => {
await removeCancelledSubscriptionsQueue.close();
});
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;

View File

@@ -1,4 +1,31 @@
import { generateQueue } from './queue.js';
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);
process.on('SIGTERM', async () => {
await triggerQueue.close();
});
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);
});
const triggerQueue = generateQueue('trigger');
export default triggerQueue;

View File

@@ -26,7 +26,7 @@ const serializers = {
Permission: permissionSerializer,
AdminSamlAuthProvider: adminSamlAuthProviderSerializer,
SamlAuthProvider: samlAuthProviderSerializer,
RoleMapping: samlAuthProviderRoleMappingSerializer,
SamlAuthProvidersRoleMapping: samlAuthProviderRoleMappingSerializer,
AppAuthClient: appAuthClientSerializer,
AppConfig: appConfigSerializer,
Flow: flowSerializer,

View File

@@ -1,22 +1,20 @@
import * as Sentry from './helpers/sentry.ee.js';
import process from 'node:process';
import appConfig from './config/app.js';
Sentry.init();
import './config/orm.js';
import './helpers/check-worker-readiness.js';
import queues from './queues/index.js';
import workers from './workers/index.js';
import './workers/flow.js';
import './workers/trigger.js';
import './workers/action.js';
import './workers/email.js';
import './workers/delete-user.ee.js';
process.on('SIGTERM', async () => {
for (const queue of queues) {
await queue.close();
}
for (const worker of workers) {
await worker.close();
}
});
if (appConfig.isCloud) {
import('./workers/remove-cancelled-subscriptions.ee.js');
import('./queues/remove-cancelled-subscriptions.ee.js');
}
import telemetry from './helpers/telemetry/index.js';

View File

@@ -1,4 +1,5 @@
import { Worker } from 'bullmq';
import process from 'node:process';
import * as Sentry from '../helpers/sentry.ee.js';
import redisConfig from '../config/redis.js';
@@ -14,7 +15,7 @@ import delayAsMilliseconds from '../helpers/delay-as-milliseconds.js';
const DEFAULT_DELAY_DURATION = 0;
const actionWorker = new Worker(
export const worker = new Worker(
'action',
async (job) => {
const { stepId, flowId, executionId, computedParameters, executionStep } =
@@ -54,11 +55,11 @@ const actionWorker = new Worker(
{ connection: redisConfig }
);
actionWorker.on('completed', (job) => {
worker.on('completed', (job) => {
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
});
actionWorker.on('failed', (job, err) => {
worker.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}
@@ -73,4 +74,6 @@ actionWorker.on('failed', (job, err) => {
});
});
export default actionWorker;
process.on('SIGTERM', async () => {
await worker.close();
});

View File

@@ -1,4 +1,5 @@
import { Worker } from 'bullmq';
import process from 'node:process';
import * as Sentry from '../helpers/sentry.ee.js';
import redisConfig from '../config/redis.js';
@@ -7,7 +8,7 @@ import appConfig from '../config/app.js';
import User from '../models/user.js';
import ExecutionStep from '../models/execution-step.js';
const deleteUserWorker = new Worker(
export const worker = new Worker(
'delete-user',
async (job) => {
const { id } = job.data;
@@ -45,13 +46,13 @@ const deleteUserWorker = new Worker(
{ connection: redisConfig }
);
deleteUserWorker.on('completed', (job) => {
worker.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) => {
worker.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}
@@ -66,4 +67,6 @@ deleteUserWorker.on('failed', (job, err) => {
});
});
export default deleteUserWorker;
process.on('SIGTERM', async () => {
await worker.close();
});

View File

@@ -1,4 +1,5 @@
import { Worker } from 'bullmq';
import process from 'node:process';
import * as Sentry from '../helpers/sentry.ee.js';
import redisConfig from '../config/redis.js';
@@ -15,7 +16,7 @@ const isAutomatischEmail = (email) => {
return email.endsWith('@automatisch.io');
};
const emailWorker = new Worker(
export const worker = new Worker(
'email',
async (job) => {
const { email, subject, template, params } = job.data;
@@ -38,13 +39,13 @@ const emailWorker = new Worker(
{ connection: redisConfig }
);
emailWorker.on('completed', (job) => {
worker.on('completed', (job) => {
logger.info(
`JOB ID: ${job.id} - ${job.data.subject} email sent to ${job.data.email}!`
);
});
emailWorker.on('failed', (job, err) => {
worker.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}
@@ -59,4 +60,6 @@ emailWorker.on('failed', (job, err) => {
});
});
export default emailWorker;
process.on('SIGTERM', async () => {
await worker.close();
});

View File

@@ -1,4 +1,5 @@
import { Worker } from 'bullmq';
import process from 'node:process';
import * as Sentry from '../helpers/sentry.ee.js';
import redisConfig from '../config/redis.js';
@@ -12,7 +13,7 @@ import {
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
} from '../helpers/remove-job-configuration.js';
const flowWorker = new Worker(
export const worker = new Worker(
'flow',
async (job) => {
const { flowId } = job.data;
@@ -63,11 +64,11 @@ const flowWorker = new Worker(
{ connection: redisConfig }
);
flowWorker.on('completed', (job) => {
worker.on('completed', (job) => {
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
});
flowWorker.on('failed', async (job, err) => {
worker.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}
@@ -94,4 +95,6 @@ flowWorker.on('failed', async (job, err) => {
});
});
export default flowWorker;
process.on('SIGTERM', async () => {
await worker.close();
});

View File

@@ -1,21 +0,0 @@
import appConfig from '../config/app.js';
import actionWorker from './action.js';
import emailWorker from './email.js';
import flowWorker from './flow.js';
import triggerWorker from './trigger.js';
import deleteUserWorker from './delete-user.ee.js';
import removeCancelledSubscriptionsWorker from './remove-cancelled-subscriptions.ee.js';
const workers = [
actionWorker,
emailWorker,
flowWorker,
triggerWorker,
deleteUserWorker,
];
if (appConfig.isCloud) {
workers.push(removeCancelledSubscriptionsWorker);
}
export default workers;

View File

@@ -1,11 +1,12 @@
import { Worker } from 'bullmq';
import process from 'node:process';
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';
const removeCancelledSubscriptionsWorker = new Worker(
export const worker = new Worker(
'remove-cancelled-subscriptions',
async () => {
await Subscription.query()
@@ -22,13 +23,13 @@ const removeCancelledSubscriptionsWorker = new Worker(
{ connection: redisConfig }
);
removeCancelledSubscriptionsWorker.on('completed', (job) => {
worker.on('completed', (job) => {
logger.info(
`JOB ID: ${job.id} - The cancelled subscriptions have been removed!`
);
});
removeCancelledSubscriptionsWorker.on('failed', (job, err) => {
worker.on('failed', (job, err) => {
const errorMessage = `
JOB ID: ${job.id} - ERROR: The cancelled subscriptions can not be removed! ${err.message}
\n ${err.stack}
@@ -41,4 +42,6 @@ removeCancelledSubscriptionsWorker.on('failed', (job, err) => {
});
});
export default removeCancelledSubscriptionsWorker;
process.on('SIGTERM', async () => {
await worker.close();
});

View File

@@ -1,4 +1,5 @@
import { Worker } from 'bullmq';
import process from 'node:process';
import * as Sentry from '../helpers/sentry.ee.js';
import redisConfig from '../config/redis.js';
@@ -11,7 +12,7 @@ import {
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
} from '../helpers/remove-job-configuration.js';
const triggerWorker = new Worker(
export const worker = new Worker(
'trigger',
async (job) => {
const { flowId, executionId, stepId, executionStep } = await processTrigger(
@@ -40,11 +41,11 @@ const triggerWorker = new Worker(
{ connection: redisConfig }
);
triggerWorker.on('completed', (job) => {
worker.on('completed', (job) => {
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
});
triggerWorker.on('failed', (job, err) => {
worker.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}
@@ -59,4 +60,6 @@ triggerWorker.on('failed', (job, err) => {
});
});
export default triggerWorker;
process.on('SIGTERM', async () => {
await worker.close();
});

View File

@@ -1,15 +1,16 @@
import { faker } from '@faker-js/faker';
import { createRole } from './role.js';
import RoleMapping from '../../src/models/role-mapping.ee.js';
import { createSamlAuthProvider } from './saml-auth-provider.ee.js';
import SamlAuthProviderRoleMapping from '../../src/models/saml-auth-providers-role-mapping.ee.js';
export const createRoleMapping = async (params = {}) => {
params.roleId = params.roleId || (await createRole()).id;
params.roleId = params?.roleId || (await createRole()).id;
params.samlAuthProviderId =
params.samlAuthProviderId || (await createSamlAuthProvider()).id;
params.remoteRoleName = params.remoteRoleName || faker.person.jobType();
params?.samlAuthProviderId || (await createSamlAuthProvider()).id;
const roleMapping = await RoleMapping.query().insertAndFetch(params);
params.remoteRoleName = params?.remoteRoleName || 'User';
return roleMapping;
const samlAuthProviderRoleMapping =
await SamlAuthProviderRoleMapping.query().insertAndFetch(params);
return samlAuthProviderRoleMapping;
};

View File

@@ -0,0 +1,16 @@
import { faker } from '@faker-js/faker';
import { createRole } from './role.js';
import SamlAuthProvidersRoleMapping from '../../src/models/saml-auth-providers-role-mapping.ee.js';
import { createSamlAuthProvider } from './saml-auth-provider.ee.js';
export const createSamlAuthProvidersRoleMapping = async (params = {}) => {
params.roleId = params.roleId || (await createRole()).id;
params.samlAuthProviderId =
params.samlAuthProviderId || (await createSamlAuthProvider()).id;
params.remoteRoleName = params.remoteRoleName || faker.person.jobType();
const samlAuthProvider =
await SamlAuthProvidersRoleMapping.query().insertAndFetch(params);
return samlAuthProvider;
};

View File

@@ -15,7 +15,7 @@ const getRoleMappingsMock = async (roleMappings) => {
currentPage: null,
isArray: true,
totalPages: null,
type: 'RoleMapping',
type: 'SamlAuthProvidersRoleMapping',
},
};
};

View File

@@ -15,7 +15,7 @@ const createRoleMappingsMock = async (roleMappings) => {
currentPage: null,
isArray: true,
totalPages: null,
type: 'RoleMapping',
type: 'SamlAuthProvidersRoleMapping',
},
};
};

View File

@@ -2,25 +2,8 @@ import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
root: './',
environment: 'node',
setupFiles: ['./test/setup/global-hooks.js'],
globals: true,
reporters: process.env.GITHUB_ACTIONS ? ['dot', 'github-actions'] : ['dot'],
coverage: {
reportOnFailure: true,
provider: 'v8',
reportsDirectory: './coverage',
reporter: ['text', 'lcov'],
all: true,
include: ['**/src/models/**', '**/src/controllers/**'],
thresholds: {
autoUpdate: true,
statements: 93.41,
branches: 93.46,
functions: 95.95,
lines: 93.41,
},
},
},
});

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
pages/.vitepress/cache

View File

@@ -4,7 +4,6 @@
"license": "See LICENSE file",
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
"private": true,
"type": "module",
"scripts": {
"dev": "vitepress dev pages --port 3002",
"build": "vitepress build pages",

View File

@@ -6,19 +6,11 @@ Clone main branch of Automatisch.
git clone git@github.com:automatisch/automatisch.git
```
Then, install the dependencies for both backend and web packages separately.
Then, install the dependencies.
```bash
cd automatisch
# Install backend dependencies
cd packages/backend
yarn install
# Install web dependencies
cd packages/web
yarn install
```
## Backend
@@ -61,14 +53,12 @@ yarn db:seed:user
Start the main backend server.
```bash
cd packages/backend
yarn dev
```
Start the worker server in another terminal tab.
```bash
cd packages/backend
yarn worker
```
@@ -94,7 +84,6 @@ It will automatically open [http://localhost:3001](http://localhost:3001) in you
```bash
cd packages/docs
yarn install
yarn dev
```

View File

@@ -1,6 +1,6 @@
# Repository Structure
We manage a monorepo structure with the following packages:
We use `lerna` with `yarn workspaces` to manage the mono repository. We have the following packages:
```
.
@@ -15,5 +15,3 @@ We manage a monorepo structure with the following packages:
- `docs` - The docs package contains the documentation website.
- `e2e-tests` - The e2e-tests package contains the end-to-end tests for the internal usage.
- `web` - The web package contains the frontend application of Automatisch.
Each package is independently managed, and has its own package.json file to manage dependencies. This allows for better isolation and flexibility.

File diff suppressed because it is too large Load Diff

View File

@@ -29,12 +29,10 @@
"@playwright/test": "^1.45.1"
},
"dependencies": {
"axios": "^1.6.0",
"dotenv": "^16.3.1",
"eslint": "^8.13.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"knex": "^2.4.0",
"luxon": "^3.4.4",
"micro": "^10.0.1",
"pg": "^8.12.0",

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,6 @@
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"axios": "^1.6.0",
"clipboard-copy": "^4.0.1",
"compare-versions": "^4.1.3",
"lodash": "^4.17.21",

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

@@ -6,7 +6,7 @@ import FormHelperText from '@mui/material/FormHelperText';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import ClearIcon from '@mui/icons-material/Clear';
import { ActionButtonsWrapper } from './style';
import { ClickAwayListener } from '@mui/base/ClickAwayListener';
import ClickAwayListener from '@mui/base/ClickAwayListener';
import InputLabel from '@mui/material/InputLabel';
import { createEditor } from 'slate';
import { Editable, ReactEditor } from 'slate-react';

View File

@@ -1,8 +1,8 @@
import * as React from 'react';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import Alert from '@mui/material/Alert';
import LoadingButton from '@mui/lab/LoadingButton';
import { enqueueSnackbar } from 'notistack';
import useForgotPassword from 'hooks/useForgotPassword';
import Form from 'components/Form';
@@ -12,17 +12,25 @@ import useFormatMessage from 'hooks/useFormatMessage';
export default function ForgotPasswordForm() {
const formatMessage = useFormatMessage();
const {
mutate: forgotPassword,
mutateAsync: forgotPassword,
isPending: loading,
isSuccess,
isError,
error,
} = useForgotPassword();
const handleSubmit = ({ email }) => {
forgotPassword({
email,
});
const handleSubmit = async (values) => {
const { email } = values;
try {
await forgotPassword({
email,
});
} catch (error) {
enqueueSnackbar(
error?.message || formatMessage('forgotPasswordForm.error'),
{
variant: 'error',
},
);
}
};
return (
@@ -49,16 +57,6 @@ export default function ForgotPasswordForm() {
margin="dense"
autoComplete="username"
/>
{isError && (
<Alert severity="error" sx={{ mt: 2 }}>
{error?.message || formatMessage('forgotPasswordForm.error')}
</Alert>
)}
{isSuccess && (
<Alert severity="success" sx={{ mt: 2 }}>
{formatMessage('forgotPasswordForm.instructionsSent')}
</Alert>
)}
<LoadingButton
type="submit"
variant="contained"
@@ -70,6 +68,14 @@ export default function ForgotPasswordForm() {
>
{formatMessage('forgotPasswordForm.submit')}
</LoadingButton>
{isSuccess && (
<Typography
variant="body1"
sx={{ color: (theme) => theme.palette.success.main }}
>
{formatMessage('forgotPasswordForm.instructionsSent')}
</Typography>
)}
</Form>
</Paper>
);

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

@@ -2,7 +2,6 @@ import * as React from 'react';
import { useNavigate, Link as RouterLink } from 'react-router-dom';
import Paper from '@mui/material/Paper';
import Link from '@mui/material/Link';
import Alert from '@mui/material/Alert';
import Typography from '@mui/material/Typography';
import LoadingButton from '@mui/lab/LoadingButton';
import useAuthentication from 'hooks/useAuthentication';
@@ -12,18 +11,16 @@ 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';
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 || [
error?.message || 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

@@ -1,5 +1,5 @@
import PropTypes from 'prop-types';
import { ClickAwayListener } from '@mui/base/ClickAwayListener';
import ClickAwayListener from '@mui/base/ClickAwayListener';
import FormHelperText from '@mui/material/FormHelperText';
import InputLabel from '@mui/material/InputLabel';
import * as React from 'react';

View File

@@ -2,7 +2,6 @@ import { yupResolver } from '@hookform/resolvers/yup';
import LoadingButton from '@mui/lab/LoadingButton';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import Alert from '@mui/material/Alert';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
import * as React from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
@@ -31,8 +30,6 @@ export default function ResetPasswordForm() {
mutateAsync: resetPassword,
isPending,
isSuccess,
error,
isError,
} = useResetPassword();
const token = searchParams.get('token');
@@ -50,23 +47,14 @@ export default function ResetPasswordForm() {
},
});
navigate(URLS.LOGIN);
} catch {}
};
const renderError = () => {
if (!isError) {
return null;
} catch (error) {
enqueueSnackbar(
error?.message || formatMessage('resetPasswordForm.error'),
{
variant: 'error',
},
);
}
const errors = error?.response?.data?.errors?.general || [
error?.message || formatMessage('resetPasswordForm.error'),
];
return errors.map((error) => (
<Alert severity="error" sx={{ mt: 2 }}>
{error}
</Alert>
));
};
return (
@@ -108,6 +96,7 @@ export default function ResetPasswordForm() {
: ''
}
/>
<TextField
label={formatMessage(
'resetPasswordForm.confirmPasswordFieldLabel',
@@ -128,7 +117,7 @@ export default function ResetPasswordForm() {
: ''
}
/>
{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

@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import Button from '@mui/material/Button';
import ButtonGroup from '@mui/material/ButtonGroup';
import { ClickAwayListener } from '@mui/base/ClickAwayListener';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import Grow from '@mui/material/Grow';
import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';

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

@@ -66,8 +66,8 @@ function RoleMappings({ provider, providerLoading }) {
const enqueueSnackbar = useEnqueueSnackbar();
const {
mutateAsync: updateRoleMappings,
isPending: isUpdateRoleMappingsPending,
mutateAsync: updateSamlAuthProvidersRoleMappings,
isPending: isUpdateSamlAuthProvidersRoleMappingsPending,
} = useAdminUpdateSamlAuthProviderRoleMappings(provider?.id);
const { data, isLoading: isAdminSamlAuthProviderRoleMappingsLoading } =
@@ -79,7 +79,7 @@ function RoleMappings({ provider, providerLoading }) {
const handleRoleMappingsUpdate = async (values) => {
try {
if (provider?.id) {
await updateRoleMappings(
await updateSamlAuthProvidersRoleMappings(
values.roleMappings.map(({ roleId, remoteRoleName }) => ({
roleId,
remoteRoleName,
@@ -148,7 +148,7 @@ function RoleMappings({ provider, providerLoading }) {
variant="contained"
color="primary"
sx={{ boxShadow: 2 }}
loading={isUpdateRoleMappingsPending}
loading={isUpdateSamlAuthProvidersRoleMappingsPending}
>
{formatMessage('roleMappingsForm.save')}
</LoadingButton>

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(

File diff suppressed because it is too large Load Diff

17164
yarn.lock Normal file

File diff suppressed because it is too large Load Diff