Compare commits
63 Commits
AUT-1281
...
test-cover
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f36155f3a5 | ||
![]() |
09335fcd79 | ||
![]() |
823a2c8b73 | ||
![]() |
741866e742 | ||
![]() |
41622678b0 | ||
![]() |
449b953401 | ||
![]() |
6345ce5195 | ||
![]() |
95651f6163 | ||
![]() |
b02c1545b7 | ||
![]() |
2deaab9b24 | ||
![]() |
f0d4853533 | ||
![]() |
af81ae812f | ||
![]() |
bae76064e5 | ||
![]() |
07d9198cc8 | ||
![]() |
a2e07ea2f7 | ||
![]() |
864c762fe2 | ||
![]() |
167bb4e8a0 | ||
![]() |
4cf64ede74 | ||
![]() |
bb309fea6f | ||
![]() |
1133362028 | ||
![]() |
eb9226bd4a | ||
![]() |
a9abdcc37e | ||
![]() |
6ace93bdbf | ||
![]() |
b89197939a | ||
![]() |
da788106af | ||
![]() |
49e92e6f1d | ||
![]() |
a6c3276104 | ||
![]() |
6388bfc714 | ||
![]() |
bebc3b181d | ||
![]() |
5a6d561c1a | ||
![]() |
5ba575fdfd | ||
![]() |
dcf8bbd804 | ||
![]() |
ff93ffd0b1 | ||
![]() |
395c09df92 | ||
![]() |
4c903cd08b | ||
![]() |
64cb98717c | ||
![]() |
b0e4ce54fb | ||
![]() |
d67a37002f | ||
![]() |
965ff8bc3f | ||
![]() |
400a495ad2 | ||
![]() |
09d0822a8d | ||
![]() |
7016c20ccc | ||
![]() |
df54895805 | ||
![]() |
62d5e6fe51 | ||
![]() |
4615a0b7ea | ||
![]() |
280d603b14 | ||
![]() |
36271f0749 | ||
![]() |
579638f932 | ||
![]() |
48871c82a6 | ||
![]() |
14056c42ef | ||
![]() |
90fe1576de | ||
![]() |
d61cf13985 | ||
![]() |
dfe6dfd0c6 | ||
![]() |
c138c7d0e9 | ||
![]() |
d542be947e | ||
![]() |
c76366e72e | ||
![]() |
75abfda783 | ||
![]() |
f3d8d7d4ad | ||
![]() |
7255eccb22 | ||
![]() |
a0decb70cc | ||
![]() |
532f562495 | ||
![]() |
27e58ae925 | ||
![]() |
4867ffcb4b |
@@ -5,8 +5,11 @@ BACKEND_PORT=3000
|
|||||||
WEB_PORT=3001
|
WEB_PORT=3001
|
||||||
|
|
||||||
echo "Configuring backend environment variables..."
|
echo "Configuring backend environment variables..."
|
||||||
|
|
||||||
cd packages/backend
|
cd packages/backend
|
||||||
|
|
||||||
rm -rf .env
|
rm -rf .env
|
||||||
|
|
||||||
echo "
|
echo "
|
||||||
PORT=$BACKEND_PORT
|
PORT=$BACKEND_PORT
|
||||||
WEB_APP_URL=http://localhost:$WEB_PORT
|
WEB_APP_URL=http://localhost:$WEB_PORT
|
||||||
@@ -21,24 +24,35 @@ WEBHOOK_SECRET_KEY=sample_webhook_secret_key
|
|||||||
APP_SECRET_KEY=sample_app_secret_key
|
APP_SECRET_KEY=sample_app_secret_key
|
||||||
REDIS_HOST=redis
|
REDIS_HOST=redis
|
||||||
SERVE_WEB_APP_SEPARATELY=true" >> .env
|
SERVE_WEB_APP_SEPARATELY=true" >> .env
|
||||||
|
|
||||||
|
echo "Installing backend dependencies..."
|
||||||
|
|
||||||
|
yarn
|
||||||
|
|
||||||
cd $CURRENT_DIR
|
cd $CURRENT_DIR
|
||||||
|
|
||||||
echo "Configuring web environment variables..."
|
echo "Configuring web environment variables..."
|
||||||
|
|
||||||
cd packages/web
|
cd packages/web
|
||||||
|
|
||||||
rm -rf .env
|
rm -rf .env
|
||||||
|
|
||||||
echo "
|
echo "
|
||||||
PORT=$WEB_PORT
|
PORT=$WEB_PORT
|
||||||
REACT_APP_BACKEND_URL=http://localhost:$BACKEND_PORT
|
REACT_APP_BACKEND_URL=http://localhost:$BACKEND_PORT
|
||||||
" >> .env
|
" >> .env
|
||||||
|
|
||||||
|
echo "Installing web dependencies..."
|
||||||
|
|
||||||
|
yarn
|
||||||
|
|
||||||
cd $CURRENT_DIR
|
cd $CURRENT_DIR
|
||||||
|
|
||||||
echo "Installing and linking dependencies..."
|
|
||||||
yarn
|
|
||||||
yarn lerna bootstrap
|
|
||||||
|
|
||||||
echo "Migrating database..."
|
echo "Migrating database..."
|
||||||
|
|
||||||
cd packages/backend
|
cd packages/backend
|
||||||
|
|
||||||
yarn db:migrate
|
yarn db:migrate
|
||||||
yarn db:seed:user
|
yarn db:seed:user
|
||||||
|
|
||||||
echo "Done!"
|
echo "Done!"
|
||||||
|
9
.github/workflows/backend.yml
vendored
9
.github/workflows/backend.yml
vendored
@@ -41,8 +41,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: cd packages/backend && yarn
|
run: yarn
|
||||||
|
working-directory: packages/backend
|
||||||
- name: Copy .env-example.test file to .env.test
|
- name: Copy .env-example.test file to .env.test
|
||||||
run: cd packages/backend && cp .env-example.test .env.test
|
run: cp .env-example.test .env.test
|
||||||
|
working-directory: packages/backend
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cd packages/backend && yarn test
|
run: yarn test:coverage
|
||||||
|
working-directory: packages/backend
|
||||||
|
30
.github/workflows/ci.yml
vendored
30
.github/workflows/ci.yml
vendored
@@ -18,11 +18,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
cache-dependency-path: yarn.lock
|
cache-dependency-path: packages/backend/yarn.lock
|
||||||
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
- 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: echo "🖥️ The workflow is now ready to test your code on the runner."
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
- run: cd packages/backend && yarn lint
|
working-directory: packages/backend
|
||||||
|
- run: yarn lint
|
||||||
|
working-directory: packages/backend
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||||
start-backend-server:
|
start-backend-server:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -35,11 +37,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
cache-dependency-path: yarn.lock
|
cache-dependency-path: packages/backend/yarn.lock
|
||||||
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
- 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: echo "🖥️ The workflow is now ready to test your code on the runner."
|
||||||
- run: yarn --frozen-lockfile && yarn lerna bootstrap
|
- run: yarn --frozen-lockfile
|
||||||
- run: cd packages/backend && yarn start
|
working-directory: packages/backend
|
||||||
|
- run: yarn start
|
||||||
|
working-directory: packages/backend
|
||||||
env:
|
env:
|
||||||
ENCRYPTION_KEY: sample_encryption_key
|
ENCRYPTION_KEY: sample_encryption_key
|
||||||
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
|
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
|
||||||
@@ -55,11 +59,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
cache-dependency-path: yarn.lock
|
cache-dependency-path: packages/backend/yarn.lock
|
||||||
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
- 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: echo "🖥️ The workflow is now ready to test your code on the runner."
|
||||||
- run: yarn --frozen-lockfile && yarn lerna bootstrap
|
- run: yarn --frozen-lockfile
|
||||||
- run: cd packages/backend && yarn start:worker
|
working-directory: packages/backend
|
||||||
|
- run: yarn start:worker
|
||||||
|
working-directory: packages/backend
|
||||||
env:
|
env:
|
||||||
ENCRYPTION_KEY: sample_encryption_key
|
ENCRYPTION_KEY: sample_encryption_key
|
||||||
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
|
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
|
||||||
@@ -75,11 +81,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
cache-dependency-path: yarn.lock
|
cache-dependency-path: packages/web/yarn.lock
|
||||||
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
- 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: echo "🖥️ The workflow is now ready to test your code on the runner."
|
||||||
- run: yarn --frozen-lockfile && yarn lerna bootstrap
|
- run: yarn --frozen-lockfile
|
||||||
- run: cd packages/web && yarn build
|
working-directory: packages/web
|
||||||
|
- run: yarn build
|
||||||
|
working-directory: packages/web
|
||||||
env:
|
env:
|
||||||
CI: false
|
CI: false
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||||
|
14
.github/workflows/playwright.yml
vendored
14
.github/workflows/playwright.yml
vendored
@@ -58,13 +58,21 @@ jobs:
|
|||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
- name: Install dependencies
|
- name: Install web dependencies
|
||||||
run: yarn && yarn lerna bootstrap
|
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 Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: yarn playwright install --with-deps
|
run: yarn playwright install --with-deps
|
||||||
|
working-directory: ./packages/e2e-tests
|
||||||
- name: Build Automatisch web
|
- name: Build Automatisch web
|
||||||
working-directory: ./packages/web
|
|
||||||
run: yarn build
|
run: yarn build
|
||||||
|
working-directory: ./packages/web
|
||||||
env:
|
env:
|
||||||
# Keep this until we clean up warnings in build processes
|
# Keep this until we clean up warnings in build processes
|
||||||
CI: false
|
CI: false
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,7 +4,6 @@ logs
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
lerna-debug.log*
|
|
||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
@@ -11,10 +11,12 @@ WORKDIR /automatisch
|
|||||||
# copy the app, note .dockerignore
|
# copy the app, note .dockerignore
|
||||||
COPY . /automatisch
|
COPY . /automatisch
|
||||||
|
|
||||||
RUN yarn
|
RUN cd packages/web && yarn
|
||||||
|
|
||||||
RUN cd packages/web && yarn build
|
RUN cd packages/web && yarn build
|
||||||
|
|
||||||
|
RUN cd packages/backend && yarn --production
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
rm -rf /usr/local/share/.cache/ && \
|
rm -rf /usr/local/share/.cache/ && \
|
||||||
apk del build-dependencies
|
apk del build-dependencies
|
||||||
|
13
lerna.json
13
lerna.json
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"packages": [
|
|
||||||
"packages/*"
|
|
||||||
],
|
|
||||||
"version": "0.10.0",
|
|
||||||
"npmClient": "yarn",
|
|
||||||
"useWorkspaces": true,
|
|
||||||
"command": {
|
|
||||||
"add": {
|
|
||||||
"exact": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
32
package.json
32
package.json
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
@@ -12,6 +12,7 @@
|
|||||||
"pretest": "APP_ENV=test node ./test/setup/prepare-test-env.js",
|
"pretest": "APP_ENV=test node ./test/setup/prepare-test-env.js",
|
||||||
"test": "APP_ENV=test vitest run",
|
"test": "APP_ENV=test vitest run",
|
||||||
"test:watch": "APP_ENV=test vitest watch",
|
"test:watch": "APP_ENV=test vitest watch",
|
||||||
|
"test:coverage": "yarn test --coverage",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"db:create": "node ./bin/database/create.js",
|
"db:create": "node ./bin/database/create.js",
|
||||||
"db:seed:user": "node ./bin/database/seed-user.js",
|
"db:seed:user": "node ./bin/database/seed-user.js",
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bull-board/express": "^3.10.1",
|
"@bull-board/express": "^3.10.1",
|
||||||
"@casl/ability": "^6.5.0",
|
"@casl/ability": "^6.5.0",
|
||||||
|
"@faker-js/faker": "^9.2.0",
|
||||||
"@node-saml/passport-saml": "^4.0.4",
|
"@node-saml/passport-saml": "^4.0.4",
|
||||||
"@rudderstack/rudder-sdk-node": "^1.1.2",
|
"@rudderstack/rudder-sdk-node": "^1.1.2",
|
||||||
"@sentry/node": "^7.42.0",
|
"@sentry/node": "^7.42.0",
|
||||||
@@ -36,6 +38,9 @@
|
|||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"debug": "~2.6.9",
|
"debug": "~2.6.9",
|
||||||
"dotenv": "^10.0.0",
|
"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": "~4.18.2",
|
||||||
"express-async-errors": "^3.1.1",
|
"express-async-errors": "^3.1.1",
|
||||||
"express-basic-auth": "^1.2.1",
|
"express-basic-auth": "^1.2.1",
|
||||||
@@ -61,6 +66,7 @@
|
|||||||
"pg": "^8.7.1",
|
"pg": "^8.7.1",
|
||||||
"php-serialize": "^4.0.2",
|
"php-serialize": "^4.0.2",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
|
"prettier": "^2.5.1",
|
||||||
"raw-body": "^2.5.2",
|
"raw-body": "^2.5.2",
|
||||||
"showdown": "^2.1.0",
|
"showdown": "^2.1.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
@@ -92,10 +98,11 @@
|
|||||||
"url": "https://github.com/automatisch/automatisch/issues"
|
"url": "https://github.com/automatisch/automatisch/issues"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@vitest/coverage-v8": "^2.1.5",
|
||||||
"node-gyp": "^10.1.0",
|
"node-gyp": "^10.1.0",
|
||||||
"nodemon": "^2.0.13",
|
"nodemon": "^2.0.13",
|
||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.3",
|
||||||
"vitest": "^1.1.3"
|
"vitest": "^2.1.5"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
@@ -8,7 +8,7 @@ export default {
|
|||||||
key: 'instanceUrl',
|
key: 'instanceUrl',
|
||||||
label: 'WordPress instance URL',
|
label: 'WordPress instance URL',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: false,
|
required: true,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
value: null,
|
value: null,
|
||||||
placeholder: null,
|
placeholder: null,
|
||||||
|
@@ -52,7 +52,7 @@ const appConfig = {
|
|||||||
isDev: appEnv === 'development',
|
isDev: appEnv === 'development',
|
||||||
isTest: appEnv === 'test',
|
isTest: appEnv === 'test',
|
||||||
isProd: appEnv === 'production',
|
isProd: appEnv === 'production',
|
||||||
version: '0.13.1',
|
version: '0.14.0',
|
||||||
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
|
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
|
||||||
postgresSchema: process.env.POSTGRES_SCHEMA || 'public',
|
postgresSchema: process.env.POSTGRES_SCHEMA || 'public',
|
||||||
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),
|
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),
|
||||||
|
@@ -10,7 +10,7 @@ describe('GET /api/v1/automatisch/version', () => {
|
|||||||
|
|
||||||
const expectedPayload = {
|
const expectedPayload = {
|
||||||
data: {
|
data: {
|
||||||
version: '0.13.1',
|
version: '0.14.0',
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
count: 1,
|
count: 1,
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { vi, describe, it, expect } from 'vitest';
|
||||||
import SamlAuthProvider from '../models/saml-auth-provider.ee';
|
import SamlAuthProvider from '../models/saml-auth-provider.ee';
|
||||||
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
|
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
|
||||||
import Identity from './identity.ee';
|
import Identity from './identity.ee';
|
||||||
import Base from './base';
|
import Base from './base';
|
||||||
|
import appConfig from '../config/app';
|
||||||
|
|
||||||
describe('SamlAuthProvider model', () => {
|
describe('SamlAuthProvider model', () => {
|
||||||
it('tableName should return correct name', () => {
|
it('tableName should return correct name', () => {
|
||||||
@@ -45,4 +46,39 @@ describe('SamlAuthProvider model', () => {
|
|||||||
|
|
||||||
expect(virtualAttributes).toStrictEqual(expectedAttributes);
|
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'
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -366,6 +366,18 @@ class User extends Base {
|
|||||||
return now.getTime() - sentAt.getTime() < fourHoursInMilliseconds;
|
return now.getTime() - sentAt.getTime() < fourHoursInMilliseconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toTestTestCoverage() {
|
||||||
|
if (!this.resetPasswordTokenSentAt) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sentAt = new Date(this.resetPasswordTokenSentAt);
|
||||||
|
const now = new Date();
|
||||||
|
const fourHoursInMilliseconds = 1000 * 60 * 60 * 4;
|
||||||
|
|
||||||
|
return now.getTime() - sentAt.getTime() < fourHoursInMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
async sendInvitationEmail() {
|
async sendInvitationEmail() {
|
||||||
await this.generateInvitationToken();
|
await this.generateInvitationToken();
|
||||||
|
|
||||||
@@ -407,7 +419,7 @@ class User extends Base {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async startTrialPeriod() {
|
startTrialPeriod() {
|
||||||
this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
|
this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,7 +602,7 @@ class User extends Base {
|
|||||||
await this.generateHash();
|
await this.generateHash();
|
||||||
|
|
||||||
if (appConfig.isCloud) {
|
if (appConfig.isCloud) {
|
||||||
await this.startTrialPeriod();
|
this.startTrialPeriod();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { DateTime, Duration } from 'luxon';
|
||||||
import appConfig from '../config/app.js';
|
import appConfig from '../config/app.js';
|
||||||
import Base from './base.js';
|
import Base from './base.js';
|
||||||
import AccessToken from './access-token.js';
|
import AccessToken from './access-token.js';
|
||||||
@@ -12,6 +13,12 @@ import Step from './step.js';
|
|||||||
import Subscription from './subscription.ee.js';
|
import Subscription from './subscription.ee.js';
|
||||||
import UsageData from './usage-data.ee.js';
|
import UsageData from './usage-data.ee.js';
|
||||||
import User from './user.js';
|
import User from './user.js';
|
||||||
|
import deleteUserQueue from '../queues/delete-user.ee.js';
|
||||||
|
import emailQueue from '../queues/email.js';
|
||||||
|
import {
|
||||||
|
REMOVE_AFTER_30_DAYS_OR_150_JOBS,
|
||||||
|
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
||||||
|
} from '../helpers/remove-job-configuration.js';
|
||||||
import { createUser } from '../../test/factories/user.js';
|
import { createUser } from '../../test/factories/user.js';
|
||||||
import { createConnection } from '../../test/factories/connection.js';
|
import { createConnection } from '../../test/factories/connection.js';
|
||||||
import { createRole } from '../../test/factories/role.js';
|
import { createRole } from '../../test/factories/role.js';
|
||||||
@@ -580,4 +587,292 @@ describe('User model', () => {
|
|||||||
expect(refetchedUser.invitationTokenSentAt).toBe(null);
|
expect(refetchedUser.invitationTokenSentAt).toBe(null);
|
||||||
expect(refetchedUser.status).toBe('active');
|
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.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('softRemove should soft remove the user, its associations and queue it for hard deletion in 30 days', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
const date = new Date(2024, 10, 12, 12, 50, 0, 0);
|
||||||
|
vi.setSystemTime(date);
|
||||||
|
|
||||||
|
const user = await createUser();
|
||||||
|
|
||||||
|
const softRemoveAssociationsSpy = vi
|
||||||
|
.spyOn(user, 'softRemoveAssociations')
|
||||||
|
.mockReturnValue();
|
||||||
|
|
||||||
|
const deleteUserQueueAddSpy = vi
|
||||||
|
.spyOn(deleteUserQueue, 'add')
|
||||||
|
.mockResolvedValue();
|
||||||
|
|
||||||
|
await user.softRemove();
|
||||||
|
|
||||||
|
const refetchedSoftDeletedUser = await user.$query().withSoftDeleted();
|
||||||
|
|
||||||
|
const millisecondsFor30Days = Duration.fromObject({ days: 30 }).toMillis();
|
||||||
|
const jobName = `Delete user - ${user.id}`;
|
||||||
|
const jobPayload = { id: user.id };
|
||||||
|
|
||||||
|
const jobOptions = {
|
||||||
|
delay: millisecondsFor30Days,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(softRemoveAssociationsSpy).toHaveBeenCalledOnce();
|
||||||
|
expect(refetchedSoftDeletedUser.deletedAt).toStrictEqual(date);
|
||||||
|
|
||||||
|
expect(deleteUserQueueAddSpy).toHaveBeenCalledWith(
|
||||||
|
jobName,
|
||||||
|
jobPayload,
|
||||||
|
jobOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.todo('softRemoveAssociations');
|
||||||
|
|
||||||
|
it('sendResetPasswordEmail should generate reset password token and queue to send reset password email', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
const date = new Date(2024, 10, 12, 14, 33, 0, 0);
|
||||||
|
vi.setSystemTime(date);
|
||||||
|
|
||||||
|
const user = await createUser();
|
||||||
|
|
||||||
|
const generateResetPasswordTokenSpy = vi
|
||||||
|
.spyOn(user, 'generateResetPasswordToken')
|
||||||
|
.mockReturnValue();
|
||||||
|
|
||||||
|
const emailQueueAddSpy = vi.spyOn(emailQueue, 'add').mockResolvedValue();
|
||||||
|
|
||||||
|
await user.sendResetPasswordEmail();
|
||||||
|
|
||||||
|
const refetchedUser = await user.$query();
|
||||||
|
const jobName = `Reset Password Email - ${user.id}`;
|
||||||
|
|
||||||
|
const jobPayload = {
|
||||||
|
email: refetchedUser.email,
|
||||||
|
subject: 'Reset Password',
|
||||||
|
template: 'reset-password-instructions.ee',
|
||||||
|
params: {
|
||||||
|
token: refetchedUser.resetPasswordToken,
|
||||||
|
webAppUrl: appConfig.webAppUrl,
|
||||||
|
fullName: refetchedUser.fullName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const jobOptions = {
|
||||||
|
removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
||||||
|
removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(generateResetPasswordTokenSpy).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect(emailQueueAddSpy).toHaveBeenCalledWith(
|
||||||
|
jobName,
|
||||||
|
jobPayload,
|
||||||
|
jobOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isResetPasswordTokenValid', () => {
|
||||||
|
it('should return true when resetPasswordTokenSentAt is within the next four hours', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
const date = DateTime.fromObject(
|
||||||
|
{ year: 2024, month: 11, day: 12, hour: 16, minute: 30 },
|
||||||
|
{ zone: 'UTC+0' }
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.setSystemTime(date);
|
||||||
|
|
||||||
|
const user = new User();
|
||||||
|
user.resetPasswordTokenSentAt = '2024-11-12T13:31:00.000Z';
|
||||||
|
|
||||||
|
expect(user.isResetPasswordTokenValid()).toBe(true);
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when there is no resetPasswordTokenSentAt', async () => {
|
||||||
|
const user = new User();
|
||||||
|
|
||||||
|
expect(user.isResetPasswordTokenValid()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when resetPasswordTokenSentAt is older than four hours', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
const date = DateTime.fromObject(
|
||||||
|
{ year: 2024, month: 11, day: 12, hour: 16, minute: 30 },
|
||||||
|
{ zone: 'UTC+0' }
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.setSystemTime(date);
|
||||||
|
|
||||||
|
const user = new User();
|
||||||
|
user.resetPasswordTokenSentAt = '2024-11-12T12:29:00.000Z';
|
||||||
|
|
||||||
|
expect(user.isResetPasswordTokenValid()).toBe(false);
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sendInvitationEmail should generate invitation token and queue to send invitation email', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
const date = DateTime.fromObject(
|
||||||
|
{ year: 2024, month: 11, day: 12, hour: 17, minute: 10 },
|
||||||
|
{ zone: 'UTC+0' }
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.setSystemTime(date);
|
||||||
|
|
||||||
|
const user = await createUser();
|
||||||
|
|
||||||
|
const generateInvitationTokenSpy = vi
|
||||||
|
.spyOn(user, 'generateInvitationToken')
|
||||||
|
.mockReturnValue();
|
||||||
|
|
||||||
|
const emailQueueAddSpy = vi.spyOn(emailQueue, 'add').mockResolvedValue();
|
||||||
|
|
||||||
|
await user.sendInvitationEmail();
|
||||||
|
|
||||||
|
const refetchedUser = await user.$query();
|
||||||
|
const jobName = `Invitation Email - ${refetchedUser.id}`;
|
||||||
|
|
||||||
|
const jobPayload = {
|
||||||
|
email: refetchedUser.email,
|
||||||
|
subject: 'You are invited!',
|
||||||
|
template: 'invitation-instructions',
|
||||||
|
params: {
|
||||||
|
fullName: refetchedUser.fullName,
|
||||||
|
acceptInvitationUrl: refetchedUser.acceptInvitationUrl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const jobOptions = {
|
||||||
|
removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
||||||
|
removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(generateInvitationTokenSpy).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
expect(emailQueueAddSpy).toHaveBeenCalledWith(
|
||||||
|
jobName,
|
||||||
|
jobPayload,
|
||||||
|
jobOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isInvitationTokenValid', () => {
|
||||||
|
it('should return truen when invitationTokenSentAt is within the next four hours', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
const date = DateTime.fromObject(
|
||||||
|
{ year: 2024, month: 11, day: 14, hour: 14, minute: 30 },
|
||||||
|
{ zone: 'UTC+0' }
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.setSystemTime(date);
|
||||||
|
|
||||||
|
const user = new User();
|
||||||
|
user.invitationTokenSentAt = '2024-11-14T13:31:00.000Z';
|
||||||
|
|
||||||
|
expect(user.isInvitationTokenValid()).toBe(true);
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when there is no invitationTokenSentAt', async () => {
|
||||||
|
const user = new User();
|
||||||
|
|
||||||
|
expect(user.isInvitationTokenValid()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when invitationTokenSentAt is older than seventy two hours', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
const date = DateTime.fromObject(
|
||||||
|
{ year: 2024, month: 11, day: 14, hour: 14, minute: 30 },
|
||||||
|
{ zone: 'UTC+0' }
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.setSystemTime(date);
|
||||||
|
|
||||||
|
const user = new User();
|
||||||
|
user.invitationTokenSentAt = '2024-11-11T14:20:00.000Z';
|
||||||
|
|
||||||
|
expect(user.isInvitationTokenValid()).toBe(false);
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('generateHash', () => {
|
||||||
|
it('should hash password and re-assign it', async () => {
|
||||||
|
const user = new User();
|
||||||
|
user.password = 'sample-password';
|
||||||
|
|
||||||
|
await user.generateHash();
|
||||||
|
|
||||||
|
expect(user.password).not.toBe('sample-password');
|
||||||
|
expect(await user.login('sample-password')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing when password does not exist', async () => {
|
||||||
|
const user = new User();
|
||||||
|
|
||||||
|
await user.generateHash();
|
||||||
|
|
||||||
|
expect(user.password).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('startTrialPeriod should assign trialExpiryDate 30 days from now', () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
const date = DateTime.fromObject(
|
||||||
|
{ year: 2024, month: 11, day: 14, hour: 16 },
|
||||||
|
{ zone: 'UTC+0' }
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.setSystemTime(date);
|
||||||
|
|
||||||
|
const user = new User();
|
||||||
|
|
||||||
|
user.startTrialPeriod();
|
||||||
|
|
||||||
|
expect(user.trialExpiryDate).toBe('2024-12-14');
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -2,8 +2,25 @@ import { defineConfig } from 'vitest/config';
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
|
root: './',
|
||||||
environment: 'node',
|
environment: 'node',
|
||||||
setupFiles: ['./test/setup/global-hooks.js'],
|
setupFiles: ['./test/setup/global-hooks.js'],
|
||||||
globals: true,
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
4821
packages/backend/yarn.lock
Normal file
4821
packages/backend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
1
packages/docs/.gitignore
vendored
Normal file
1
packages/docs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pages/.vitepress/cache
|
@@ -4,6 +4,7 @@
|
|||||||
"license": "See LICENSE file",
|
"license": "See LICENSE file",
|
||||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vitepress dev pages --port 3002",
|
"dev": "vitepress dev pages --port 3002",
|
||||||
"build": "vitepress build pages",
|
"build": "vitepress build pages",
|
||||||
|
@@ -6,11 +6,19 @@ Clone main branch of Automatisch.
|
|||||||
git clone git@github.com:automatisch/automatisch.git
|
git clone git@github.com:automatisch/automatisch.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, install the dependencies.
|
Then, install the dependencies for both backend and web packages separately.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd automatisch
|
cd automatisch
|
||||||
|
|
||||||
|
# Install backend dependencies
|
||||||
|
cd packages/backend
|
||||||
yarn install
|
yarn install
|
||||||
|
|
||||||
|
# Install web dependencies
|
||||||
|
cd packages/web
|
||||||
|
yarn install
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Backend
|
## Backend
|
||||||
@@ -53,12 +61,14 @@ yarn db:seed:user
|
|||||||
Start the main backend server.
|
Start the main backend server.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cd packages/backend
|
||||||
yarn dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Start the worker server in another terminal tab.
|
Start the worker server in another terminal tab.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cd packages/backend
|
||||||
yarn worker
|
yarn worker
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -84,6 +94,7 @@ It will automatically open [http://localhost:3001](http://localhost:3001) in you
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd packages/docs
|
cd packages/docs
|
||||||
|
yarn install
|
||||||
yarn dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# Repository Structure
|
# Repository Structure
|
||||||
|
|
||||||
We use `lerna` with `yarn workspaces` to manage the mono repository. We have the following packages:
|
We manage a monorepo structure with the following packages:
|
||||||
|
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
@@ -15,3 +15,5 @@ We use `lerna` with `yarn workspaces` to manage the mono repository. We have the
|
|||||||
- `docs` - The docs package contains the documentation website.
|
- `docs` - The docs package contains the documentation website.
|
||||||
- `e2e-tests` - The e2e-tests package contains the end-to-end tests for the internal usage.
|
- `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.
|
- `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.
|
||||||
|
1192
packages/docs/yarn.lock
Normal file
1192
packages/docs/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -29,10 +29,12 @@
|
|||||||
"@playwright/test": "^1.45.1"
|
"@playwright/test": "^1.45.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.6.0",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"eslint": "^8.13.0",
|
"eslint": "^8.13.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"knex": "^2.4.0",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"pg": "^8.12.0",
|
"pg": "^8.12.0",
|
||||||
|
1099
packages/e2e-tests/yarn.lock
Normal file
1099
packages/e2e-tests/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@
|
|||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.1.0",
|
||||||
"@testing-library/user-event": "^12.1.10",
|
"@testing-library/user-event": "^12.1.10",
|
||||||
|
"axios": "^1.6.0",
|
||||||
"clipboard-copy": "^4.0.1",
|
"clipboard-copy": "^4.0.1",
|
||||||
"compare-versions": "^4.1.3",
|
"compare-versions": "^4.1.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
@@ -112,7 +112,7 @@ export default function ResetPasswordForm() {
|
|||||||
<Alert
|
<Alert
|
||||||
data-test="accept-invitation-form-error"
|
data-test="accept-invitation-form-error"
|
||||||
severity="error"
|
severity="error"
|
||||||
sx={{ mt: 1, fontWeight: 500 }}
|
sx={{ mt: 1 }}
|
||||||
>
|
>
|
||||||
{formatMessage('acceptInvitationForm.invalidToken')}
|
{formatMessage('acceptInvitationForm.invalidToken')}
|
||||||
</Alert>
|
</Alert>
|
||||||
|
@@ -126,7 +126,7 @@ function AddAppConnection(props) {
|
|||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
|
||||||
{authDocUrl && (
|
{authDocUrl && (
|
||||||
<Alert severity="info" sx={{ fontWeight: 300 }}>
|
<Alert severity="info">
|
||||||
{formatMessage('addAppConnection.callToDocs', {
|
{formatMessage('addAppConnection.callToDocs', {
|
||||||
appName: name,
|
appName: name,
|
||||||
docsLink: generateExternalLink(authDocUrl),
|
docsLink: generateExternalLink(authDocUrl),
|
||||||
@@ -138,7 +138,7 @@ function AddAppConnection(props) {
|
|||||||
<Alert
|
<Alert
|
||||||
data-test="add-connection-error"
|
data-test="add-connection-error"
|
||||||
severity="error"
|
severity="error"
|
||||||
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
|
sx={{ mt: 1, wordBreak: 'break-all' }}
|
||||||
>
|
>
|
||||||
{!errorDetails && errorMessage}
|
{!errorDetails && errorMessage}
|
||||||
{errorDetails && (
|
{errorDetails && (
|
||||||
|
@@ -32,10 +32,7 @@ function AdminApplicationAuthClientDialog(props) {
|
|||||||
<Dialog open={true} onClose={onClose}>
|
<Dialog open={true} onClose={onClose}>
|
||||||
<DialogTitle>{title}</DialogTitle>
|
<DialogTitle>{title}</DialogTitle>
|
||||||
{error && (
|
{error && (
|
||||||
<Alert
|
<Alert severity="error" sx={{ mt: 1, wordBreak: 'break-all' }}>
|
||||||
severity="error"
|
|
||||||
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
|
|
||||||
>
|
|
||||||
{error.message}
|
{error.message}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
@@ -6,7 +6,7 @@ import FormHelperText from '@mui/material/FormHelperText';
|
|||||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||||
import ClearIcon from '@mui/icons-material/Clear';
|
import ClearIcon from '@mui/icons-material/Clear';
|
||||||
import { ActionButtonsWrapper } from './style';
|
import { ActionButtonsWrapper } from './style';
|
||||||
import ClickAwayListener from '@mui/base/ClickAwayListener';
|
import { ClickAwayListener } from '@mui/base/ClickAwayListener';
|
||||||
import InputLabel from '@mui/material/InputLabel';
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
import { createEditor } from 'slate';
|
import { createEditor } from 'slate';
|
||||||
import { Editable, ReactEditor } from 'slate-react';
|
import { Editable, ReactEditor } from 'slate-react';
|
||||||
|
@@ -188,7 +188,7 @@ function InstallationForm() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{install.isSuccess && (
|
{install.isSuccess && (
|
||||||
<Alert data-test="success-alert" severity="success" sx={{ mt: 3, fontWeight: 500 }}>
|
<Alert data-test="success-alert" severity="success" sx={{ mt: 3 }}>
|
||||||
{formatMessage('installationForm.success', {
|
{formatMessage('installationForm.success', {
|
||||||
link: (str) => (
|
link: (str) => (
|
||||||
<Link
|
<Link
|
||||||
|
@@ -11,16 +11,19 @@ import Form from 'components/Form';
|
|||||||
import TextField from 'components/TextField';
|
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 { Alert } from '@mui/material';
|
||||||
|
|
||||||
function LoginForm() {
|
function LoginForm() {
|
||||||
const isCloud = useCloud();
|
const isCloud = useCloud();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
|
||||||
const authentication = useAuthentication();
|
const authentication = useAuthentication();
|
||||||
const { mutateAsync: createAccessToken, isPending: loading } =
|
const {
|
||||||
useCreateAccessToken();
|
mutateAsync: createAccessToken,
|
||||||
|
isPending: loading,
|
||||||
|
error,
|
||||||
|
isError,
|
||||||
|
} = useCreateAccessToken();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (authentication.isAuthenticated) {
|
if (authentication.isAuthenticated) {
|
||||||
@@ -37,11 +40,19 @@ function LoginForm() {
|
|||||||
});
|
});
|
||||||
const { token } = data;
|
const { token } = data;
|
||||||
authentication.updateToken(token);
|
authentication.updateToken(token);
|
||||||
} catch (error) {
|
} catch {}
|
||||||
enqueueSnackbar(error?.message || formatMessage('loginForm.error'), {
|
};
|
||||||
variant: 'error',
|
|
||||||
});
|
const renderError = () => {
|
||||||
}
|
const errors = error?.response?.data?.errors?.general || [
|
||||||
|
formatMessage('loginForm.error'),
|
||||||
|
];
|
||||||
|
|
||||||
|
return errors.map((error) => (
|
||||||
|
<Alert severity="error" sx={{ mt: 2 }}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -94,6 +105,8 @@ function LoginForm() {
|
|||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isError && renderError()}
|
||||||
|
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ClickAwayListener from '@mui/base/ClickAwayListener';
|
import { ClickAwayListener } from '@mui/base/ClickAwayListener';
|
||||||
import FormHelperText from '@mui/material/FormHelperText';
|
import FormHelperText from '@mui/material/FormHelperText';
|
||||||
import InputLabel from '@mui/material/InputLabel';
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
@@ -7,7 +7,7 @@ import FormControl from '@mui/material/FormControl';
|
|||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
|
||||||
export default function SearchInput({ onChange }) {
|
export default function SearchInput({ onChange, defaultValue = '' }) {
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
return (
|
return (
|
||||||
<FormControl variant="outlined" fullWidth>
|
<FormControl variant="outlined" fullWidth>
|
||||||
@@ -16,6 +16,7 @@ export default function SearchInput({ onChange }) {
|
|||||||
</InputLabel>
|
</InputLabel>
|
||||||
|
|
||||||
<OutlinedInput
|
<OutlinedInput
|
||||||
|
defaultValue={defaultValue}
|
||||||
id="search-input"
|
id="search-input"
|
||||||
type="text"
|
type="text"
|
||||||
size="medium"
|
size="medium"
|
||||||
@@ -34,4 +35,5 @@ export default function SearchInput({ onChange }) {
|
|||||||
|
|
||||||
SearchInput.propTypes = {
|
SearchInput.propTypes = {
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
|
defaultValue: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import ButtonGroup from '@mui/material/ButtonGroup';
|
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
import { ClickAwayListener } from '@mui/base/ClickAwayListener';
|
||||||
import Grow from '@mui/material/Grow';
|
import Grow from '@mui/material/Grow';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import MenuList from '@mui/material/MenuList';
|
import MenuList from '@mui/material/MenuList';
|
||||||
|
@@ -84,10 +84,7 @@ function TestSubstep(props) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{hasError && (
|
{hasError && (
|
||||||
<Alert
|
<Alert severity="error" sx={{ mb: 2, width: '100%' }}>
|
||||||
severity="error"
|
|
||||||
sx={{ mb: 2, fontWeight: 500, width: '100%' }}
|
|
||||||
>
|
|
||||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>
|
<pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>
|
||||||
{JSON.stringify(errorDetails, null, 2)}
|
{JSON.stringify(errorDetails, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
@@ -104,13 +101,11 @@ function TestSubstep(props) {
|
|||||||
severity="warning"
|
severity="warning"
|
||||||
sx={{ mb: 1, width: '100%' }}
|
sx={{ mb: 1, width: '100%' }}
|
||||||
>
|
>
|
||||||
<AlertTitle sx={{ fontWeight: 700 }}>
|
<AlertTitle>
|
||||||
{formatMessage('flowEditor.noTestDataTitle')}
|
{formatMessage('flowEditor.noTestDataTitle')}
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
<Box sx={{ fontWeight: 400 }}>
|
<Box>{formatMessage('flowEditor.noTestDataMessage')}</Box>
|
||||||
{formatMessage('flowEditor.noTestDataMessage')}
|
|
||||||
</Box>
|
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@@ -124,7 +124,6 @@ export default function CreateUser() {
|
|||||||
<Alert
|
<Alert
|
||||||
severity="info"
|
severity="info"
|
||||||
color="primary"
|
color="primary"
|
||||||
sx={{ fontWeight: '500' }}
|
|
||||||
data-test="invitation-email-info-alert"
|
data-test="invitation-email-info-alert"
|
||||||
>
|
>
|
||||||
{formatMessage('createUser.invitationEmailInfo', {
|
{formatMessage('createUser.invitationEmailInfo', {
|
||||||
|
@@ -42,13 +42,9 @@ export default function Execution() {
|
|||||||
<Grid container item sx={{ mt: 2, mb: [2, 5] }} rowGap={3}>
|
<Grid container item sx={{ mt: 2, mb: [2, 5] }} rowGap={3}>
|
||||||
{!isExecutionStepsLoading && !data?.pages?.[0].data.length && (
|
{!isExecutionStepsLoading && !data?.pages?.[0].data.length && (
|
||||||
<Alert severity="warning" sx={{ flex: 1 }}>
|
<Alert severity="warning" sx={{ flex: 1 }}>
|
||||||
<AlertTitle sx={{ fontWeight: 700 }}>
|
<AlertTitle>{formatMessage('execution.noDataTitle')}</AlertTitle>
|
||||||
{formatMessage('execution.noDataTitle')}
|
|
||||||
</AlertTitle>
|
|
||||||
|
|
||||||
<Box sx={{ fontWeight: 400 }}>
|
<Box>{formatMessage('execution.noDataMessage')}</Box>
|
||||||
{formatMessage('execution.noDataMessage')}
|
|
||||||
</Box>
|
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Link, useSearchParams } from 'react-router-dom';
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Grid from '@mui/material/Grid';
|
import Grid from '@mui/material/Grid';
|
||||||
@@ -23,13 +23,18 @@ import useLazyFlows from 'hooks/useLazyFlows';
|
|||||||
|
|
||||||
export default function Flows() {
|
export default function Flows() {
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
|
const navigate = useNavigate();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const page = parseInt(searchParams.get('page') || '', 10) || 1;
|
const page = parseInt(searchParams.get('page') || '', 10) || 1;
|
||||||
const [flowName, setFlowName] = React.useState('');
|
const flowName = searchParams.get('flowName') || '';
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
const [isLoading, setIsLoading] = React.useState(true);
|
||||||
const currentUserAbility = useCurrentUserAbility();
|
const currentUserAbility = useCurrentUserAbility();
|
||||||
|
|
||||||
const { data, mutate: fetchFlows } = useLazyFlows(
|
const {
|
||||||
|
data,
|
||||||
|
mutate: fetchFlows,
|
||||||
|
isSuccess,
|
||||||
|
} = useLazyFlows(
|
||||||
{ flowName, page },
|
{ flowName, page },
|
||||||
{
|
{
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
@@ -38,6 +43,36 @@ 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(
|
const fetchData = React.useMemo(
|
||||||
() => debounce(fetchFlows, 300),
|
() => debounce(fetchFlows, 300),
|
||||||
[fetchFlows],
|
[fetchFlows],
|
||||||
@@ -54,21 +89,14 @@ export default function Flows() {
|
|||||||
}, [fetchData, flowName, page]);
|
}, [fetchData, flowName, page]);
|
||||||
|
|
||||||
React.useEffect(
|
React.useEffect(
|
||||||
function resetPageOnSearch() {
|
function redirectToLastPage() {
|
||||||
// reset search params which only consists of `page`
|
if (navigateToLastPage) {
|
||||||
setSearchParams({});
|
navigate(getPathWithSearchParams(pageInfo.totalPages, flowName));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[flowName],
|
[navigateToLastPage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const flows = data?.data || [];
|
|
||||||
const pageInfo = data?.meta;
|
|
||||||
const hasFlows = flows?.length;
|
|
||||||
|
|
||||||
const onSearchChange = React.useCallback((event) => {
|
|
||||||
setFlowName(event.target.value);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ py: 3 }}>
|
<Box sx={{ py: 3 }}>
|
||||||
<Container>
|
<Container>
|
||||||
@@ -78,7 +106,7 @@ export default function Flows() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} sm="auto" order={{ xs: 2, sm: 1 }}>
|
<Grid item xs={12} sm="auto" order={{ xs: 2, sm: 1 }}>
|
||||||
<SearchInput onChange={onSearchChange} />
|
<SearchInput onChange={onSearchChange} defaultValue={flowName} />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
@@ -111,7 +139,7 @@ export default function Flows() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Divider sx={{ mt: [2, 0], mb: 2 }} />
|
<Divider sx={{ mt: [2, 0], mb: 2 }} />
|
||||||
{isLoading && (
|
{(isLoading || navigateToLastPage) && (
|
||||||
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
||||||
)}
|
)}
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
@@ -119,11 +147,11 @@ export default function Flows() {
|
|||||||
<FlowRow
|
<FlowRow
|
||||||
key={flow.id}
|
key={flow.id}
|
||||||
flow={flow}
|
flow={flow}
|
||||||
onDuplicateFlow={fetchFlows}
|
onDuplicateFlow={onDuplicateFlow}
|
||||||
onDeleteFlow={fetchFlows}
|
onDeleteFlow={fetchFlows}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{!isLoading && !hasFlows && (
|
{!isLoading && !navigateToLastPage && !hasFlows && (
|
||||||
<NoResultFound
|
<NoResultFound
|
||||||
text={formatMessage('flows.noFlows')}
|
text={formatMessage('flows.noFlows')}
|
||||||
{...(currentUserAbility.can('create', 'Flow') && {
|
{...(currentUserAbility.can('create', 'Flow') && {
|
||||||
@@ -131,23 +159,23 @@ export default function Flows() {
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isLoading && pageInfo && pageInfo.totalPages > 1 && (
|
{!isLoading &&
|
||||||
<Pagination
|
!navigateToLastPage &&
|
||||||
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
|
pageInfo &&
|
||||||
page={pageInfo?.currentPage}
|
pageInfo.totalPages > 1 && (
|
||||||
count={pageInfo?.totalPages}
|
<Pagination
|
||||||
onChange={(event, page) =>
|
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
|
||||||
setSearchParams({ page: page.toString() })
|
page={pageInfo?.currentPage}
|
||||||
}
|
count={pageInfo?.totalPages}
|
||||||
renderItem={(item) => (
|
renderItem={(item) => (
|
||||||
<PaginationItem
|
<PaginationItem
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`${item.page === 1 ? '' : `?page=${item.page}`}`}
|
to={getPathWithSearchParams(item.page, flowName)}
|
||||||
{...item}
|
{...item}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@@ -266,8 +266,8 @@ function ProfileSettings() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
||||||
<Alert variant="outlined" severity="error" sx={{ fontWeight: 500 }}>
|
<Alert variant="outlined" severity="error">
|
||||||
<AlertTitle sx={{ fontWeight: 700 }}>
|
<AlertTitle>
|
||||||
{formatMessage('profileSettings.deleteMyAccount')}
|
{formatMessage('profileSettings.deleteMyAccount')}
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
|
@@ -278,6 +278,20 @@ export const defaultTheme = createTheme({
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
MuiAlert: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: ({ theme }) => ({
|
||||||
|
fontWeight: theme.typography.fontWeightRegular,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiAlertTitle: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: ({ theme }) => ({
|
||||||
|
fontWeight: theme.typography.fontWeightBold,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
export const mationTheme = createTheme(
|
export const mationTheme = createTheme(
|
||||||
|
11069
packages/web/yarn.lock
Normal file
11069
packages/web/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user