Compare commits
94 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3de18ab46f | ||
![]() |
54f509ee38 | ||
![]() |
978ceaadb6 | ||
![]() |
770b07179f | ||
![]() |
6d15167ad9 | ||
![]() |
39cba6bc74 | ||
![]() |
9558e66abf | ||
![]() |
ff7908955e | ||
![]() |
26b095b835 | ||
![]() |
feba2a32f9 | ||
![]() |
5090ece9b6 | ||
![]() |
221b19586e | ||
![]() |
3346c14255 | ||
![]() |
6e97e023c9 | ||
![]() |
b26e2ecf2e | ||
![]() |
d896238f23 | ||
![]() |
d2c8f5a75c | ||
![]() |
ce430d238c | ||
![]() |
ee397441ed | ||
![]() |
ba82d986c1 | ||
![]() |
2361cb521e | ||
![]() |
05f8d95281 | ||
![]() |
6c60b1c263 | ||
![]() |
0c32a0693c | ||
![]() |
807faa3c93 | ||
![]() |
fb53e37f7a | ||
![]() |
4ffdf98e16 | ||
![]() |
b8da721e39 | ||
![]() |
db8b98ca16 | ||
![]() |
01b8c600fe | ||
![]() |
69bd5549a2 | ||
![]() |
bc631e3931 | ||
![]() |
8ca4bc5a33 | ||
![]() |
58a569afb0 | ||
![]() |
db718d6fc3 | ||
![]() |
ca9cb8b07b | ||
![]() |
ef14586412 | ||
![]() |
09335fcd79 | ||
![]() |
15f1fca6fe | ||
![]() |
a570b8eb7a | ||
![]() |
02e2735b7a | ||
![]() |
54fa347142 | ||
![]() |
0c752beace | ||
![]() |
c14f808d29 | ||
![]() |
ad71173671 | ||
![]() |
204325ef44 | ||
![]() |
7ce6117659 | ||
![]() |
823a2c8b73 | ||
![]() |
741866e742 | ||
![]() |
41622678b0 | ||
![]() |
449b953401 | ||
![]() |
551548400f | ||
![]() |
6345ce5195 | ||
![]() |
95651f6163 | ||
![]() |
b02c1545b7 | ||
![]() |
2deaab9b24 | ||
![]() |
f0d4853533 | ||
![]() |
af81ae812f | ||
![]() |
bae76064e5 | ||
![]() |
07d9198cc8 | ||
![]() |
a2e07ea2f7 | ||
![]() |
864c762fe2 | ||
![]() |
167bb4e8a0 | ||
![]() |
4cf64ede74 | ||
![]() |
bb309fea6f | ||
![]() |
90a7b4c1c0 | ||
![]() |
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 |
@@ -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,23 +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
|
||||||
cd $CURRENT_DIR
|
|
||||||
|
|
||||||
echo "Installing and linking dependencies..."
|
echo "Installing web dependencies..."
|
||||||
|
|
||||||
yarn
|
yarn
|
||||||
|
|
||||||
|
cd $CURRENT_DIR
|
||||||
|
|
||||||
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
|
||||||
|
24
.github/workflows/ci.yml
vendored
24
.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
|
- 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
|
- 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
|
- 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 }}."
|
||||||
|
25
.github/workflows/playwright.yml
vendored
25
.github/workflows/playwright.yml
vendored
@@ -3,12 +3,13 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
pull_request:
|
# TODO: Add pull request after optimizing the total excecution time of the test suite.
|
||||||
paths:
|
# pull_request:
|
||||||
- 'packages/backend/**'
|
# paths:
|
||||||
- 'packages/e2e-tests/**'
|
# - 'packages/backend/**'
|
||||||
- 'packages/web/**'
|
# - 'packages/e2e-tests/**'
|
||||||
- '!packages/backend/src/apps/**'
|
# - 'packages/web/**'
|
||||||
|
# - '!packages/backend/src/apps/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -58,13 +59,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
|
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
|
||||||
|
11
lerna.json
11
lerna.json
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"packages": ["packages/*"],
|
|
||||||
"version": "0.14.0",
|
|
||||||
"npmClient": "yarn",
|
|
||||||
"command": {
|
|
||||||
"add": {
|
|
||||||
"exact": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
|
||||||
}
|
|
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": "^8.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"
|
||||||
|
@@ -7,7 +7,7 @@ export default async (request, response) => {
|
|||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
const roleMappings = await samlAuthProvider
|
const roleMappings = await samlAuthProvider
|
||||||
.$relatedQuery('samlAuthProvidersRoleMappings')
|
.$relatedQuery('roleMappings')
|
||||||
.orderBy('remote_role_name', 'asc');
|
.orderBy('remote_role_name', 'asc');
|
||||||
|
|
||||||
renderObject(response, roleMappings);
|
renderObject(response, roleMappings);
|
||||||
|
@@ -8,15 +8,14 @@ export default async (request, response) => {
|
|||||||
.findById(samlAuthProviderId)
|
.findById(samlAuthProviderId)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
const samlAuthProvidersRoleMappings =
|
const roleMappings = await samlAuthProvider.updateRoleMappings(
|
||||||
await samlAuthProvider.updateRoleMappings(
|
roleMappingsParams(request)
|
||||||
samlAuthProvidersRoleMappingsParams(request)
|
);
|
||||||
);
|
|
||||||
|
|
||||||
renderObject(response, samlAuthProvidersRoleMappings);
|
renderObject(response, roleMappings);
|
||||||
};
|
};
|
||||||
|
|
||||||
const samlAuthProvidersRoleMappingsParams = (request) => {
|
const roleMappingsParams = (request) => {
|
||||||
const roleMappings = request.body;
|
const roleMappings = request.body;
|
||||||
|
|
||||||
return roleMappings.map(({ roleId, remoteRoleName }) => ({
|
return roleMappings.map(({ roleId, remoteRoleName }) => ({
|
||||||
|
@@ -6,7 +6,7 @@ import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by
|
|||||||
import { createRole } from '../../../../../../test/factories/role.js';
|
import { createRole } from '../../../../../../test/factories/role.js';
|
||||||
import { createUser } from '../../../../../../test/factories/user.js';
|
import { createUser } from '../../../../../../test/factories/user.js';
|
||||||
import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js';
|
import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js';
|
||||||
import { createSamlAuthProvidersRoleMapping } from '../../../../../../test/factories/saml-auth-providers-role-mapping.js';
|
import { createRoleMapping } from '../../../../../../test/factories/role-mapping.js';
|
||||||
import createRoleMappingsMock from '../../../../../../test/mocks/rest/api/v1/admin/saml-auth-providers/update-role-mappings.ee.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';
|
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();
|
samlAuthProvider = await createSamlAuthProvider();
|
||||||
|
|
||||||
await createSamlAuthProvidersRoleMapping({
|
await createRoleMapping({
|
||||||
samlAuthProviderId: samlAuthProvider.id,
|
samlAuthProviderId: samlAuthProvider.id,
|
||||||
remoteRoleName: 'Viewer',
|
remoteRoleName: 'Viewer',
|
||||||
});
|
});
|
||||||
|
|
||||||
await createSamlAuthProvidersRoleMapping({
|
await createRoleMapping({
|
||||||
samlAuthProviderId: samlAuthProvider.id,
|
samlAuthProviderId: samlAuthProvider.id,
|
||||||
remoteRoleName: 'Editor',
|
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 () => {
|
it('should delete role mappings when given empty role mappings', async () => {
|
||||||
const existingRoleMappings = await samlAuthProvider.$relatedQuery(
|
const existingRoleMappings = await samlAuthProvider.$relatedQuery(
|
||||||
'samlAuthProvidersRoleMappings'
|
'roleMappings'
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(existingRoleMappings.length).toBe(2);
|
expect(existingRoleMappings.length).toBe(2);
|
||||||
@@ -149,34 +149,4 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi
|
|||||||
.send(roleMappings)
|
.send(roleMappings)
|
||||||
.expect(404);
|
.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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@@ -87,14 +87,14 @@ describe('GET /api/v1/apps/:appKey/connections', () => {
|
|||||||
|
|
||||||
it('should return not found response for invalid connection UUID', async () => {
|
it('should return not found response for invalid connection UUID', async () => {
|
||||||
await createPermission({
|
await createPermission({
|
||||||
action: 'update',
|
action: 'read',
|
||||||
subject: 'Connection',
|
subject: 'Connection',
|
||||||
roleId: currentUserRole.id,
|
roleId: currentUserRole.id,
|
||||||
conditions: ['isCreator'],
|
conditions: ['isCreator'],
|
||||||
});
|
});
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
.get('/api/v1/connections/invalid-connection-id/connections')
|
.get('/api/v1/apps/invalid-connection-id/connections')
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
@@ -193,7 +193,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
|
|||||||
const notExistingStepUUID = Crypto.randomUUID();
|
const notExistingStepUUID = Crypto.randomUUID();
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
.get(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`)
|
.post(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`)
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
@@ -216,7 +216,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
|
|||||||
const step = await createStep({ appKey: null });
|
const step = await createStep({ appKey: null });
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
.get(`/api/v1/steps/${step.id}/dynamic-data`)
|
.post(`/api/v1/steps/${step.id}/dynamic-data`)
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
@@ -118,7 +118,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
|
|||||||
const notExistingStepUUID = Crypto.randomUUID();
|
const notExistingStepUUID = Crypto.randomUUID();
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
.get(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`)
|
.post(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`)
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
@@ -138,10 +138,11 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
|
|||||||
conditions: [],
|
conditions: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const step = await createStep({ appKey: null });
|
const step = await createStep();
|
||||||
|
await step.$query().patch({ appKey: null });
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
.get(`/api/v1/steps/${step.id}/dynamic-fields`)
|
.post(`/api/v1/steps/${step.id}/dynamic-fields`)
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
@@ -0,0 +1,52 @@
|
|||||||
|
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');
|
||||||
|
}
|
@@ -30,7 +30,7 @@ const findOrCreateUserBySamlIdentity = async (
|
|||||||
: [mappedUser.role];
|
: [mappedUser.role];
|
||||||
|
|
||||||
const samlAuthProviderRoleMapping = await samlAuthProvider
|
const samlAuthProviderRoleMapping = await samlAuthProvider
|
||||||
.$relatedQuery('samlAuthProvidersRoleMappings')
|
.$relatedQuery('roleMappings')
|
||||||
.whereIn('remote_role_name', mappedRoles)
|
.whereIn('remote_role_name', mappedRoles)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.first();
|
.first();
|
||||||
|
46
packages/backend/src/helpers/user-ability.test.js
Normal file
46
packages/backend/src/helpers/user-ability.test.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
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([]);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,30 @@
|
|||||||
|
// 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",
|
||||||
|
}
|
||||||
|
`;
|
@@ -1,6 +1,6 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`SamlAuthProvidersRoleMapping model > jsonSchema should have the correct schema 1`] = `
|
exports[`RoleMapping model > jsonSchema should have the correct schema 1`] = `
|
||||||
{
|
{
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import Base from './base.js';
|
import Base from './base.js';
|
||||||
import SamlAuthProvider from './saml-auth-provider.ee.js';
|
import SamlAuthProvider from './saml-auth-provider.ee.js';
|
||||||
|
|
||||||
class SamlAuthProvidersRoleMapping extends Base {
|
class RoleMapping extends Base {
|
||||||
static tableName = 'saml_auth_providers_role_mappings';
|
static tableName = 'role_mappings';
|
||||||
|
|
||||||
static jsonSchema = {
|
static jsonSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@@ -21,11 +21,11 @@ class SamlAuthProvidersRoleMapping extends Base {
|
|||||||
relation: Base.BelongsToOneRelation,
|
relation: Base.BelongsToOneRelation,
|
||||||
modelClass: SamlAuthProvider,
|
modelClass: SamlAuthProvider,
|
||||||
join: {
|
join: {
|
||||||
from: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
from: 'role_mappings.saml_auth_provider_id',
|
||||||
to: 'saml_auth_providers.id',
|
to: 'saml_auth_providers.id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SamlAuthProvidersRoleMapping;
|
export default RoleMapping;
|
@@ -1,28 +1,26 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
|
import RoleMapping from './role-mapping.ee';
|
||||||
import SamlAuthProvider from './saml-auth-provider.ee';
|
import SamlAuthProvider from './saml-auth-provider.ee';
|
||||||
import Base from './base';
|
import Base from './base';
|
||||||
|
|
||||||
describe('SamlAuthProvidersRoleMapping model', () => {
|
describe('RoleMapping model', () => {
|
||||||
it('tableName should return correct name', () => {
|
it('tableName should return correct name', () => {
|
||||||
expect(SamlAuthProvidersRoleMapping.tableName).toBe(
|
expect(RoleMapping.tableName).toBe('role_mappings');
|
||||||
'saml_auth_providers_role_mappings'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('jsonSchema should have the correct schema', () => {
|
it('jsonSchema should have the correct schema', () => {
|
||||||
expect(SamlAuthProvidersRoleMapping.jsonSchema).toMatchSnapshot();
|
expect(RoleMapping.jsonSchema).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('relationMappings should return correct associations', () => {
|
it('relationMappings should return correct associations', () => {
|
||||||
const relationMappings = SamlAuthProvidersRoleMapping.relationMappings();
|
const relationMappings = RoleMapping.relationMappings();
|
||||||
|
|
||||||
const expectedRelations = {
|
const expectedRelations = {
|
||||||
samlAuthProvider: {
|
samlAuthProvider: {
|
||||||
relation: Base.BelongsToOneRelation,
|
relation: Base.BelongsToOneRelation,
|
||||||
modelClass: SamlAuthProvider,
|
modelClass: SamlAuthProvider,
|
||||||
join: {
|
join: {
|
||||||
from: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
from: 'role_mappings.saml_auth_provider_id',
|
||||||
to: 'saml_auth_providers.id',
|
to: 'saml_auth_providers.id',
|
||||||
},
|
},
|
||||||
},
|
},
|
@@ -5,7 +5,7 @@ import appConfig from '../config/app.js';
|
|||||||
import axios from '../helpers/axios-with-proxy.js';
|
import axios from '../helpers/axios-with-proxy.js';
|
||||||
import Base from './base.js';
|
import Base from './base.js';
|
||||||
import Identity from './identity.ee.js';
|
import Identity from './identity.ee.js';
|
||||||
import SamlAuthProvidersRoleMapping from './saml-auth-providers-role-mapping.ee.js';
|
import RoleMapping from './role-mapping.ee.js';
|
||||||
|
|
||||||
class SamlAuthProvider extends Base {
|
class SamlAuthProvider extends Base {
|
||||||
static tableName = 'saml_auth_providers';
|
static tableName = 'saml_auth_providers';
|
||||||
@@ -53,12 +53,12 @@ class SamlAuthProvider extends Base {
|
|||||||
to: 'saml_auth_providers.id',
|
to: 'saml_auth_providers.id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
samlAuthProvidersRoleMappings: {
|
roleMappings: {
|
||||||
relation: Base.HasManyRelation,
|
relation: Base.HasManyRelation,
|
||||||
modelClass: SamlAuthProvidersRoleMapping,
|
modelClass: RoleMapping,
|
||||||
join: {
|
join: {
|
||||||
from: 'saml_auth_providers.id',
|
from: 'saml_auth_providers.id',
|
||||||
to: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
to: 'role_mappings.saml_auth_provider_id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -133,27 +133,22 @@ class SamlAuthProvider extends Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateRoleMappings(roleMappings) {
|
async updateRoleMappings(roleMappings) {
|
||||||
return await SamlAuthProvider.transaction(async (trx) => {
|
await this.$relatedQuery('roleMappings').delete();
|
||||||
await this.$relatedQuery('samlAuthProvidersRoleMappings', trx).delete();
|
|
||||||
|
|
||||||
if (isEmpty(roleMappings)) {
|
if (isEmpty(roleMappings)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const samlAuthProvidersRoleMappingsData = roleMappings.map(
|
const roleMappingsData = roleMappings.map((roleMapping) => ({
|
||||||
(samlAuthProvidersRoleMapping) => ({
|
...roleMapping,
|
||||||
...samlAuthProvidersRoleMapping,
|
samlAuthProviderId: this.id,
|
||||||
samlAuthProviderId: this.id,
|
}));
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const samlAuthProvidersRoleMappings =
|
const newRoleMappings = await RoleMapping.query().insertAndFetch(
|
||||||
await SamlAuthProvidersRoleMapping.query(trx).insertAndFetch(
|
roleMappingsData
|
||||||
samlAuthProvidersRoleMappingsData
|
);
|
||||||
);
|
|
||||||
|
|
||||||
return samlAuthProvidersRoleMappings;
|
return newRoleMappings;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,9 +1,14 @@
|
|||||||
import { vi, describe, it, expect } from 'vitest';
|
import { vi, beforeEach, describe, it, expect } from 'vitest';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
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 RoleMapping from '../models/role-mapping.ee';
|
||||||
|
import axios from '../helpers/axios-with-proxy.js';
|
||||||
import Identity from './identity.ee';
|
import Identity from './identity.ee';
|
||||||
import Base from './base';
|
import Base from './base';
|
||||||
import appConfig from '../config/app';
|
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', () => {
|
describe('SamlAuthProvider model', () => {
|
||||||
it('tableName should return correct name', () => {
|
it('tableName should return correct name', () => {
|
||||||
@@ -26,12 +31,12 @@ describe('SamlAuthProvider model', () => {
|
|||||||
to: 'saml_auth_providers.id',
|
to: 'saml_auth_providers.id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
samlAuthProvidersRoleMappings: {
|
roleMappings: {
|
||||||
relation: Base.HasManyRelation,
|
relation: Base.HasManyRelation,
|
||||||
modelClass: SamlAuthProvidersRoleMapping,
|
modelClass: RoleMapping,
|
||||||
join: {
|
join: {
|
||||||
from: 'saml_auth_providers.id',
|
from: 'saml_auth_providers.id',
|
||||||
to: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
to: 'role_mappings.saml_auth_provider_id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -81,4 +86,146 @@ describe('SamlAuthProvider model', () => {
|
|||||||
'https://example.com/saml/logout'
|
'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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -212,6 +212,10 @@ class User extends Base {
|
|||||||
return `${appConfig.webAppUrl}/accept-invitation?token=${this.invitationToken}`;
|
return `${appConfig.webAppUrl}/accept-invitation?token=${this.invitationToken}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get ability() {
|
||||||
|
return userAbility(this);
|
||||||
|
}
|
||||||
|
|
||||||
static async authenticate(email, password) {
|
static async authenticate(email, password) {
|
||||||
const user = await User.query().findOne({
|
const user = await User.query().findOne({
|
||||||
email: email?.toLowerCase() || null,
|
email: email?.toLowerCase() || null,
|
||||||
@@ -407,7 +411,7 @@ class User extends Base {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async startTrialPeriod() {
|
startTrialPeriod() {
|
||||||
this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
|
this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,62 +587,6 @@ class User extends Base {
|
|||||||
return user;
|
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) {
|
can(action, subject) {
|
||||||
const can = this.ability.can(action, subject);
|
const can = this.ability.can(action, subject);
|
||||||
|
|
||||||
@@ -654,12 +602,68 @@ class User extends Base {
|
|||||||
return conditionMap;
|
return conditionMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
cannot(action, subject) {
|
lowercaseEmail() {
|
||||||
const cannot = this.ability.cannot(action, subject);
|
if (this.email) {
|
||||||
|
this.email = this.email.toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (cannot) throw new NotAuthorizedError();
|
async createUsageData() {
|
||||||
|
if (appConfig.isCloud) {
|
||||||
|
return await this.$relatedQuery('usageData').insertAndFetch({
|
||||||
|
userId: this.id,
|
||||||
|
consumedTaskCount: 0,
|
||||||
|
nextResetAt: DateTime.now().plus({ days: 30 }).toISODate(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return cannot;
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -11,10 +11,6 @@ const redisConnection = {
|
|||||||
|
|
||||||
const actionQueue = new Queue('action', redisConnection);
|
const actionQueue = new Queue('action', redisConnection);
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
|
||||||
await actionQueue.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
actionQueue.on('error', (error) => {
|
actionQueue.on('error', (error) => {
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
if (error.code === CONNECTION_REFUSED) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
@@ -11,10 +11,6 @@ const redisConnection = {
|
|||||||
|
|
||||||
const deleteUserQueue = new Queue('delete-user', redisConnection);
|
const deleteUserQueue = new Queue('delete-user', redisConnection);
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
|
||||||
await deleteUserQueue.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
deleteUserQueue.on('error', (error) => {
|
deleteUserQueue.on('error', (error) => {
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
if (error.code === CONNECTION_REFUSED) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
@@ -11,10 +11,6 @@ const redisConnection = {
|
|||||||
|
|
||||||
const emailQueue = new Queue('email', redisConnection);
|
const emailQueue = new Queue('email', redisConnection);
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
|
||||||
await emailQueue.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
emailQueue.on('error', (error) => {
|
emailQueue.on('error', (error) => {
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
if (error.code === CONNECTION_REFUSED) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
@@ -11,10 +11,6 @@ const redisConnection = {
|
|||||||
|
|
||||||
const flowQueue = new Queue('flow', redisConnection);
|
const flowQueue = new Queue('flow', redisConnection);
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
|
||||||
await flowQueue.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
flowQueue.on('error', (error) => {
|
flowQueue.on('error', (error) => {
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
if (error.code === CONNECTION_REFUSED) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
21
packages/backend/src/queues/index.js
Normal file
21
packages/backend/src/queues/index.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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;
|
@@ -14,10 +14,6 @@ const removeCancelledSubscriptionsQueue = new Queue(
|
|||||||
redisConnection
|
redisConnection
|
||||||
);
|
);
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
|
||||||
await removeCancelledSubscriptionsQueue.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
removeCancelledSubscriptionsQueue.on('error', (error) => {
|
removeCancelledSubscriptionsQueue.on('error', (error) => {
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
if (error.code === CONNECTION_REFUSED) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
@@ -11,10 +11,6 @@ const redisConnection = {
|
|||||||
|
|
||||||
const triggerQueue = new Queue('trigger', redisConnection);
|
const triggerQueue = new Queue('trigger', redisConnection);
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
|
||||||
await triggerQueue.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
triggerQueue.on('error', (error) => {
|
triggerQueue.on('error', (error) => {
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
if (error.code === CONNECTION_REFUSED) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
@@ -26,7 +26,7 @@ const serializers = {
|
|||||||
Permission: permissionSerializer,
|
Permission: permissionSerializer,
|
||||||
AdminSamlAuthProvider: adminSamlAuthProviderSerializer,
|
AdminSamlAuthProvider: adminSamlAuthProviderSerializer,
|
||||||
SamlAuthProvider: samlAuthProviderSerializer,
|
SamlAuthProvider: samlAuthProviderSerializer,
|
||||||
SamlAuthProvidersRoleMapping: samlAuthProviderRoleMappingSerializer,
|
RoleMapping: samlAuthProviderRoleMappingSerializer,
|
||||||
AppAuthClient: appAuthClientSerializer,
|
AppAuthClient: appAuthClientSerializer,
|
||||||
AppConfig: appConfigSerializer,
|
AppConfig: appConfigSerializer,
|
||||||
Flow: flowSerializer,
|
Flow: flowSerializer,
|
||||||
|
@@ -1,20 +1,22 @@
|
|||||||
import * as Sentry from './helpers/sentry.ee.js';
|
import * as Sentry from './helpers/sentry.ee.js';
|
||||||
import appConfig from './config/app.js';
|
import process from 'node:process';
|
||||||
|
|
||||||
Sentry.init();
|
Sentry.init();
|
||||||
|
|
||||||
import './config/orm.js';
|
import './config/orm.js';
|
||||||
import './helpers/check-worker-readiness.js';
|
import './helpers/check-worker-readiness.js';
|
||||||
import './workers/flow.js';
|
import queues from './queues/index.js';
|
||||||
import './workers/trigger.js';
|
import workers from './workers/index.js';
|
||||||
import './workers/action.js';
|
|
||||||
import './workers/email.js';
|
|
||||||
import './workers/delete-user.ee.js';
|
|
||||||
|
|
||||||
if (appConfig.isCloud) {
|
process.on('SIGTERM', async () => {
|
||||||
import('./workers/remove-cancelled-subscriptions.ee.js');
|
for (const queue of queues) {
|
||||||
import('./queues/remove-cancelled-subscriptions.ee.js');
|
await queue.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const worker of workers) {
|
||||||
|
await worker.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
import telemetry from './helpers/telemetry/index.js';
|
import telemetry from './helpers/telemetry/index.js';
|
||||||
|
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { Worker } from 'bullmq';
|
||||||
import process from 'node:process';
|
|
||||||
|
|
||||||
import * as Sentry from '../helpers/sentry.ee.js';
|
import * as Sentry from '../helpers/sentry.ee.js';
|
||||||
import redisConfig from '../config/redis.js';
|
import redisConfig from '../config/redis.js';
|
||||||
@@ -15,7 +14,7 @@ import delayAsMilliseconds from '../helpers/delay-as-milliseconds.js';
|
|||||||
|
|
||||||
const DEFAULT_DELAY_DURATION = 0;
|
const DEFAULT_DELAY_DURATION = 0;
|
||||||
|
|
||||||
export const worker = new Worker(
|
const actionWorker = new Worker(
|
||||||
'action',
|
'action',
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const { stepId, flowId, executionId, computedParameters, executionStep } =
|
const { stepId, flowId, executionId, computedParameters, executionStep } =
|
||||||
@@ -55,11 +54,11 @@ export const worker = new Worker(
|
|||||||
{ connection: redisConfig }
|
{ connection: redisConfig }
|
||||||
);
|
);
|
||||||
|
|
||||||
worker.on('completed', (job) => {
|
actionWorker.on('completed', (job) => {
|
||||||
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
||||||
});
|
});
|
||||||
|
|
||||||
worker.on('failed', (job, err) => {
|
actionWorker.on('failed', (job, err) => {
|
||||||
const errorMessage = `
|
const errorMessage = `
|
||||||
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}
|
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}
|
||||||
\n ${err.stack}
|
\n ${err.stack}
|
||||||
@@ -74,6 +73,4 @@ worker.on('failed', (job, err) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
export default actionWorker;
|
||||||
await worker.close();
|
|
||||||
});
|
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { Worker } from 'bullmq';
|
||||||
import process from 'node:process';
|
|
||||||
|
|
||||||
import * as Sentry from '../helpers/sentry.ee.js';
|
import * as Sentry from '../helpers/sentry.ee.js';
|
||||||
import redisConfig from '../config/redis.js';
|
import redisConfig from '../config/redis.js';
|
||||||
@@ -8,7 +7,7 @@ import appConfig from '../config/app.js';
|
|||||||
import User from '../models/user.js';
|
import User from '../models/user.js';
|
||||||
import ExecutionStep from '../models/execution-step.js';
|
import ExecutionStep from '../models/execution-step.js';
|
||||||
|
|
||||||
export const worker = new Worker(
|
const deleteUserWorker = new Worker(
|
||||||
'delete-user',
|
'delete-user',
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const { id } = job.data;
|
const { id } = job.data;
|
||||||
@@ -46,13 +45,13 @@ export const worker = new Worker(
|
|||||||
{ connection: redisConfig }
|
{ connection: redisConfig }
|
||||||
);
|
);
|
||||||
|
|
||||||
worker.on('completed', (job) => {
|
deleteUserWorker.on('completed', (job) => {
|
||||||
logger.info(
|
logger.info(
|
||||||
`JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has been deleted!`
|
`JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has been deleted!`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
worker.on('failed', (job, err) => {
|
deleteUserWorker.on('failed', (job, err) => {
|
||||||
const errorMessage = `
|
const errorMessage = `
|
||||||
JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has failed to be deleted! ${err.message}
|
JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has failed to be deleted! ${err.message}
|
||||||
\n ${err.stack}
|
\n ${err.stack}
|
||||||
@@ -67,6 +66,4 @@ worker.on('failed', (job, err) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
export default deleteUserWorker;
|
||||||
await worker.close();
|
|
||||||
});
|
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { Worker } from 'bullmq';
|
||||||
import process from 'node:process';
|
|
||||||
|
|
||||||
import * as Sentry from '../helpers/sentry.ee.js';
|
import * as Sentry from '../helpers/sentry.ee.js';
|
||||||
import redisConfig from '../config/redis.js';
|
import redisConfig from '../config/redis.js';
|
||||||
@@ -16,7 +15,7 @@ const isAutomatischEmail = (email) => {
|
|||||||
return email.endsWith('@automatisch.io');
|
return email.endsWith('@automatisch.io');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const worker = new Worker(
|
const emailWorker = new Worker(
|
||||||
'email',
|
'email',
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const { email, subject, template, params } = job.data;
|
const { email, subject, template, params } = job.data;
|
||||||
@@ -39,13 +38,13 @@ export const worker = new Worker(
|
|||||||
{ connection: redisConfig }
|
{ connection: redisConfig }
|
||||||
);
|
);
|
||||||
|
|
||||||
worker.on('completed', (job) => {
|
emailWorker.on('completed', (job) => {
|
||||||
logger.info(
|
logger.info(
|
||||||
`JOB ID: ${job.id} - ${job.data.subject} email sent to ${job.data.email}!`
|
`JOB ID: ${job.id} - ${job.data.subject} email sent to ${job.data.email}!`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
worker.on('failed', (job, err) => {
|
emailWorker.on('failed', (job, err) => {
|
||||||
const errorMessage = `
|
const errorMessage = `
|
||||||
JOB ID: ${job.id} - ${job.data.subject} email to ${job.data.email} has failed to send with ${err.message}
|
JOB ID: ${job.id} - ${job.data.subject} email to ${job.data.email} has failed to send with ${err.message}
|
||||||
\n ${err.stack}
|
\n ${err.stack}
|
||||||
@@ -60,6 +59,4 @@ worker.on('failed', (job, err) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
export default emailWorker;
|
||||||
await worker.close();
|
|
||||||
});
|
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { Worker } from 'bullmq';
|
||||||
import process from 'node:process';
|
|
||||||
|
|
||||||
import * as Sentry from '../helpers/sentry.ee.js';
|
import * as Sentry from '../helpers/sentry.ee.js';
|
||||||
import redisConfig from '../config/redis.js';
|
import redisConfig from '../config/redis.js';
|
||||||
@@ -13,7 +12,7 @@ import {
|
|||||||
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
||||||
} from '../helpers/remove-job-configuration.js';
|
} from '../helpers/remove-job-configuration.js';
|
||||||
|
|
||||||
export const worker = new Worker(
|
const flowWorker = new Worker(
|
||||||
'flow',
|
'flow',
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const { flowId } = job.data;
|
const { flowId } = job.data;
|
||||||
@@ -64,11 +63,11 @@ export const worker = new Worker(
|
|||||||
{ connection: redisConfig }
|
{ connection: redisConfig }
|
||||||
);
|
);
|
||||||
|
|
||||||
worker.on('completed', (job) => {
|
flowWorker.on('completed', (job) => {
|
||||||
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
||||||
});
|
});
|
||||||
|
|
||||||
worker.on('failed', async (job, err) => {
|
flowWorker.on('failed', async (job, err) => {
|
||||||
const errorMessage = `
|
const errorMessage = `
|
||||||
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}
|
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}
|
||||||
\n ${err.stack}
|
\n ${err.stack}
|
||||||
@@ -95,6 +94,4 @@ worker.on('failed', async (job, err) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
export default flowWorker;
|
||||||
await worker.close();
|
|
||||||
});
|
|
||||||
|
21
packages/backend/src/workers/index.js
Normal file
21
packages/backend/src/workers/index.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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;
|
@@ -1,12 +1,11 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { Worker } from 'bullmq';
|
||||||
import process from 'node:process';
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import * as Sentry from '../helpers/sentry.ee.js';
|
import * as Sentry from '../helpers/sentry.ee.js';
|
||||||
import redisConfig from '../config/redis.js';
|
import redisConfig from '../config/redis.js';
|
||||||
import logger from '../helpers/logger.js';
|
import logger from '../helpers/logger.js';
|
||||||
import Subscription from '../models/subscription.ee.js';
|
import Subscription from '../models/subscription.ee.js';
|
||||||
|
|
||||||
export const worker = new Worker(
|
const removeCancelledSubscriptionsWorker = new Worker(
|
||||||
'remove-cancelled-subscriptions',
|
'remove-cancelled-subscriptions',
|
||||||
async () => {
|
async () => {
|
||||||
await Subscription.query()
|
await Subscription.query()
|
||||||
@@ -23,13 +22,13 @@ export const worker = new Worker(
|
|||||||
{ connection: redisConfig }
|
{ connection: redisConfig }
|
||||||
);
|
);
|
||||||
|
|
||||||
worker.on('completed', (job) => {
|
removeCancelledSubscriptionsWorker.on('completed', (job) => {
|
||||||
logger.info(
|
logger.info(
|
||||||
`JOB ID: ${job.id} - The cancelled subscriptions have been removed!`
|
`JOB ID: ${job.id} - The cancelled subscriptions have been removed!`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
worker.on('failed', (job, err) => {
|
removeCancelledSubscriptionsWorker.on('failed', (job, err) => {
|
||||||
const errorMessage = `
|
const errorMessage = `
|
||||||
JOB ID: ${job.id} - ERROR: The cancelled subscriptions can not be removed! ${err.message}
|
JOB ID: ${job.id} - ERROR: The cancelled subscriptions can not be removed! ${err.message}
|
||||||
\n ${err.stack}
|
\n ${err.stack}
|
||||||
@@ -42,6 +41,4 @@ worker.on('failed', (job, err) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
export default removeCancelledSubscriptionsWorker;
|
||||||
await worker.close();
|
|
||||||
});
|
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { Worker } from 'bullmq';
|
||||||
import process from 'node:process';
|
|
||||||
|
|
||||||
import * as Sentry from '../helpers/sentry.ee.js';
|
import * as Sentry from '../helpers/sentry.ee.js';
|
||||||
import redisConfig from '../config/redis.js';
|
import redisConfig from '../config/redis.js';
|
||||||
@@ -12,7 +11,7 @@ import {
|
|||||||
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
||||||
} from '../helpers/remove-job-configuration.js';
|
} from '../helpers/remove-job-configuration.js';
|
||||||
|
|
||||||
export const worker = new Worker(
|
const triggerWorker = new Worker(
|
||||||
'trigger',
|
'trigger',
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const { flowId, executionId, stepId, executionStep } = await processTrigger(
|
const { flowId, executionId, stepId, executionStep } = await processTrigger(
|
||||||
@@ -41,11 +40,11 @@ export const worker = new Worker(
|
|||||||
{ connection: redisConfig }
|
{ connection: redisConfig }
|
||||||
);
|
);
|
||||||
|
|
||||||
worker.on('completed', (job) => {
|
triggerWorker.on('completed', (job) => {
|
||||||
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
||||||
});
|
});
|
||||||
|
|
||||||
worker.on('failed', (job, err) => {
|
triggerWorker.on('failed', (job, err) => {
|
||||||
const errorMessage = `
|
const errorMessage = `
|
||||||
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}
|
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}
|
||||||
\n ${err.stack}
|
\n ${err.stack}
|
||||||
@@ -60,6 +59,4 @@ worker.on('failed', (job, err) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
export default triggerWorker;
|
||||||
await worker.close();
|
|
||||||
});
|
|
||||||
|
@@ -1,16 +1,15 @@
|
|||||||
|
import { faker } from '@faker-js/faker';
|
||||||
import { createRole } from './role.js';
|
import { createRole } from './role.js';
|
||||||
|
import RoleMapping from '../../src/models/role-mapping.ee.js';
|
||||||
import { createSamlAuthProvider } from './saml-auth-provider.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 = {}) => {
|
export const createRoleMapping = async (params = {}) => {
|
||||||
params.roleId = params?.roleId || (await createRole()).id;
|
params.roleId = params.roleId || (await createRole()).id;
|
||||||
params.samlAuthProviderId =
|
params.samlAuthProviderId =
|
||||||
params?.samlAuthProviderId || (await createSamlAuthProvider()).id;
|
params.samlAuthProviderId || (await createSamlAuthProvider()).id;
|
||||||
|
params.remoteRoleName = params.remoteRoleName || faker.person.jobType();
|
||||||
|
|
||||||
params.remoteRoleName = params?.remoteRoleName || 'User';
|
const roleMapping = await RoleMapping.query().insertAndFetch(params);
|
||||||
|
|
||||||
const samlAuthProviderRoleMapping =
|
return roleMapping;
|
||||||
await SamlAuthProviderRoleMapping.query().insertAndFetch(params);
|
|
||||||
|
|
||||||
return samlAuthProviderRoleMapping;
|
|
||||||
};
|
};
|
||||||
|
@@ -1,16 +0,0 @@
|
|||||||
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;
|
|
||||||
};
|
|
@@ -15,7 +15,7 @@ const getRoleMappingsMock = async (roleMappings) => {
|
|||||||
currentPage: null,
|
currentPage: null,
|
||||||
isArray: true,
|
isArray: true,
|
||||||
totalPages: null,
|
totalPages: null,
|
||||||
type: 'SamlAuthProvidersRoleMapping',
|
type: 'RoleMapping',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -15,7 +15,7 @@ const createRoleMappingsMock = async (roleMappings) => {
|
|||||||
currentPage: null,
|
currentPage: null,
|
||||||
isArray: true,
|
isArray: true,
|
||||||
totalPages: null,
|
totalPages: null,
|
||||||
type: 'SamlAuthProvidersRoleMapping',
|
type: 'RoleMapping',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -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
@@ -1,4 +1,4 @@
|
|||||||
import { BasePage } from "./base-page";
|
import { BasePage } from './base-page';
|
||||||
const { faker } = require('@faker-js/faker');
|
const { faker } = require('@faker-js/faker');
|
||||||
const { expect } = require('@playwright/test');
|
const { expect } = require('@playwright/test');
|
||||||
|
|
||||||
@@ -6,16 +6,18 @@ export class AdminSetupPage extends BasePage {
|
|||||||
path = '/installation';
|
path = '/installation';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@playwright/test').Page} page
|
* @param {import('@playwright/test').Page} page
|
||||||
*/
|
*/
|
||||||
constructor(page) {
|
constructor(page) {
|
||||||
super(page);
|
super(page);
|
||||||
|
|
||||||
this.fullNameTextField = this.page.getByTestId('fullName-text-field');
|
this.fullNameTextField = this.page.getByTestId('fullName-text-field');
|
||||||
this.emailTextField = this.page.getByTestId('email-text-field');
|
this.emailTextField = this.page.getByTestId('email-text-field');
|
||||||
this.passwordTextField = this.page.getByTestId('password-text-field');
|
this.passwordTextField = this.page.getByTestId('password-text-field');
|
||||||
this.repeatPasswordTextField = this.page.getByTestId('repeat-password-text-field');
|
this.repeatPasswordTextField = this.page.getByTestId(
|
||||||
this.createAdminButton = this.page.getByTestId('signUp-button');
|
'repeat-password-text-field'
|
||||||
|
);
|
||||||
|
this.createAdminButton = this.page.getByTestId('installation-button');
|
||||||
this.invalidFields = this.page.locator('p.Mui-error');
|
this.invalidFields = this.page.locator('p.Mui-error');
|
||||||
this.successAlert = this.page.getByTestId('success-alert');
|
this.successAlert = this.page.getByTestId('success-alert');
|
||||||
}
|
}
|
||||||
@@ -46,7 +48,7 @@ export class AdminSetupPage extends BasePage {
|
|||||||
await this.repeatPasswordTextField.fill(testUser.wronglyRepeatedPassword);
|
await this.repeatPasswordTextField.fill(testUser.wronglyRepeatedPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
async submitAdminForm() {
|
async submitAdminForm() {
|
||||||
await this.createAdminButton.click();
|
await this.createAdminButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +61,10 @@ export class AdminSetupPage extends BasePage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async expectSuccessMessageToContainLoginLink() {
|
async expectSuccessMessageToContainLoginLink() {
|
||||||
await expect(await this.successAlert.locator('a')).toHaveAttribute('href', '/login');
|
await expect(await this.successAlert.locator('a')).toHaveAttribute(
|
||||||
|
'href',
|
||||||
|
'/login'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
generateUser() {
|
generateUser() {
|
||||||
@@ -69,7 +74,7 @@ export class AdminSetupPage extends BasePage {
|
|||||||
fullName: faker.person.fullName(),
|
fullName: faker.person.fullName(),
|
||||||
email: faker.internet.email(),
|
email: faker.internet.email(),
|
||||||
password: faker.internet.password(),
|
password: faker.internet.password(),
|
||||||
wronglyRepeatedPassword: faker.internet.password()
|
wronglyRepeatedPassword: faker.internet.password(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
@@ -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",
|
||||||
|
@@ -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';
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
|
||||||
|
|
||||||
import useForgotPassword from 'hooks/useForgotPassword';
|
import useForgotPassword from 'hooks/useForgotPassword';
|
||||||
import Form from 'components/Form';
|
import Form from 'components/Form';
|
||||||
@@ -12,25 +12,17 @@ import useFormatMessage from 'hooks/useFormatMessage';
|
|||||||
export default function ForgotPasswordForm() {
|
export default function ForgotPasswordForm() {
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const {
|
const {
|
||||||
mutateAsync: forgotPassword,
|
mutate: forgotPassword,
|
||||||
isPending: loading,
|
isPending: loading,
|
||||||
isSuccess,
|
isSuccess,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
} = useForgotPassword();
|
} = useForgotPassword();
|
||||||
|
|
||||||
const handleSubmit = async (values) => {
|
const handleSubmit = ({ email }) => {
|
||||||
const { email } = values;
|
forgotPassword({
|
||||||
try {
|
email,
|
||||||
await forgotPassword({
|
});
|
||||||
email,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
enqueueSnackbar(
|
|
||||||
error?.message || formatMessage('forgotPasswordForm.error'),
|
|
||||||
{
|
|
||||||
variant: 'error',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -57,6 +49,16 @@ export default function ForgotPasswordForm() {
|
|||||||
margin="dense"
|
margin="dense"
|
||||||
autoComplete="username"
|
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
|
<LoadingButton
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@@ -68,14 +70,6 @@ export default function ForgotPasswordForm() {
|
|||||||
>
|
>
|
||||||
{formatMessage('forgotPasswordForm.submit')}
|
{formatMessage('forgotPasswordForm.submit')}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
{isSuccess && (
|
|
||||||
<Typography
|
|
||||||
variant="body1"
|
|
||||||
sx={{ color: (theme) => theme.palette.success.main }}
|
|
||||||
>
|
|
||||||
{formatMessage('forgotPasswordForm.instructionsSent')}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Form>
|
</Form>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
@@ -46,7 +46,12 @@ function Form(props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form onSubmit={methods.handleSubmit(onSubmit)} {...formProps}>
|
<form
|
||||||
|
onSubmit={methods.handleSubmit((data, event) =>
|
||||||
|
onSubmit?.(data, event, methods.setError),
|
||||||
|
)}
|
||||||
|
{...formProps}
|
||||||
|
>
|
||||||
{render ? render(methods) : children}
|
{render ? render(methods) : children}
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
@@ -2,35 +2,55 @@ import * as React from 'react';
|
|||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { Alert } from '@mui/material';
|
import Alert from '@mui/material/Alert';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
import * as yup from 'yup';
|
import * as yup from 'yup';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import Link from '@mui/material/Link';
|
import Link from '@mui/material/Link';
|
||||||
|
|
||||||
|
import { getGeneralErrorMessage } from 'helpers/errors';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useInstallation from 'hooks/useInstallation';
|
import useInstallation from 'hooks/useInstallation';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import Form from 'components/Form';
|
import Form from 'components/Form';
|
||||||
import TextField from 'components/TextField';
|
import TextField from 'components/TextField';
|
||||||
|
|
||||||
const validationSchema = yup.object().shape({
|
const getValidationSchema = (formatMessage) => {
|
||||||
fullName: yup.string().trim().required('installationForm.mandatoryInput'),
|
const getMandatoryInputMessage = (inputNameId) =>
|
||||||
email: yup
|
formatMessage('installationForm.mandatoryInput', {
|
||||||
.string()
|
inputName: formatMessage(inputNameId),
|
||||||
.trim()
|
});
|
||||||
.email('installationForm.validateEmail')
|
|
||||||
.required('installationForm.mandatoryInput'),
|
|
||||||
password: yup.string().required('installationForm.mandatoryInput'),
|
|
||||||
confirmPassword: yup
|
|
||||||
.string()
|
|
||||||
.required('installationForm.mandatoryInput')
|
|
||||||
.oneOf([yup.ref('password')], 'installationForm.passwordsMustMatch'),
|
|
||||||
});
|
|
||||||
|
|
||||||
const initialValues = {
|
return yup.object().shape({
|
||||||
|
fullName: yup
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.required(
|
||||||
|
getMandatoryInputMessage('installationForm.fullNameFieldLabel'),
|
||||||
|
),
|
||||||
|
email: yup
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.required(getMandatoryInputMessage('installationForm.emailFieldLabel'))
|
||||||
|
.email(formatMessage('installationForm.validateEmail')),
|
||||||
|
password: yup
|
||||||
|
.string()
|
||||||
|
.required(getMandatoryInputMessage('installationForm.passwordFieldLabel'))
|
||||||
|
.min(6, formatMessage('installationForm.passwordMinLength')),
|
||||||
|
confirmPassword: yup
|
||||||
|
.string()
|
||||||
|
.required(
|
||||||
|
getMandatoryInputMessage('installationForm.confirmPasswordFieldLabel'),
|
||||||
|
)
|
||||||
|
.oneOf(
|
||||||
|
[yup.ref('password')],
|
||||||
|
formatMessage('installationForm.passwordsMustMatch'),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
fullName: '',
|
fullName: '',
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
@@ -39,7 +59,7 @@ const initialValues = {
|
|||||||
|
|
||||||
function InstallationForm() {
|
function InstallationForm() {
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const install = useInstallation();
|
const { mutateAsync: install, isSuccess, isPending } = useInstallation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const handleOnRedirect = () => {
|
const handleOnRedirect = () => {
|
||||||
@@ -48,21 +68,38 @@ function InstallationForm() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (values) => {
|
const handleSubmit = async ({ fullName, email, password }, e, setError) => {
|
||||||
const { fullName, email, password } = values;
|
|
||||||
try {
|
try {
|
||||||
await install.mutateAsync({
|
await install({
|
||||||
fullName,
|
fullName,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
enqueueSnackbar(
|
const errors = error?.response?.data?.errors;
|
||||||
error?.message || formatMessage('installationForm.error'),
|
if (errors) {
|
||||||
{
|
const fieldNames = Object.keys(defaultValues);
|
||||||
variant: 'error',
|
Object.entries(errors).forEach(([fieldName, fieldErrors]) => {
|
||||||
},
|
if (fieldNames.includes(fieldName) && Array.isArray(fieldErrors)) {
|
||||||
);
|
setError(fieldName, {
|
||||||
|
type: 'fieldRequestError',
|
||||||
|
message: fieldErrors.join(', '),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const generalError = getGeneralErrorMessage({
|
||||||
|
error,
|
||||||
|
fallbackMessage: formatMessage('installationForm.error'),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (generalError) {
|
||||||
|
setError('root.general', {
|
||||||
|
type: 'requestError',
|
||||||
|
message: generalError,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -82,11 +119,13 @@ function InstallationForm() {
|
|||||||
{formatMessage('installationForm.title')}
|
{formatMessage('installationForm.title')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Form
|
<Form
|
||||||
defaultValues={initialValues}
|
automaticValidation={false}
|
||||||
|
noValidate
|
||||||
|
defaultValues={defaultValues}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
resolver={yupResolver(validationSchema)}
|
resolver={yupResolver(getValidationSchema(formatMessage))}
|
||||||
mode="onChange"
|
mode="onChange"
|
||||||
render={({ formState: { errors, touchedFields } }) => (
|
render={({ formState: { errors } }) => (
|
||||||
<>
|
<>
|
||||||
<TextField
|
<TextField
|
||||||
label={formatMessage('installationForm.fullNameFieldLabel')}
|
label={formatMessage('installationForm.fullNameFieldLabel')}
|
||||||
@@ -95,19 +134,12 @@ function InstallationForm() {
|
|||||||
margin="dense"
|
margin="dense"
|
||||||
autoComplete="fullName"
|
autoComplete="fullName"
|
||||||
data-test="fullName-text-field"
|
data-test="fullName-text-field"
|
||||||
error={touchedFields.fullName && !!errors?.fullName}
|
error={!!errors?.fullName}
|
||||||
helperText={
|
helperText={errors?.fullName?.message}
|
||||||
touchedFields.fullName && errors?.fullName?.message
|
|
||||||
? formatMessage(errors?.fullName?.message, {
|
|
||||||
inputName: formatMessage(
|
|
||||||
'installationForm.fullNameFieldLabel',
|
|
||||||
),
|
|
||||||
})
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
required
|
required
|
||||||
readOnly={install.isSuccess}
|
readOnly={isSuccess}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label={formatMessage('installationForm.emailFieldLabel')}
|
label={formatMessage('installationForm.emailFieldLabel')}
|
||||||
name="email"
|
name="email"
|
||||||
@@ -115,19 +147,12 @@ function InstallationForm() {
|
|||||||
margin="dense"
|
margin="dense"
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
data-test="email-text-field"
|
data-test="email-text-field"
|
||||||
error={touchedFields.email && !!errors?.email}
|
error={!!errors?.email}
|
||||||
helperText={
|
helperText={errors?.email?.message}
|
||||||
touchedFields.email && errors?.email?.message
|
|
||||||
? formatMessage(errors?.email?.message, {
|
|
||||||
inputName: formatMessage(
|
|
||||||
'installationForm.emailFieldLabel',
|
|
||||||
),
|
|
||||||
})
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
required
|
required
|
||||||
readOnly={install.isSuccess}
|
readOnly={isSuccess}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label={formatMessage('installationForm.passwordFieldLabel')}
|
label={formatMessage('installationForm.passwordFieldLabel')}
|
||||||
name="password"
|
name="password"
|
||||||
@@ -135,19 +160,12 @@ function InstallationForm() {
|
|||||||
margin="dense"
|
margin="dense"
|
||||||
type="password"
|
type="password"
|
||||||
data-test="password-text-field"
|
data-test="password-text-field"
|
||||||
error={touchedFields.password && !!errors?.password}
|
error={!!errors?.password}
|
||||||
helperText={
|
helperText={errors?.password?.message}
|
||||||
touchedFields.password && errors?.password?.message
|
|
||||||
? formatMessage(errors?.password?.message, {
|
|
||||||
inputName: formatMessage(
|
|
||||||
'installationForm.passwordFieldLabel',
|
|
||||||
),
|
|
||||||
})
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
required
|
required
|
||||||
readOnly={install.isSuccess}
|
readOnly={isSuccess}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label={formatMessage(
|
label={formatMessage(
|
||||||
'installationForm.confirmPasswordFieldLabel',
|
'installationForm.confirmPasswordFieldLabel',
|
||||||
@@ -157,52 +175,53 @@ function InstallationForm() {
|
|||||||
margin="dense"
|
margin="dense"
|
||||||
type="password"
|
type="password"
|
||||||
data-test="repeat-password-text-field"
|
data-test="repeat-password-text-field"
|
||||||
error={touchedFields.confirmPassword && !!errors?.confirmPassword}
|
error={!!errors?.confirmPassword}
|
||||||
helperText={
|
helperText={errors?.confirmPassword?.message}
|
||||||
touchedFields.confirmPassword &&
|
|
||||||
errors?.confirmPassword?.message
|
|
||||||
? formatMessage(errors?.confirmPassword?.message, {
|
|
||||||
inputName: formatMessage(
|
|
||||||
'installationForm.confirmPasswordFieldLabel',
|
|
||||||
),
|
|
||||||
})
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
required
|
required
|
||||||
readOnly={install.isSuccess}
|
readOnly={isSuccess}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{errors?.root?.general && (
|
||||||
|
<Alert data-test="error-alert" severity="error" sx={{ mt: 2 }}>
|
||||||
|
{errors.root.general.message}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isSuccess && (
|
||||||
|
<Alert
|
||||||
|
data-test="success-alert"
|
||||||
|
severity="success"
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
>
|
||||||
|
{formatMessage('installationForm.success', {
|
||||||
|
link: (str) => (
|
||||||
|
<Link
|
||||||
|
component={RouterLink}
|
||||||
|
to={URLS.LOGIN}
|
||||||
|
onClick={handleOnRedirect}
|
||||||
|
replace
|
||||||
|
>
|
||||||
|
{str}
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
sx={{ boxShadow: 2, mt: 3 }}
|
sx={{ boxShadow: 2, mt: 2 }}
|
||||||
loading={install.isPending}
|
loading={isPending}
|
||||||
disabled={install.isSuccess}
|
disabled={isSuccess}
|
||||||
fullWidth
|
fullWidth
|
||||||
data-test="signUp-button"
|
data-test="installation-button"
|
||||||
>
|
>
|
||||||
{formatMessage('installationForm.submit')}
|
{formatMessage('installationForm.submit')}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{install.isSuccess && (
|
|
||||||
<Alert data-test="success-alert" severity="success" sx={{ mt: 3 }}>
|
|
||||||
{formatMessage('installationForm.success', {
|
|
||||||
link: (str) => (
|
|
||||||
<Link
|
|
||||||
component={RouterLink}
|
|
||||||
to={URLS.LOGIN}
|
|
||||||
onClick={handleOnRedirect}
|
|
||||||
replace
|
|
||||||
>
|
|
||||||
{str}
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
})}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ import * as React from 'react';
|
|||||||
import { useNavigate, Link as RouterLink } from 'react-router-dom';
|
import { useNavigate, Link as RouterLink } from 'react-router-dom';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import Link from '@mui/material/Link';
|
import Link from '@mui/material/Link';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
import useAuthentication from 'hooks/useAuthentication';
|
import useAuthentication from 'hooks/useAuthentication';
|
||||||
@@ -11,7 +12,6 @@ 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 { Alert } from '@mui/material';
|
|
||||||
|
|
||||||
function LoginForm() {
|
function LoginForm() {
|
||||||
const isCloud = useCloud();
|
const isCloud = useCloud();
|
||||||
@@ -45,7 +45,7 @@ function LoginForm() {
|
|||||||
|
|
||||||
const renderError = () => {
|
const renderError = () => {
|
||||||
const errors = error?.response?.data?.errors?.general || [
|
const errors = error?.response?.data?.errors?.general || [
|
||||||
formatMessage('loginForm.error'),
|
error?.message || formatMessage('loginForm.error'),
|
||||||
];
|
];
|
||||||
|
|
||||||
return errors.map((error) => (
|
return errors.map((error) => (
|
||||||
|
@@ -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';
|
||||||
|
@@ -2,6 +2,7 @@ import { yupResolver } from '@hookform/resolvers/yup';
|
|||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
@@ -30,6 +31,8 @@ export default function ResetPasswordForm() {
|
|||||||
mutateAsync: resetPassword,
|
mutateAsync: resetPassword,
|
||||||
isPending,
|
isPending,
|
||||||
isSuccess,
|
isSuccess,
|
||||||
|
error,
|
||||||
|
isError,
|
||||||
} = useResetPassword();
|
} = useResetPassword();
|
||||||
const token = searchParams.get('token');
|
const token = searchParams.get('token');
|
||||||
|
|
||||||
@@ -47,14 +50,23 @@ export default function ResetPasswordForm() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
navigate(URLS.LOGIN);
|
navigate(URLS.LOGIN);
|
||||||
} catch (error) {
|
} catch {}
|
||||||
enqueueSnackbar(
|
};
|
||||||
error?.message || formatMessage('resetPasswordForm.error'),
|
|
||||||
{
|
const renderError = () => {
|
||||||
variant: 'error',
|
if (!isError) {
|
||||||
},
|
return null;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
@@ -96,7 +108,6 @@ export default function ResetPasswordForm() {
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label={formatMessage(
|
label={formatMessage(
|
||||||
'resetPasswordForm.confirmPasswordFieldLabel',
|
'resetPasswordForm.confirmPasswordFieldLabel',
|
||||||
@@ -117,7 +128,7 @@ export default function ResetPasswordForm() {
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{renderError()}
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
@@ -3,33 +3,52 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
import * as yup from 'yup';
|
import * as yup from 'yup';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
|
||||||
|
import { getGeneralErrorMessage } from 'helpers/errors';
|
||||||
import useAuthentication from 'hooks/useAuthentication';
|
import useAuthentication from 'hooks/useAuthentication';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import Form from 'components/Form';
|
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 useRegisterUser from 'hooks/useRegisterUser';
|
import useRegisterUser from 'hooks/useRegisterUser';
|
||||||
|
|
||||||
const validationSchema = yup.object().shape({
|
const getValidationSchema = (formatMessage) => {
|
||||||
fullName: yup.string().trim().required('signupForm.mandatoryInput'),
|
const getMandatoryInputMessage = (inputNameId) =>
|
||||||
email: yup
|
formatMessage('signupForm.mandatoryInput', {
|
||||||
.string()
|
inputName: formatMessage(inputNameId),
|
||||||
.trim()
|
});
|
||||||
.email('signupForm.validateEmail')
|
|
||||||
.required('signupForm.mandatoryInput'),
|
|
||||||
password: yup.string().required('signupForm.mandatoryInput'),
|
|
||||||
confirmPassword: yup
|
|
||||||
.string()
|
|
||||||
.required('signupForm.mandatoryInput')
|
|
||||||
.oneOf([yup.ref('password')], 'signupForm.passwordsMustMatch'),
|
|
||||||
});
|
|
||||||
|
|
||||||
const initialValues = {
|
return yup.object().shape({
|
||||||
|
fullName: yup
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.required(getMandatoryInputMessage('signupForm.fullNameFieldLabel')),
|
||||||
|
email: yup
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.required(getMandatoryInputMessage('signupForm.emailFieldLabel'))
|
||||||
|
.email(formatMessage('signupForm.validateEmail')),
|
||||||
|
password: yup
|
||||||
|
.string()
|
||||||
|
.required(getMandatoryInputMessage('signupForm.passwordFieldLabel'))
|
||||||
|
.min(6, formatMessage('signupForm.passwordMinLength')),
|
||||||
|
confirmPassword: yup
|
||||||
|
.string()
|
||||||
|
.required(
|
||||||
|
getMandatoryInputMessage('signupForm.confirmPasswordFieldLabel'),
|
||||||
|
)
|
||||||
|
.oneOf(
|
||||||
|
[yup.ref('password')],
|
||||||
|
formatMessage('signupForm.passwordsMustMatch'),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
fullName: '',
|
fullName: '',
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
@@ -40,7 +59,6 @@ function SignUpForm() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const authentication = useAuthentication();
|
const authentication = useAuthentication();
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
|
||||||
const { mutateAsync: registerUser, isPending: isRegisterUserPending } =
|
const { mutateAsync: registerUser, isPending: isRegisterUserPending } =
|
||||||
useRegisterUser();
|
useRegisterUser();
|
||||||
const { mutateAsync: createAccessToken, isPending: loginLoading } =
|
const { mutateAsync: createAccessToken, isPending: loginLoading } =
|
||||||
@@ -52,7 +70,7 @@ function SignUpForm() {
|
|||||||
}
|
}
|
||||||
}, [authentication.isAuthenticated]);
|
}, [authentication.isAuthenticated]);
|
||||||
|
|
||||||
const handleSubmit = async (values) => {
|
const handleSubmit = async (values, e, setError) => {
|
||||||
try {
|
try {
|
||||||
const { fullName, email, password } = values;
|
const { fullName, email, password } = values;
|
||||||
await registerUser({
|
await registerUser({
|
||||||
@@ -67,25 +85,28 @@ function SignUpForm() {
|
|||||||
const { token } = data;
|
const { token } = data;
|
||||||
authentication.updateToken(token);
|
authentication.updateToken(token);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errors = error?.response?.data?.errors
|
const errors = error?.response?.data?.errors;
|
||||||
? Object.values(error.response.data.errors)
|
if (errors) {
|
||||||
: [];
|
const fieldNames = Object.keys(defaultValues);
|
||||||
|
Object.entries(errors).forEach(([fieldName, fieldErrors]) => {
|
||||||
|
if (fieldNames.includes(fieldName) && Array.isArray(fieldErrors)) {
|
||||||
|
setError(fieldName, {
|
||||||
|
type: 'fieldRequestError',
|
||||||
|
message: fieldErrors.join(', '),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (errors.length) {
|
const generalError = getGeneralErrorMessage({
|
||||||
for (const [error] of errors) {
|
error,
|
||||||
enqueueSnackbar(error, {
|
fallbackMessage: formatMessage('signupForm.error'),
|
||||||
variant: 'error',
|
});
|
||||||
SnackbarProps: {
|
|
||||||
'data-test': 'snackbar-sign-up-error',
|
if (generalError) {
|
||||||
},
|
setError('root.general', {
|
||||||
});
|
type: 'requestError',
|
||||||
}
|
message: generalError,
|
||||||
} else {
|
|
||||||
enqueueSnackbar(error?.message || formatMessage('signupForm.error'), {
|
|
||||||
variant: 'error',
|
|
||||||
SnackbarProps: {
|
|
||||||
'data-test': 'snackbar-sign-up-error',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,11 +129,13 @@ function SignUpForm() {
|
|||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
defaultValues={initialValues}
|
automaticValidation={false}
|
||||||
|
noValidate
|
||||||
|
defaultValues={defaultValues}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
resolver={yupResolver(validationSchema)}
|
resolver={yupResolver(getValidationSchema(formatMessage))}
|
||||||
mode="onChange"
|
mode="onChange"
|
||||||
render={({ formState: { errors, touchedFields } }) => (
|
render={({ formState: { errors } }) => (
|
||||||
<>
|
<>
|
||||||
<TextField
|
<TextField
|
||||||
label={formatMessage('signupForm.fullNameFieldLabel')}
|
label={formatMessage('signupForm.fullNameFieldLabel')}
|
||||||
@@ -121,14 +144,9 @@ function SignUpForm() {
|
|||||||
margin="dense"
|
margin="dense"
|
||||||
autoComplete="fullName"
|
autoComplete="fullName"
|
||||||
data-test="fullName-text-field"
|
data-test="fullName-text-field"
|
||||||
error={touchedFields.fullName && !!errors?.fullName}
|
error={!!errors?.fullName}
|
||||||
helperText={
|
helperText={errors?.fullName?.message}
|
||||||
touchedFields.fullName && errors?.fullName?.message
|
required
|
||||||
? formatMessage(errors?.fullName?.message, {
|
|
||||||
inputName: formatMessage('signupForm.fullNameFieldLabel'),
|
|
||||||
})
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
@@ -138,14 +156,9 @@ function SignUpForm() {
|
|||||||
margin="dense"
|
margin="dense"
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
data-test="email-text-field"
|
data-test="email-text-field"
|
||||||
error={touchedFields.email && !!errors?.email}
|
error={!!errors?.email}
|
||||||
helperText={
|
helperText={errors?.email?.message}
|
||||||
touchedFields.email && errors?.email?.message
|
required
|
||||||
? formatMessage(errors?.email?.message, {
|
|
||||||
inputName: formatMessage('signupForm.emailFieldLabel'),
|
|
||||||
})
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
@@ -154,14 +167,9 @@ function SignUpForm() {
|
|||||||
fullWidth
|
fullWidth
|
||||||
margin="dense"
|
margin="dense"
|
||||||
type="password"
|
type="password"
|
||||||
error={touchedFields.password && !!errors?.password}
|
error={!!errors?.password}
|
||||||
helperText={
|
helperText={errors?.password?.message}
|
||||||
touchedFields.password && errors?.password?.message
|
required
|
||||||
? formatMessage(errors?.password?.message, {
|
|
||||||
inputName: formatMessage('signupForm.passwordFieldLabel'),
|
|
||||||
})
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
@@ -170,19 +178,21 @@ function SignUpForm() {
|
|||||||
fullWidth
|
fullWidth
|
||||||
margin="dense"
|
margin="dense"
|
||||||
type="password"
|
type="password"
|
||||||
error={touchedFields.confirmPassword && !!errors?.confirmPassword}
|
error={!!errors?.confirmPassword}
|
||||||
helperText={
|
helperText={errors?.confirmPassword?.message}
|
||||||
touchedFields.confirmPassword &&
|
required
|
||||||
errors?.confirmPassword?.message
|
|
||||||
? formatMessage(errors?.confirmPassword?.message, {
|
|
||||||
inputName: formatMessage(
|
|
||||||
'signupForm.confirmPasswordFieldLabel',
|
|
||||||
),
|
|
||||||
})
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{errors?.root?.general && (
|
||||||
|
<Alert
|
||||||
|
data-test="alert-sign-up-error"
|
||||||
|
severity="error"
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
>
|
||||||
|
{errors.root.general.message}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
@@ -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';
|
||||||
|
18
packages/web/src/helpers/errors.js
Normal file
18
packages/web/src/helpers/errors.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// Helpers to extract errors received from the API
|
||||||
|
|
||||||
|
export const getGeneralErrorMessage = ({ error, fallbackMessage }) => {
|
||||||
|
if (!error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = error?.response?.data?.errors;
|
||||||
|
const generalError = errors?.general;
|
||||||
|
|
||||||
|
if (generalError && Array.isArray(generalError)) {
|
||||||
|
return generalError.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!errors) {
|
||||||
|
return error?.message || fallbackMessage;
|
||||||
|
}
|
||||||
|
};
|
@@ -138,6 +138,7 @@
|
|||||||
"installationForm.submit": "Create admin",
|
"installationForm.submit": "Create admin",
|
||||||
"installationForm.validateEmail": "Email must be valid.",
|
"installationForm.validateEmail": "Email must be valid.",
|
||||||
"installationForm.passwordsMustMatch": "Passwords must match.",
|
"installationForm.passwordsMustMatch": "Passwords must match.",
|
||||||
|
"installationForm.passwordMinLength": "Password must be at least 6 characters long.",
|
||||||
"installationForm.mandatoryInput": "{inputName} is required.",
|
"installationForm.mandatoryInput": "{inputName} is required.",
|
||||||
"installationForm.success": "The admin account has been created, and thus, the installation has been completed. You can now log in <link>here</link>.",
|
"installationForm.success": "The admin account has been created, and thus, the installation has been completed. You can now log in <link>here</link>.",
|
||||||
"installationForm.error": "Something went wrong. Please try again.",
|
"installationForm.error": "Something went wrong. Please try again.",
|
||||||
@@ -149,6 +150,7 @@
|
|||||||
"signupForm.submit": "Sign up",
|
"signupForm.submit": "Sign up",
|
||||||
"signupForm.validateEmail": "Email must be valid.",
|
"signupForm.validateEmail": "Email must be valid.",
|
||||||
"signupForm.passwordsMustMatch": "Passwords must match.",
|
"signupForm.passwordsMustMatch": "Passwords must match.",
|
||||||
|
"signupForm.passwordMinLength": "Password must be at least 6 characters long.",
|
||||||
"signupForm.mandatoryInput": "{inputName} is required.",
|
"signupForm.mandatoryInput": "{inputName} is required.",
|
||||||
"signupForm.error": "Something went wrong. Please try again.",
|
"signupForm.error": "Something went wrong. Please try again.",
|
||||||
"loginForm.title": "Login",
|
"loginForm.title": "Login",
|
||||||
|
@@ -66,8 +66,8 @@ function RoleMappings({ provider, providerLoading }) {
|
|||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
const enqueueSnackbar = useEnqueueSnackbar();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutateAsync: updateSamlAuthProvidersRoleMappings,
|
mutateAsync: updateRoleMappings,
|
||||||
isPending: isUpdateSamlAuthProvidersRoleMappingsPending,
|
isPending: isUpdateRoleMappingsPending,
|
||||||
} = useAdminUpdateSamlAuthProviderRoleMappings(provider?.id);
|
} = useAdminUpdateSamlAuthProviderRoleMappings(provider?.id);
|
||||||
|
|
||||||
const { data, isLoading: isAdminSamlAuthProviderRoleMappingsLoading } =
|
const { data, isLoading: isAdminSamlAuthProviderRoleMappingsLoading } =
|
||||||
@@ -79,7 +79,7 @@ function RoleMappings({ provider, providerLoading }) {
|
|||||||
const handleRoleMappingsUpdate = async (values) => {
|
const handleRoleMappingsUpdate = async (values) => {
|
||||||
try {
|
try {
|
||||||
if (provider?.id) {
|
if (provider?.id) {
|
||||||
await updateSamlAuthProvidersRoleMappings(
|
await updateRoleMappings(
|
||||||
values.roleMappings.map(({ roleId, remoteRoleName }) => ({
|
values.roleMappings.map(({ roleId, remoteRoleName }) => ({
|
||||||
roleId,
|
roleId,
|
||||||
remoteRoleName,
|
remoteRoleName,
|
||||||
@@ -148,7 +148,7 @@ function RoleMappings({ provider, providerLoading }) {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
sx={{ boxShadow: 2 }}
|
sx={{ boxShadow: 2 }}
|
||||||
loading={isUpdateSamlAuthProvidersRoleMappingsPending}
|
loading={isUpdateRoleMappingsPending}
|
||||||
>
|
>
|
||||||
{formatMessage('roleMappingsForm.save')}
|
{formatMessage('roleMappingsForm.save')}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
|
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